update Piwik to version 2.16 (fixes #91)

This commit is contained in:
oliver 2016-04-10 18:55:57 +02:00
commit 47263617c5
79 changed files with 418860 additions and 226988 deletions

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -9,78 +9,144 @@
namespace Piwik\Plugins\CoreAdminHome;
use Exception;
use Piwik\Config;
use Piwik\DataAccess\ArchiveTableCreator;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Piwik\Container\StaticContainer;
use Piwik\Archive\ArchiveInvalidator;
use Piwik\CronArchive;
use Piwik\Date;
use Piwik\Db;
use Piwik\Option;
use Piwik\Period;
use Piwik\Period\Week;
use Piwik\Piwik;
use Piwik\Plugins\PrivacyManager\PrivacyManager;
use Piwik\Plugins\SitesManager\SitesManager;
use Piwik\SettingsPiwik;
use Piwik\Segment;
use Piwik\Scheduler\Scheduler;
use Piwik\Site;
use Piwik\TaskScheduler;
/**
* @method static \Piwik\Plugins\CoreAdminHome\API getInstance()
*/
class API extends \Piwik\Plugin\API
{
/**
* @var Scheduler
*/
private $scheduler;
/**
* @var ArchiveInvalidator
*/
private $invalidator;
public function __construct(Scheduler $scheduler, ArchiveInvalidator $invalidator)
{
$this->scheduler = $scheduler;
$this->invalidator = $invalidator;
}
/**
* Will run all scheduled tasks due to run at this time.
*
* @return array
* @hideExceptForSuperUser
*/
public function runScheduledTasks()
{
Piwik::checkUserHasSuperUserAccess();
return TaskScheduler::runTasks();
return $this->scheduler->run();
}
/*
* stores the list of websites IDs to re-reprocess in archive.php
*/
const OPTION_INVALIDATED_IDSITES = 'InvalidatedOldReports_WebsiteIds';
/**
* When tracking data in the past (using Tracking API), this function
* can be used to invalidate reports for the idSites and dates where new data
* was added.
* DEV: If you call this API, the UI should display the data correctly, but will process
* in real time, which could be very slow after large data imports.
* After calling this function via REST, you can manually force all data
* to be reprocessed by visiting the script as the Super User:
* http://example.net/piwik/misc/cron/archive.php?token_auth=$SUPER_USER_TOKEN_AUTH_HERE
* REQUIREMENTS: On large piwik setups, you will need in PHP configuration: max_execution_time = 0
* We recommend to use an hourly schedule of the script at misc/cron/archive.php
* More information: http://piwik.org/setup-auto-archiving/
* Invalidates report data, forcing it to be recomputed during the next archiving run.
*
* @param string $idSites Comma separated list of idSite that have had data imported for the specified dates
* @param string $dates Comma separated list of dates to invalidate for all these websites
* Note: This is done automatically when tracking or importing visits in the past.
*
* @param string $idSites Comma separated list of site IDs to invalidate reports for.
* @param string $dates Comma separated list of dates of periods to invalidate reports for.
* @param string|bool $period The type of period to invalidate: either 'day', 'week', 'month', 'year', 'range'.
* The command will automatically cascade up, invalidating reports for parent periods as
* well. So invalidating a day will invalidate the week it's in, the month it's in and the
* year it's in, since those periods will need to be recomputed too.
* @param string|bool $segment Optional. The segment to invalidate reports for.
* @param bool $cascadeDown If true, child periods will be invalidated as well. So if it is requested to invalidate
* a month, then all the weeks and days within that month will also be invalidated. But only
* if this parameter is set.
* @throws Exception
* @return array
* @hideExceptForSuperUser
*/
public function invalidateArchivedReports($idSites, $dates)
public function invalidateArchivedReports($idSites, $dates, $period = false, $segment = false, $cascadeDown = false)
{
$idSites = Site::getIdSitesFromIdSitesString($idSites);
if (empty($idSites)) {
throw new Exception("Specify a value for &idSites= as a comma separated list of website IDs, for which your token_auth has 'admin' permission");
}
Piwik::checkUserHasAdminAccess($idSites);
// Ensure the specified dates are valid
$toInvalidate = $invalidDates = array();
$dates = explode(',', $dates);
if (!empty($segment)) {
$segment = new Segment($segment, $idSites);
} else {
$segment = null;
}
list($dateObjects, $invalidDates) = $this->getDatesToInvalidateFromString($dates);
$invalidationResult = $this->invalidator->markArchivesAsInvalidated($idSites, $dateObjects, $period, $segment, (bool)$cascadeDown);
$output = $invalidationResult->makeOutputLogs();
if ($invalidDates) {
$output[] = 'Warning: some of the Dates to invalidate were invalid: ' .
implode(", ", $invalidDates) . ". Piwik simply ignored those and proceeded with the others.";
}
Site::clearCache(); // TODO: is this needed? it shouldn't be needed...
return $invalidationResult->makeOutputLogs();
}
/**
* Initiates cron archiving via web request.
*
* @hideExceptForSuperUser
*/
public function runCronArchiving()
{
Piwik::checkUserHasSuperUserAccess();
// HTTP request: logs needs to be dumped in the HTTP response (on top of existing log destinations)
/** @var \Monolog\Logger $logger */
$logger = StaticContainer::get('Psr\Log\LoggerInterface');
$handler = new StreamHandler('php://output', Logger::INFO);
$handler->setFormatter(StaticContainer::get('Piwik\Plugins\Monolog\Formatter\LineMessageFormatter'));
$logger->pushHandler($handler);
$archiver = new CronArchive();
$archiver->main();
}
/**
* Ensure the specified dates are valid.
* Store invalid date so we can log them
* @param array $dates
* @return Date[]
*/
private function getDatesToInvalidateFromString($dates)
{
$toInvalidate = array();
$invalidDates = array();
$dates = explode(',', trim($dates));
$dates = array_unique($dates);
foreach ($dates as $theDate) {
$theDate = trim($theDate);
try {
$date = Date::factory($theDate);
} catch (Exception $e) {
} catch (\Exception $e) {
$invalidDates[] = $theDate;
continue;
}
if ($date->toString() == $theDate) {
$toInvalidate[] = $date;
} else {
@ -88,122 +154,6 @@ class API extends \Piwik\Plugin\API
}
}
// If using the feature "Delete logs older than N days"...
$purgeDataSettings = PrivacyManager::getPurgeDataSettings();
$logsAreDeletedBeforeThisDate = $purgeDataSettings['delete_logs_schedule_lowest_interval'];
$logsDeleteEnabled = $purgeDataSettings['delete_logs_enable'];
$minimumDateWithLogs = false;
if ($logsDeleteEnabled
&& $logsAreDeletedBeforeThisDate
) {
$minimumDateWithLogs = Date::factory('today')->subDay($logsAreDeletedBeforeThisDate);
}
// Given the list of dates, process which tables they should be deleted from
$minDate = false;
$warningDates = $processedDates = array();
/* @var $date Date */
foreach ($toInvalidate as $date) {
// we should only delete reports for dates that are more recent than N days
if ($minimumDateWithLogs
&& $date->isEarlier($minimumDateWithLogs)
) {
$warningDates[] = $date->toString();
} else {
$processedDates[] = $date->toString();
}
$month = $date->toString('Y_m');
// For a given date, we must invalidate in the monthly archive table
$datesByMonth[$month][] = $date->toString();
// But also the year stored in January
$year = $date->toString('Y_01');
$datesByMonth[$year][] = $date->toString();
// but also weeks overlapping several months stored in the month where the week is starting
/* @var $week Week */
$week = Period::factory('week', $date);
$weekAsString = $week->getDateStart()->toString('Y_m');
$datesByMonth[$weekAsString][] = $date->toString();
// Keep track of the minimum date for each website
if ($minDate === false
|| $date->isEarlier($minDate)
) {
$minDate = $date;
}
}
// In each table, invalidate day/week/month/year containing this date
$archiveTables = ArchiveTableCreator::getTablesArchivesInstalled();
foreach ($archiveTables as $table) {
// Extract Y_m from table name
$suffix = ArchiveTableCreator::getDateFromTableName($table);
if (!isset($datesByMonth[$suffix])) {
continue;
}
// Dates which are to be deleted from this table
$datesToDeleteInTable = $datesByMonth[$suffix];
// Build one statement to delete all dates from the given table
$sql = $bind = array();
$datesToDeleteInTable = array_unique($datesToDeleteInTable);
foreach ($datesToDeleteInTable as $dateToDelete) {
$sql[] = '(date1 <= ? AND ? <= date2)';
$bind[] = $dateToDelete;
$bind[] = $dateToDelete;
}
$sql = implode(" OR ", $sql);
$query = "DELETE FROM $table " .
" WHERE ( $sql ) " .
" AND idsite IN (" . implode(",", $idSites) . ")";
Db::query($query, $bind);
}
\Piwik\Plugins\SitesManager\API::getInstance()->updateSiteCreatedTime($idSites, $minDate);
// Force to re-process data for these websites in the next archive.php cron run
$invalidatedIdSites = self::getWebsiteIdsToInvalidate();
$invalidatedIdSites = array_merge($invalidatedIdSites, $idSites);
$invalidatedIdSites = array_unique($invalidatedIdSites);
$invalidatedIdSites = array_values($invalidatedIdSites);
Option::set(self::OPTION_INVALIDATED_IDSITES, serialize($invalidatedIdSites));
Site::clearCache();
$output = array();
// output logs
if ($warningDates) {
$output[] = 'Warning: the following Dates have not been invalidated, because they are earlier than your Log Deletion limit: ' .
implode(", ", $warningDates) .
"\n The last day with logs is " . $minimumDateWithLogs . ". " .
"\n Please disable 'Delete old Logs' or set it to a higher deletion threshold (eg. 180 days or 365 years).'.";
}
$output[] = "Success. The following dates were invalidated successfully: " .
implode(", ", $processedDates);
return $output;
return array($toInvalidate, $invalidDates);
}
/**
* Returns array of idSites to force re-process next time archive.php runs
*
* @ignore
* @return mixed
*/
static public function getWebsiteIdsToInvalidate()
{
Piwik::checkUserHasSomeAdminAccess();
Option::clearCachedOption(self::OPTION_INVALIDATED_IDSITES);
$invalidatedIdSites = Option::get(self::OPTION_INVALIDATED_IDSITES);
if ($invalidatedIdSites
&& ($invalidatedIdSites = unserialize($invalidatedIdSites))
&& count($invalidatedIdSites)
) {
return $invalidatedIdSites;
}
return array();
}
}
}

View file

@ -0,0 +1,201 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugins\CoreAdminHome\Commands;
use Piwik\Common;
use Piwik\Container\StaticContainer;
use Piwik\DataAccess\RawLogDao;
use Piwik\Date;
use Piwik\Db;
use Piwik\LogDeleter;
use Piwik\Plugin\ConsoleCommand;
use Piwik\Site;
use Piwik\Timer;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
/**
* Command to selectively delete visits.
*/
class DeleteLogsData extends ConsoleCommand
{
private static $logTables = array(
'log_visit',
'log_link_visit_action',
'log_conversion',
'log_conversion_item',
'log_action'
);
/**
* @var RawLogDao
*/
private $rawLogDao;
/**
* @var LogDeleter
*/
private $logDeleter;
public function __construct(LogDeleter $logDeleter = null, RawLogDao $rawLogDao = null)
{
parent::__construct();
$this->logDeleter = $logDeleter ?: StaticContainer::get('Piwik\LogDeleter');
$this->rawLogDao = $rawLogDao ?: StaticContainer::get('Piwik\DataAccess\RawLogDao');
}
protected function configure()
{
$this->setName('core:delete-logs-data');
$this->setDescription('Delete data from the user log tables: ' . implode(', ', self::$logTables) . '.');
$this->addOption('dates', null, InputOption::VALUE_REQUIRED, 'Delete log data with a date within this date range. Eg, 2012-01-01,2013-01-01');
$this->addOption('idsite', null, InputOption::VALUE_OPTIONAL,
'Delete log data belonging to the site with this ID. Comma separated list of website id. Eg, 1, 2, 3, etc. By default log data from all sites is purged.');
$this->addOption('limit', null, InputOption::VALUE_REQUIRED, "The number of rows to delete at a time. The larger the number, "
. "the more time is spent deleting logs, and the less progress will be printed to the screen.", 1000);
$this->addOption('optimize-tables', null, InputOption::VALUE_NONE,
"If supplied, the command will optimize log tables after deleting logs. Note: this can take a very long time.");
}
protected function execute(InputInterface $input, OutputInterface $output)
{
list($from, $to) = $this->getDateRangeToDeleteFrom($input);
$idSite = $this->getSiteToDeleteFrom($input);
$step = $this->getRowIterationStep($input);
$output->writeln( sprintf(
"<info>Preparing to delete all visits belonging to %s between $from and $to.</info>",
$idSite ? "website $idSite" : "ALL websites"
));
$confirm = $this->askForDeleteConfirmation($input, $output);
if (!$confirm) {
return;
}
$timer = new Timer();
try {
$logsDeleted = $this->logDeleter->deleteVisitsFor($from, $to, $idSite, $step, function () use ($output) {
$output->write('.');
});
} catch (\Exception $ex) {
$output->writeln("");
throw $ex;
}
$this->writeSuccessMessage($output, array(
"Successfully deleted $logsDeleted visits. <comment>" . $timer . "</comment>"));
if ($input->getOption('optimize-tables')) {
$this->optimizeTables($output);
}
}
/**
* @param InputInterface $input
* @return Date[]
*/
private function getDateRangeToDeleteFrom(InputInterface $input)
{
$dates = $input->getOption('dates');
if (empty($dates)) {
throw new \InvalidArgumentException("No date range supplied in --dates option. Deleting all logs by default is not allowed, you must specify a date range.");
}
$parts = explode(',', $dates);
$parts = array_map('trim', $parts);
if (count($parts) !== 2) {
throw new \InvalidArgumentException("Invalid date range supplied: $dates");
}
list($start, $end) = $parts;
try {
/** @var Date[] $dateObjects */
$dateObjects = array(Date::factory($start), Date::factory($end));
} catch (\Exception $ex) {
throw new \InvalidArgumentException("Invalid date range supplied: $dates (" . $ex->getMessage() . ")", $code = 0, $ex);
}
if ($dateObjects[0]->getTimestamp() > $dateObjects[1]->getTimestamp()) {
throw new \InvalidArgumentException("Invalid date range supplied: $dates (first date is older than the last date)");
}
$dateObjects = array($dateObjects[0]->getDatetime(), $dateObjects[1]->getDatetime());
return $dateObjects;
}
private function getSiteToDeleteFrom(InputInterface $input)
{
$idSite = $input->getOption('idsite');
if(is_null($idSite)) {
return $idSite;
}
// validate the site ID
try {
new Site($idSite);
} catch (\Exception $ex) {
throw new \InvalidArgumentException("Invalid site ID: $idSite", $code = 0, $ex);
}
return $idSite;
}
private function getRowIterationStep(InputInterface $input)
{
$step = (int) $input->getOption('limit');
if ($step <= 0) {
throw new \InvalidArgumentException("Invalid row limit supplied: $step. Must be a number greater than 0.");
}
return $step;
}
private function askForDeleteConfirmation(InputInterface $input, OutputInterface $output)
{
if ($input->getOption('no-interaction')) {
return true;
}
$helper = $this->getHelper('question');
$question = new ConfirmationQuestion('<comment>You are about to delete log data. This action cannot be undone, are you sure you want to continue? (Y/N)</comment> ', false);
return $helper->ask($input, $output, $question);
}
private function optimizeTables(OutputInterface $output)
{
foreach (self::$logTables as $table) {
$output->write("Optimizing table $table... ");
$timer = new Timer();
$prefixedTable = Common::prefixTable($table);
$done = Db::optimizeTables($prefixedTable);
if($done) {
$output->writeln("done. <comment>" . $timer . "</comment>");
} else {
$output->writeln("skipped! <comment>" . $timer . "</comment>");
}
}
$this->writeSuccessMessage($output, array("Table optimization finished."));
}
}

View file

@ -0,0 +1,201 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\CoreAdminHome\Commands;
use Piwik\Common;
use Piwik\Container\StaticContainer;
use Piwik\DataAccess\Actions;
use Piwik\Archive\ArchiveInvalidator;
use Piwik\Date;
use Piwik\Plugin\ConsoleCommand;
use Piwik\Plugins\CoreAdminHome\Model\DuplicateActionRemover;
use Piwik\Timer;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Finds duplicate actions rows in log_action and removes them. Fixes references to duplicate
* actions in the log_link_visit_action table, log_conversion table, and log_conversion_item
* table.
*
* Prior to version 2.11, there was a race condition in the tracker where it was possible for
* two or more actions with the same name and type to be inserted simultaneously. This resulted
* in inaccurate data. A Piwik database with this problem can be fixed using this class.
*
* With version 2.11 and above, it is still possible for duplicate actions to be inserted, but
* ONLY if the tracker's PHP process fails suddenly right after inserting an action. This is
* very rare, and even if it does happen, report data will not be affected, but the extra
* actions can be deleted w/ this class.
*/
class FixDuplicateLogActions extends ConsoleCommand
{
/**
* Used to invalidate archives. Only used if $shouldInvalidateArchives is true.
*
* @var ArchiveInvalidator
*/
private $archiveInvalidator;
/**
* DAO used to find duplicate actions in log_action and fix references to them in other tables.
*
* @var DuplicateActionRemover
*/
private $duplicateActionRemover;
/**
* DAO used to remove actions from the log_action table.
*
* @var Actions
*/
private $actionsAccess;
/**
* @var LoggerInterface
*/
private $logger;
/**
* Constructor.
*
* @param ArchiveInvalidator $invalidator
* @param DuplicateActionRemover $duplicateActionRemover
* @param Actions $actionsAccess
* @param LoggerInterface $logger
*/
public function __construct(ArchiveInvalidator $invalidator = null, DuplicateActionRemover $duplicateActionRemover = null,
Actions $actionsAccess = null, LoggerInterface $logger = null)
{
parent::__construct();
$this->archiveInvalidator = $invalidator ?: StaticContainer::get('Piwik\Archive\ArchiveInvalidator');
$this->duplicateActionRemover = $duplicateActionRemover ?: new DuplicateActionRemover();
$this->actionsAccess = $actionsAccess ?: new Actions();
$this->logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface');
}
protected function configure()
{
$this->setName('core:fix-duplicate-log-actions');
$this->addOption('invalidate-archives', null, InputOption::VALUE_NONE, "If supplied, archives for logs that use duplicate actions will be invalidated."
. " On the next cron archive run, the reports for those dates will be re-processed.");
$this->setDescription('Removes duplicates in the log action table and fixes references to the duplicates in '
. 'related tables. NOTE: This action can take a long time to run!');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$invalidateArchives = $input->getOption('invalidate-archives');
$timer = new Timer();
$duplicateActions = $this->duplicateActionRemover->getDuplicateIdActions();
if (empty($duplicateActions)) {
$output->writeln("Found no duplicate actions.");
return;
}
$output->writeln("<info>Found " . count($duplicateActions) . " actions with duplicates.</info>");
list($numberRemoved, $allArchivesAffected) = $this->fixDuplicateActionReferences($duplicateActions, $output);
$this->deleteDuplicatesFromLogAction($output, $duplicateActions);
if ($invalidateArchives) {
$this->invalidateArchivesUsingActionDuplicates($allArchivesAffected, $output);
} else {
$this->printAffectedArchives($allArchivesAffected, $output);
}
$logActionTable = Common::prefixTable('log_action');
$this->writeSuccessMessage($output, array(
"Found and deleted $numberRemoved duplicate action entries in the $logActionTable table.",
"References in log_link_visit_action, log_conversion and log_conversion_item were corrected.",
$timer->__toString()
));
}
private function invalidateArchivesUsingActionDuplicates($archivesAffected, OutputInterface $output)
{
$output->write("Invalidating archives affected by duplicates fixed...");
foreach ($archivesAffected as $archiveInfo) {
$dates = array(Date::factory($archiveInfo['server_time']));
$this->archiveInvalidator->markArchivesAsInvalidated(array($archiveInfo['idsite']), $dates, $period = false);
}
$output->writeln("Done.");
}
private function printAffectedArchives($allArchivesAffected, OutputInterface $output)
{
$output->writeln("The following archives used duplicate actions and should be invalidated if you want correct reports:");
foreach ($allArchivesAffected as $archiveInfo) {
$output->writeln("\t[ idSite = {$archiveInfo['idsite']}, date = {$archiveInfo['server_time']} ]");
}
}
private function fixDuplicateActionReferences($duplicateActions, OutputInterface $output)
{
$dupeCount = count($duplicateActions);
$numberRemoved = 0;
$allArchivesAffected = array();
foreach ($duplicateActions as $index => $dupeInfo) {
$name = $dupeInfo['name'];
$toIdAction = $dupeInfo['idaction'];
$fromIdActions = $dupeInfo['duplicateIdActions'];
$numberRemoved += count($fromIdActions);
$output->writeln("<info>[$index / $dupeCount]</info> Fixing duplicates for '$name'");
$this->logger->debug(" idaction = {idaction}, duplicate idactions = {duplicateIdActions}", array(
'idaction' => $toIdAction,
'duplicateIdActions' => $fromIdActions
));
foreach (DuplicateActionRemover::$tablesWithIdActionColumns as $table) {
$archivesAffected = $this->fixDuplicateActionsInTable($output, $table, $toIdAction, $fromIdActions);
$allArchivesAffected = array_merge($allArchivesAffected, $archivesAffected);
}
}
$allArchivesAffected = array_values(array_unique($allArchivesAffected, SORT_REGULAR));
return array($numberRemoved, $allArchivesAffected);
}
private function fixDuplicateActionsInTable(OutputInterface $output, $table, $toIdAction, $fromIdActions)
{
$timer = new Timer();
$archivesAffected = $this->duplicateActionRemover->getSitesAndDatesOfRowsUsingDuplicates($table, $fromIdActions);
$this->duplicateActionRemover->fixDuplicateActionsInTable($table, $toIdAction, $fromIdActions);
$output->writeln("\tFixed duplicates in " . Common::prefixTable($table) . ". <comment>" . $timer->__toString() . "</comment>.");
return $archivesAffected;
}
private function deleteDuplicatesFromLogAction(OutputInterface $output, $duplicateActions)
{
$logActionTable = Common::prefixTable('log_action');
$output->writeln("<info>Deleting duplicate actions from $logActionTable...</info>");
$idActions = array();
foreach ($duplicateActions as $dupeInfo) {
$idActions = array_merge($idActions, $dupeInfo['duplicateIdActions']);
}
$this->actionsAccess->delete($idActions);
}
}

View file

@ -0,0 +1,200 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugins\CoreAdminHome\Commands;
use Piwik\Container\StaticContainer;
use Piwik\Date;
use Piwik\Period;
use Piwik\Period\Range;
use Piwik\Piwik;
use Piwik\Segment;
use Piwik\Plugin\ConsoleCommand;
use Piwik\Plugins\SitesManager\API as SitesManagerAPI;
use Piwik\Site;
use Piwik\Period\Factory as PeriodFactory;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Provides a simple interface for invalidating report data by date ranges, site IDs and periods.
*/
class InvalidateReportData extends ConsoleCommand
{
const ALL_OPTION_VALUE = 'all';
protected function configure()
{
$this->setName('core:invalidate-report-data');
$this->setDescription('Invalidate archived report data by date range, site and period.');
$this->addOption('dates', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
'List of dates or date ranges to invalidate report data for, eg, 2015-01-03 or 2015-01-05,2015-02-12.');
$this->addOption('sites', null, InputOption::VALUE_REQUIRED,
'List of site IDs to invalidate report data for, eg, "1,2,3,4" or "all" for all sites.',
self::ALL_OPTION_VALUE);
$this->addOption('periods', null, InputOption::VALUE_REQUIRED,
'List of period types to invalidate report data for. Can be one or more of the following values: day, '
. 'week, month, year or "all" for all of them.',
self::ALL_OPTION_VALUE);
$this->addOption('segment', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
'List of segments to invalidate report data for.');
$this->addOption('cascade', null, InputOption::VALUE_NONE,
'If supplied, invalidation will cascade, invalidating child period types even if they aren\'t specified in'
. ' --periods. For example, if --periods=week, --cascade will cause the days within those weeks to be '
. 'invalidated as well. If --periods=month, then weeks and days will be invalidated. Note: if a period '
. 'falls partly outside of a date range, then --cascade will also invalidate data for child periods '
. 'outside the date range. For example, if --dates=2015-09-14,2015-09-15 & --periods=week, --cascade will'
. ' also invalidate all days within 2015-09-13,2015-09-19, even those outside the date range.');
$this->addOption('dry-run', null, InputOption::VALUE_NONE, 'For tests. Runs the command w/o actually '
. 'invalidating anything.');
$this->setHelp('Invalidate archived report data by date range, site and period. Invalidated archive data will '
. 'be re-archived during the next core:archive run. If your log data has changed for some reason, this '
. 'command can be used to make sure reports are generated using the new, changed log data.');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$invalidator = StaticContainer::get('Piwik\Archive\ArchiveInvalidator');
$cascade = $input->getOption('cascade');
$dryRun = $input->getOption('dry-run');
$sites = $this->getSitesToInvalidateFor($input);
$periodTypes = $this->getPeriodTypesToInvalidateFor($input);
$dateRanges = $this->getDateRangesToInvalidateFor($input);
$segments = $this->getSegmentsToInvalidateFor($input, $sites);
foreach ($periodTypes as $periodType) {
foreach ($dateRanges as $dateRange) {
foreach ($segments as $segment) {
$segmentStr = $segment ? $segment->getString() : '';
$output->writeln("Invalidating $periodType periods in $dateRange [segment = $segmentStr]...");
$dates = $this->getPeriodDates($periodType, $dateRange);
if ($dryRun) {
$output->writeln("[Dry-run] invalidating archives for site = [ " . implode(', ', $sites)
. " ], dates = [ " . implode(', ', $dates) . " ], period = [ $periodType ], segment = [ "
. "$segmentStr ], cascade = [ " . (int)$cascade . " ]");
} else {
$invalidationResult = $invalidator->markArchivesAsInvalidated($sites, $dates, $periodType, $segment, $cascade);
if ($output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) {
$output->writeln($invalidationResult->makeOutputLogs());
}
}
}
}
}
}
private function getSitesToInvalidateFor(InputInterface $input)
{
$sites = $input->getOption('sites');
$siteIds = Site::getIdSitesFromIdSitesString($sites);
if (empty($siteIds)) {
throw new \InvalidArgumentException("Invalid --sites value: '$sites'.");
}
$allSiteIds = SitesManagerAPI::getInstance()->getAllSitesId();
foreach ($siteIds as $idSite) {
if (!in_array($idSite, $allSiteIds)) {
throw new \InvalidArgumentException("Invalid --sites value: '$sites', there are no sites with IDs = $idSite");
}
}
return $siteIds;
}
private function getPeriodTypesToInvalidateFor(InputInterface $input)
{
$periods = $input->getOption('periods');
if (empty($periods)) {
throw new \InvalidArgumentException("The --periods argument is required.");
}
if ($periods == self::ALL_OPTION_VALUE) {
$result = array_keys(Piwik::$idPeriods);
unset($result[4]); // remove 'range' period
return $result;
}
$periods = explode(',', $periods);
$periods = array_map('trim', $periods);
foreach ($periods as $periodIdentifier) {
if ($periodIdentifier == 'range') {
throw new \InvalidArgumentException(
"Invalid period type: invalidating range periods is not currently supported.");
}
if (!isset(Piwik::$idPeriods[$periodIdentifier])) {
throw new \InvalidArgumentException("Invalid period type '$periodIdentifier' supplied in --periods.");
}
}
return $periods;
}
/**
* @param InputInterface $input
* @return Date[][]
*/
private function getDateRangesToInvalidateFor(InputInterface $input)
{
$dateRanges = $input->getOption('dates');
if (empty($dateRanges)) {
throw new \InvalidArgumentException("The --dates option is required.");
}
return $dateRanges;
}
private function getPeriodDates($periodType, $dateRange)
{
if (!isset(Piwik::$idPeriods[$periodType])) {
throw new \InvalidArgumentException("Invalid period type '$periodType'.");
}
try {
$period = PeriodFactory::build($periodType, $dateRange);
} catch (\Exception $ex) {
throw new \InvalidArgumentException("Invalid date or date range specifier '$dateRange'", $code = 0, $ex);
}
$result = array();
if ($period instanceof Range) {
foreach ($period->getSubperiods() as $subperiod) {
$result[] = $subperiod->getDateStart();
}
} else {
$result[] = $period->getDateStart();
}
return $result;
}
private function getSegmentsToInvalidateFor(InputInterface $input, $idSites)
{
$segments = $input->getOption('segment');
$segments = array_map('trim', $segments);
$segments = array_unique($segments);
if (empty($segments)) {
return array(null);
}
$result = array();
foreach ($segments as $segmentString) {
$result[] = new Segment($segmentString, $idSites);
}
return $result;
}
}

View file

@ -0,0 +1,124 @@
<?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\CoreAdminHome\Commands;
use Piwik\Common;
use Piwik\DataAccess\ArchiveTableCreator;
use Piwik\Date;
use Piwik\Db;
use Piwik\Plugin\ConsoleCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Administration command that optimizes archive tables (even if they use InnoDB).
*/
class OptimizeArchiveTables extends ConsoleCommand
{
const ALL_TABLES_STRING = 'all';
const CURRENT_MONTH_STRING = 'now';
protected function configure()
{
$this->setName('database:optimize-archive-tables');
$this->setDescription("Runs an OPTIMIZE TABLE query on the specified archive tables.");
$this->addArgument("dates", InputArgument::IS_ARRAY | InputArgument::REQUIRED,
"The months of the archive tables to optimize. Use '" . self::ALL_TABLES_STRING. "' for all dates or '" .
self::CURRENT_MONTH_STRING . "' to optimize the current month only.");
$this->addOption('dry-run', null, InputOption::VALUE_NONE, 'For testing purposes.');
$this->setHelp("This command can be used to ease or automate maintenance. Instead of manually running "
. "OPTIMIZE TABLE queries, the command can be used.\n\nYou should run the command if you find your "
. "archive tables grow and do not shrink after purging. Optimizing them will reclaim some space.");
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$dryRun = $input->getOption('dry-run');
$tableMonths = $this->getTableMonthsToOptimize($input);
foreach ($tableMonths as $month) {
$this->optimizeTable($output, $dryRun, 'archive_numeric_' . $month);
$this->optimizeTable($output, $dryRun, 'archive_blob_' . $month);
}
}
private function optimizeTable(OutputInterface $output, $dryRun, $table)
{
$output->write("Optimizing table '$table'...");
if ($dryRun) {
$output->write("[dry-run, not optimising table]");
} else {
Db::optimizeTables(Common::prefixTable($table), $force = true);
}
$output->writeln("Done.");
}
private function getTableMonthsToOptimize(InputInterface $input)
{
$dateSpecifiers = $input->getArgument('dates');
if (count($dateSpecifiers) === 1) {
$dateSpecifier = reset($dateSpecifiers);
if ($dateSpecifier == self::ALL_TABLES_STRING) {
return $this->getAllArchiveTableMonths();
} else if ($dateSpecifier == self::CURRENT_MONTH_STRING) {
$now = Date::factory('now');
return array(ArchiveTableCreator::getTableMonthFromDate($now));
} else if (strpos($dateSpecifier, 'last') === 0) {
$lastN = substr($dateSpecifier, 4);
if (!ctype_digit($lastN)) {
throw new \Exception("Invalid lastN specifier '$lastN'. The end must be an integer, eg, last1 or last2.");
}
if ($lastN <= 0) {
throw new \Exception("Invalid lastN value '$lastN'.");
}
return $this->getLastNTableMonths((int)$lastN);
}
}
$tableMonths = array();
foreach ($dateSpecifiers as $date) {
$date = Date::factory($date);
$tableMonths[] = ArchiveTableCreator::getTableMonthFromDate($date);
}
return $tableMonths;
}
private function getAllArchiveTableMonths()
{
$tableMonths = array();
foreach (ArchiveTableCreator::getTablesArchivesInstalled() as $table) {
$tableMonths[] = ArchiveTableCreator::getDateFromTableName($table);
}
return $tableMonths;
}
/**
* @param int $lastN
* @return string[]
*/
private function getLastNTableMonths($lastN)
{
$now = Date::factory('now');
$result = array();
for ($i = 0; $i < $lastN; ++$i) {
$date = $now->subMonth($i + 1);
$result[] = ArchiveTableCreator::getTableMonthFromDate($date);
}
return $result;
}
}

View file

@ -0,0 +1,207 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\CoreAdminHome\Commands;
use Piwik\Archive;
use Piwik\Archive\ArchivePurger;
use Piwik\DataAccess\ArchiveTableCreator;
use Piwik\Date;
use Piwik\Db;
use Piwik\Plugin\ConsoleCommand;
use Piwik\Timer;
use Psr\Log\NullLogger;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Command that allows users to force purge old or invalid archive data. In the event of a failure
* in the archive purging scheduled task, this command can be used to manually delete old/invalid archives.
*/
class PurgeOldArchiveData extends ConsoleCommand
{
const ALL_DATES_STRING = 'all';
/**
* For tests.
*
* @var Date
*/
public static $todayOverride = null;
/**
* @var ArchivePurger
*/
private $archivePurger;
public function __construct(ArchivePurger $archivePurger = null)
{
parent::__construct();
$this->archivePurger = $archivePurger;
}
protected function configure()
{
$this->setName('core:purge-old-archive-data');
$this->setDescription('Purges out of date and invalid archive data from archive tables.');
$this->addArgument("dates", InputArgument::IS_ARRAY | InputArgument::OPTIONAL,
"The months of the archive tables to purge data from. By default, only deletes from the current month. Use '" . self::ALL_DATES_STRING. "' for all dates.",
array(self::getToday()->toString()));
$this->addOption('exclude-outdated', null, InputOption::VALUE_NONE, "Do not purge outdated archive data.");
$this->addOption('exclude-invalidated', null, InputOption::VALUE_NONE, "Do not purge invalidated archive data.");
$this->addOption('exclude-ranges', null, InputOption::VALUE_NONE, "Do not purge custom ranges.");
$this->addOption('skip-optimize-tables', null, InputOption::VALUE_NONE, "Do not run OPTIMIZE TABLES query on affected archive tables.");
$this->addOption('include-year-archives', null, InputOption::VALUE_NONE, "If supplied, the command will purge archive tables that contain year archives for every supplied date.");
$this->addOption('force-optimize-tables', null, InputOption::VALUE_NONE, "If supplied, forces optimize table SQL to be run, even on InnoDB tables.");
$this->setHelp("By default old and invalidated archives are purged. Custom ranges are also purged with outdated archives.\n\n"
. "Note: archive purging is done during scheduled task execution, so under normal circumstances, you should not need to "
. "run this command manually.");
}
protected function execute(InputInterface $input, OutputInterface $output)
{
// during normal command execution, we don't want the INFO level logs logged by the ArchivePurger service
// to display in the console, so we use a NullLogger for the service
$logger = null;
if ($output->getVerbosity() <= OutputInterface::VERBOSITY_NORMAL) {
$logger = new NullLogger();
}
$archivePurger = $this->archivePurger ?: new ArchivePurger($model = null, $purgeDatesOlderThan = null, $logger);
$dates = $this->getDatesToPurgeFor($input);
$excludeOutdated = $input->getOption('exclude-outdated');
if ($excludeOutdated) {
$output->writeln("Skipping purge outdated archive data.");
} else {
foreach ($dates as $date) {
$message = sprintf("Purging outdated archives for %s...", $date->toString('Y_m'));
$this->performTimedPurging($output, $message, function () use ($date, $archivePurger) {
$archivePurger->purgeOutdatedArchives($date);
});
}
}
$excludeInvalidated = $input->getOption('exclude-invalidated');
if ($excludeInvalidated) {
$output->writeln("Skipping purge invalidated archive data.");
} else {
foreach ($dates as $date) {
$message = sprintf("Purging invalidated archives for %s...", $date->toString('Y_m'));
$this->performTimedPurging($output, $message, function () use ($archivePurger, $date) {
$archivePurger->purgeInvalidatedArchivesFrom($date);
});
}
}
$excludeCustomRanges = $input->getOption('exclude-ranges');
if ($excludeCustomRanges) {
$output->writeln("Skipping purge custom range archives.");
} else {
foreach ($dates as $date) {
$message = sprintf("Purging custom range archives for %s...", $date->toString('Y_m'));
$this->performTimedPurging($output, $message, function () use ($date, $archivePurger) {
$archivePurger->purgeArchivesWithPeriodRange($date);
});
}
}
$skipOptimizeTables = $input->getOption('skip-optimize-tables');
if ($skipOptimizeTables) {
$output->writeln("Skipping OPTIMIZE TABLES.");
} else {
$this->optimizeArchiveTables($output, $dates, $input->getOption('force-optimize-tables'));
}
}
/**
* @param InputInterface $input
* @return Date[]
*/
private function getDatesToPurgeFor(InputInterface $input)
{
$dates = array();
$dateSpecifier = $input->getArgument('dates');
if (count($dateSpecifier) === 1
&& reset($dateSpecifier) == self::ALL_DATES_STRING
) {
foreach (ArchiveTableCreator::getTablesArchivesInstalled() as $table) {
$tableDate = ArchiveTableCreator::getDateFromTableName($table);
list($year, $month) = explode('_', $tableDate);
$dates[] = Date::factory($year . '-' . $month . '-' . '01');
}
} else {
$includeYearArchives = $input->getOption('include-year-archives');
foreach ($dateSpecifier as $date) {
$dateObj = Date::factory($date);
$yearMonth = $dateObj->toString('Y-m');
$dates[$yearMonth] = $dateObj;
// if --include-year-archives is supplied, add a date for the january table for this date's year
// so year archives will be purged
if ($includeYearArchives) {
$janYearMonth = $dateObj->toString('Y') . '-01';
if (empty($dates[$janYearMonth])) {
$dates[$janYearMonth] = Date::factory($janYearMonth . '-01');
}
}
}
$dates = array_values($dates);
}
return $dates;
}
private function performTimedPurging(OutputInterface $output, $startMessage, $callback)
{
$timer = new Timer();
$output->write($startMessage);
$callback();
$output->writeln("Done. <comment>[" . $timer->__toString() . "]</comment>");
}
/**
* @param OutputInterface $output
* @param Date[] $dates
* @param bool $forceOptimzation
*/
private function optimizeArchiveTables(OutputInterface $output, $dates, $forceOptimzation = false)
{
$output->writeln("Optimizing archive tables...");
foreach ($dates as $date) {
$numericTable = ArchiveTableCreator::getNumericTable($date);
$this->performTimedPurging($output, "Optimizing table $numericTable...", function () use ($numericTable, $forceOptimzation) {
Db::optimizeTables($numericTable, $forceOptimzation);
});
$blobTable = ArchiveTableCreator::getBlobTable($date);
$this->performTimedPurging($output, "Optimizing table $blobTable...", function () use ($blobTable, $forceOptimzation) {
Db::optimizeTables($blobTable, $forceOptimzation);
});
}
}
private static function getToday()
{
return self::$todayOverride ?: Date::today();
}
}

View file

@ -0,0 +1,79 @@
<?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\CoreAdminHome\Commands;
use Piwik\Container\StaticContainer;
use Piwik\FrontController;
use Piwik\Plugin\ConsoleCommand;
use Piwik\Scheduler\Scheduler;
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 RunScheduledTasks extends ConsoleCommand
{
protected function configure()
{
$this->setName('scheduled-tasks:run');
$this->setAliases(array('core:run-scheduled-tasks'));
$this->setDescription('Will run all scheduled tasks due to run at this time.');
$this->addArgument('task', InputArgument::OPTIONAL, 'Optionally pass the name of a task to run (will run even if not scheduled to run now)');
$this->addOption('force', null, InputOption::VALUE_NONE, 'If set, it will execute all tasks even the ones not due to run at this time.');
}
/**
* Execute command like: ./console core:run-scheduled-tasks
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->forceRunAllTasksIfRequested($input);
FrontController::getInstance()->init();
// TODO use dependency injection
/** @var Scheduler $scheduler */
$scheduler = StaticContainer::get('Piwik\Scheduler\Scheduler');
$task = $input->getArgument('task');
if ($task) {
$this->runSingleTask($scheduler, $task, $output);
} else {
$scheduler->run();
}
$this->writeSuccessMessage($output, array('Scheduled Tasks executed'));
}
private function forceRunAllTasksIfRequested(InputInterface $input)
{
$force = $input->getOption('force');
if ($force && !defined('DEBUG_FORCE_SCHEDULED_TASKS')) {
define('DEBUG_FORCE_SCHEDULED_TASKS', true);
}
}
private function runSingleTask(Scheduler $scheduler, $task, OutputInterface $output)
{
try {
$message = $scheduler->runTaskNow($task);
} catch (\InvalidArgumentException $e) {
$message = $e->getMessage() . PHP_EOL
. 'Available tasks:' . PHP_EOL
. implode(PHP_EOL, $scheduler->getTaskList());
throw new \Exception($message);
}
$output->writeln($message);
}
}

View file

@ -0,0 +1,97 @@
<?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\CoreAdminHome\Commands;
use Piwik\Config;
use Piwik\Plugin\ConsoleCommand;
use Piwik\Plugins\CoreAdminHome\Commands\SetConfig\ConfigSettingManipulation;
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 SetConfig extends ConsoleCommand
{
protected function configure()
{
$this->setName('config:set');
$this->setDescription('Set one or more config settings in the file config/config.ini.php');
$this->addArgument('assignment', InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
"List of config setting assignments, eg, Section.key=1 or Section.array_key[]=value");
$this->addOption('section', null, InputOption::VALUE_REQUIRED, 'The section the INI config setting belongs to.');
$this->addOption('key', null, InputOption::VALUE_REQUIRED, 'The name of the INI config setting.');
$this->addOption('value', null, InputOption::VALUE_REQUIRED, 'The value of the setting. (Not JSON encoded)');
$this->setHelp("This command can be used to set INI config settings on a Piwik instance.
You can set config values two ways, via --section, --key, --value or by command arguments.
To use --section, --key, --value, simply supply those options. You can only set one setting this way, and you cannot
append to arrays.
To use arguments, supply one or more arguments in the following format: Section.config_setting_name=\"value\"
'Section' is the name of the section, 'config_setting_name' the name of the setting and 'value' is the value.
NOTE: 'value' must be JSON encoded, so Section.config_setting_name=\"value\" would work but
Section.config_setting_name=value would not.
To append to an array setting, supply an argument like this: Section.config_setting_name[]=\"value to append\"
To reset an array setting, supply an argument like this: Section.config_setting_name=[]
Resetting an array will not work if the array has default values in global.ini.php (such as, [log] log_writers).
In this case the values in global.ini.php will be used, since there is no way to explicitly set an
array setting to empty in INI config.
Use the --piwik-domain option to specify which instance to modify.
");
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$section = $input->getOption('section');
$key = $input->getOption('key');
$value = $input->getOption('value');
$manipulations = $this->getAssignments($input);
$isSingleAssignment = !empty($section) && !empty($key) && $value !== false;
if ($isSingleAssignment) {
$manipulations[] = new ConfigSettingManipulation($section, $key, $value);
}
if (empty($manipulations)) {
throw new \InvalidArgumentException("Nothing to assign. Add assignments as arguments or use the "
. "--section, --key and --value options.");
}
$config = Config::getInstance();
foreach ($manipulations as $manipulation) {
$manipulation->manipulate($config);
$output->writeln("<info>Setting [{$manipulation->getSectionName()}] {$manipulation->getName()} = {$manipulation->getValueString()}</info>");
}
$config->forceSave();
$this->writeSuccessMessage($output, array("Done."));
}
/**
* @return ConfigSettingManipulation[]
*/
private function getAssignments(InputInterface $input)
{
$assignments = $input->getArgument('assignment');
$result = array();
foreach ($assignments as $assignment) {
$result[] = ConfigSettingManipulation::make($assignment);
}
return $result;
}
}

View file

@ -0,0 +1,176 @@
<?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\CoreAdminHome\Commands\SetConfig;
use Piwik\Config;
/**
* Representation of a INI config manipulation operation. Only supports two types
* of manipulations: appending to a config array and assigning a config value.
*/
class ConfigSettingManipulation
{
/**
* @var string
*/
private $sectionName;
/**
* @var string
*/
private $name;
/**
* @var string
*/
private $value;
/**
* @var bool
*/
private $isArrayAppend;
/**
* @param string $sectionName
* @param string $name
* @param string $value
* @param bool $isArrayAppend
*/
public function __construct($sectionName, $name, $value, $isArrayAppend = false)
{
$this->sectionName = $sectionName;
$this->name = $name;
$this->value = $value;
$this->isArrayAppend = $isArrayAppend;
}
/**
* Performs the INI config manipulation.
*
* @param Config $config
* @throws \Exception if trying to append to a non-array setting value or if trying to set an
* array value to a non-array setting
*/
public function manipulate(Config $config)
{
if ($this->isArrayAppend) {
$this->appendToArraySetting($config);
} else {
$this->setSingleConfigValue($config);
}
}
private function setSingleConfigValue(Config $config)
{
$sectionName = $this->sectionName;
$section = $config->$sectionName;
if (isset($section[$this->name])
&& is_array($section[$this->name])
&& !is_array($this->value)
) {
throw new \Exception("Trying to set non-array value to array setting " . $this->getSettingString() . ".");
}
$section[$this->name] = $this->value;
$config->$sectionName = $section;
}
private function appendToArraySetting(Config $config)
{
$sectionName = $this->sectionName;
$section = $config->$sectionName;
if (isset($section[$this->name])
&& !is_array($section[$this->name])
) {
throw new \Exception("Trying to append to non-array setting value " . $this->getSettingString() . ".");
}
$section[$this->name][] = $this->value;
$config->$sectionName = $section;
}
/**
* Creates a ConfigSettingManipulation instance from a string like:
*
* `SectionName.setting_name=value`
*
* or
*
* `SectionName.setting_name[]=value`
*
* The value must be JSON so `="string"` will work but `=string` will not.
*
* @param string $assignment
* @return self
*/
public static function make($assignment)
{
if (!preg_match('/^([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)(\[\])?=(.*)/', $assignment, $matches)) {
throw new \InvalidArgumentException("Invalid assignment string '$assignment': expected section.name=value or section.name[]=value");
}
$section = $matches[1];
$name = $matches[2];
$isAppend = !empty($matches[3]);
$value = json_decode($matches[4], $isAssoc = true);
if ($value === null) {
throw new \InvalidArgumentException("Invalid assignment string '$assignment': could not parse value as JSON");
}
return new self($section, $name, $value, $isAppend);
}
private function getSettingString()
{
return "[{$this->sectionName}] {$this->name}";
}
/**
* @return string
*/
public function getSectionName()
{
return $this->sectionName;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @return string
*/
public function getValue()
{
return $this->value;
}
/**
* @return boolean
*/
public function isArrayAppend()
{
return $this->isArrayAppend;
}
/**
* @return string
*/
public function getValueString()
{
return json_encode($this->value);
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -13,29 +13,47 @@ use Piwik\API\ResponseBuilder;
use Piwik\ArchiveProcessor\Rules;
use Piwik\Common;
use Piwik\Config;
use Piwik\Container\StaticContainer;
use Piwik\DataTable\Renderer\Json;
use Piwik\Menu\MenuTop;
use Piwik\Menu\MenuUser;
use Piwik\Nonce;
use Piwik\Option;
use Piwik\Piwik;
use Piwik\Plugin\ControllerAdmin;
use Piwik\Plugins\CorePluginsAdmin\UpdateCommunication;
use Piwik\Plugins\CustomVariables\CustomVariables;
use Piwik\Plugins\LanguagesManager\API as APILanguagesManager;
use Piwik\Plugins\LanguagesManager\LanguagesManager;
use Piwik\Plugins\PrivacyManager\DoNotTrackHeaderChecker;
use Piwik\Plugins\SitesManager\API as APISitesManager;
use Piwik\Settings\Manager as SettingsManager;
use Piwik\Settings\SystemSetting;
use Piwik\Settings\UserSetting;
use Piwik\Site;
use Piwik\Tracker\IgnoreCookie;
use Piwik\Translation\Translator;
use Piwik\UpdateCheck;
use Piwik\Url;
use Piwik\View;
/**
*
*/
class Controller extends \Piwik\Plugin\ControllerAdmin
class Controller extends ControllerAdmin
{
const SET_PLUGIN_SETTINGS_NONCE = 'CoreAdminHome.setPluginSettings';
/**
* @var Translator
*/
private $translator;
/** @var OptOutManager */
private $optOutManager;
public function __construct(Translator $translator, OptOutManager $optOutManager)
{
$this->translator = $translator;
$this->optOutManager = $optOutManager;
parent::__construct();
}
public function index()
{
$this->redirectToIndex('UsersManager', 'userSettings');
@ -44,42 +62,87 @@ class Controller extends \Piwik\Plugin\ControllerAdmin
public function generalSettings()
{
Piwik::checkUserHasSomeAdminAccess();
Piwik::checkUserHasSuperUserAccess();
$view = new View('@CoreAdminHome/generalSettings');
$this->handleGeneralSettingsAdmin($view);
if (Piwik::hasUserSuperUserAccess()) {
$this->handleGeneralSettingsAdmin($view);
$view->trustedHosts = Url::getTrustedHostsFromConfig();
$logo = new CustomLogo();
$view->branding = array('use_custom_logo' => $logo->isEnabled());
$view->logosWriteable = $logo->isCustomLogoWritable();
$view->pathUserLogo = CustomLogo::getPathUserLogo();
$view->pathUserLogoSmall = CustomLogo::getPathUserLogoSmall();
$view->pathUserLogoSVG = CustomLogo::getPathUserSvgLogo();
$view->pathUserLogoDirectory = realpath(dirname($view->pathUserLogo) . '/');
}
$view->trustedHosts = Url::getTrustedHostsFromConfig();
$logo = new CustomLogo();
$view->branding = array('use_custom_logo' => $logo->isEnabled());
$view->fileUploadEnabled = $logo->isFileUploadEnabled();
$view->logosWriteable = $logo->isCustomLogoWritable();
$view->pathUserLogo = CustomLogo::getPathUserLogo();
$view->pathUserFavicon = CustomLogo::getPathUserFavicon();
$view->pathUserLogoSmall = CustomLogo::getPathUserLogoSmall();
$view->pathUserLogoSVG = CustomLogo::getPathUserSvgLogo();
$view->pathUserLogoDirectory = realpath(dirname($view->pathUserLogo) . '/');
$view->language = LanguagesManager::getLanguageCodeForCurrentUser();
$this->setBasicVariablesView($view);
return $view->render();
}
public function pluginSettings()
public function adminPluginSettings()
{
Piwik::checkUserHasSuperUserAccess();
$settings = $this->getPluginSettings();
$vars = array(
'nonce' => Nonce::getNonce(static::SET_PLUGIN_SETTINGS_NONCE),
'pluginsSettings' => $this->getSettingsByType($settings, 'admin'),
'firstSuperUserSettingNames' => $this->getFirstSuperUserSettingNames($settings),
'mode' => 'admin'
);
return $this->renderTemplate('pluginSettings', $vars);
}
/**
* @param \Piwik\Plugin\Settings[] $pluginsSettings
* @return array array([pluginName] => [])
*/
private function getSettingsByType($pluginsSettings, $mode)
{
$byType = array();
foreach ($pluginsSettings as $pluginName => $pluginSettings) {
$settings = array();
foreach ($pluginSettings->getSettingsForCurrentUser() as $setting) {
if ('admin' === $mode && $setting instanceof SystemSetting) {
$settings[] = $setting;
} elseif ('user' === $mode && $setting instanceof UserSetting) {
$settings[] = $setting;
}
}
if (!empty($settings)) {
$byType[$pluginName] = array(
'introduction' => $pluginSettings->getIntroduction(),
'settings' => $settings
);
}
}
return $byType;
}
public function userPluginSettings()
{
Piwik::checkUserIsNotAnonymous();
$settings = $this->getPluginSettings();
$view = new View('@CoreAdminHome/pluginSettings');
$view->nonce = Nonce::getNonce(static::SET_PLUGIN_SETTINGS_NONCE);
$view->pluginSettings = $settings;
$view->firstSuperUserSettingNames = $this->getFirstSuperUserSettingNames($settings);
$vars = array(
'nonce' => Nonce::getNonce(static::SET_PLUGIN_SETTINGS_NONCE),
'pluginsSettings' => $this->getSettingsByType($settings, 'user'),
'firstSuperUserSettingNames' => $this->getFirstSuperUserSettingNames($settings),
'mode' => 'user'
);
$this->setBasicVariablesView($view);
return $view->render();
return $this->renderTemplate('pluginSettings', $vars);
}
private function getPluginSettings()
@ -121,7 +184,7 @@ class Controller extends \Piwik\Plugin\ControllerAdmin
if (!Nonce::verifyNonce(static::SET_PLUGIN_SETTINGS_NONCE, $nonce)) {
return json_encode(array(
'result' => 'error',
'message' => Piwik::translate('General_ExceptionNonceMismatch')
'message' => $this->translator->translate('General_ExceptionNonceMismatch')
));
}
@ -140,15 +203,28 @@ class Controller extends \Piwik\Plugin\ControllerAdmin
}
}
} catch (Exception $e) {
$message = $e->getMessage();
if (!empty($setting)) {
$message = $setting->title . ': ' . $message;
}
$message = html_entity_decode($message, ENT_QUOTES, 'UTF-8');
return json_encode(array('result' => 'error', 'message' => $message));
}
try {
foreach ($pluginsSettings as $pluginSetting) {
$pluginSetting->save();
}
} catch (Exception $e) {
$message = html_entity_decode($e->getMessage(), ENT_QUOTES, 'UTF-8');
return json_encode(array('result' => 'error', 'message' => $message));
return json_encode(array(
'result' => 'error',
'message' => $this->translator->translate('CoreAdminHome_PluginSettingsSaveFailed'))
);
}
Nonce::discardNonce(static::SET_PLUGIN_SETTINGS_NONCE);
return json_encode(array('result' => 'success'));
}
@ -165,7 +241,13 @@ class Controller extends \Piwik\Plugin\ControllerAdmin
foreach ($settings as $setting) {
if ($setting['name'] == $settingKey) {
return $setting['value'];
$value = $setting['value'];
if (is_string($value)) {
return Common::unsanitizeInputValue($value);
}
return $value;
}
}
}
@ -200,9 +282,12 @@ class Controller extends \Piwik\Plugin\ControllerAdmin
*/
public function trackingCodeGenerator()
{
Piwik::checkUserHasSomeViewAccess();
$view = new View('@CoreAdminHome/trackingCodeGenerator');
$this->setBasicVariablesView($view);
$view->topMenu = MenuTop::getInstance()->getMenu();
$view->topMenu = MenuTop::getInstance()->getMenu();
$view->userMenu = MenuUser::getInstance()->getMenu();
$viewableIdSites = APISitesManager::getInstance()->getSitesIdWithAtLeastViewAccess();
@ -210,8 +295,8 @@ class Controller extends \Piwik\Plugin\ControllerAdmin
$view->idSite = Common::getRequestVar('idSite', $defaultIdSite, 'int');
$view->defaultReportSiteName = Site::getNameFor($view->idSite);
$view->defaultSiteRevenue = \Piwik\MetricsFormatter::getCurrencySymbol($view->idSite);
$view->maxCustomVariables = CustomVariables::getMaxCustomVariables();
$view->defaultSiteRevenue = Site::getCurrencySymbolFor($view->idSite);
$view->maxCustomVariables = CustomVariables::getNumUsableCustomVariables();
$allUrls = APISitesManager::getInstance()->getSiteUrlsFromId($view->idSite);
if (isset($allUrls[1])) {
@ -227,7 +312,8 @@ class Controller extends \Piwik\Plugin\ControllerAdmin
// get currencies for each viewable site
$view->currencySymbols = APISitesManager::getInstance()->getCurrencySymbols();
$view->serverSideDoNotTrackEnabled = \Piwik\Plugins\PrivacyManager\DoNotTrackHeaderChecker::isActive();
$dntChecker = new DoNotTrackHeaderChecker();
$view->serverSideDoNotTrackEnabled = $dntChecker->isActive();
return $view->render();
}
@ -237,46 +323,37 @@ class Controller extends \Piwik\Plugin\ControllerAdmin
*/
public function optOut()
{
$trackVisits = !IgnoreCookie::isIgnoreCookieFound();
$nonce = Common::getRequestVar('nonce', false);
$language = Common::getRequestVar('language', '');
if ($nonce !== false && Nonce::verifyNonce('Piwik_OptOut', $nonce)) {
Nonce::discardNonce('Piwik_OptOut');
IgnoreCookie::setIgnoreCookie();
$trackVisits = !$trackVisits;
}
$view = new View('@CoreAdminHome/optOut');
$view->trackVisits = $trackVisits;
$view->nonce = Nonce::getNonce('Piwik_OptOut', 3600);
$view->language = APILanguagesManager::getInstance()->isLanguageAvailable($language)
? $language
: LanguagesManager::getLanguageCodeForCurrentUser();
return $view->render();
return $this->optOutManager->getOptOutView()->render();
}
public function uploadCustomLogo()
{
Piwik::checkUserHasSuperUserAccess();
$this->checkTokenInUrl();
$logo = new CustomLogo();
$success = $logo->copyUploadedLogoToFilesystem();
$successLogo = $logo->copyUploadedLogoToFilesystem();
$successFavicon = $logo->copyUploadedFaviconToFilesystem();
if($success) {
if ($successLogo || $successFavicon) {
return '1';
}
return '0';
}
static public function isGeneralSettingsAdminEnabled()
public static function isGeneralSettingsAdminEnabled()
{
return (bool) Config::getInstance()->General['enable_general_settings_admin'];
}
private function makeReleaseChannels()
{
return StaticContainer::get('Piwik\Plugin\ReleaseChannels');
}
private function saveGeneralSettings()
{
if(!self::isGeneralSettingsAdminEnabled()) {
if (!self::isGeneralSettingsAdminEnabled()) {
// General settings + Beta channel + SMTP settings is disabled
return;
}
@ -287,10 +364,14 @@ class Controller extends \Piwik\Plugin\ControllerAdmin
Rules::setBrowserTriggerArchiving((bool)$enableBrowserTriggerArchiving);
Rules::setTodayArchiveTimeToLive($todayArchiveTimeToLive);
$releaseChannels = $this->makeReleaseChannels();
// update beta channel setting
$debug = Config::getInstance()->Debug;
$debug['allow_upgrades_to_beta'] = Common::getRequestVar('enableBetaReleaseCheck', '0', 'int');
Config::getInstance()->Debug = $debug;
$releaseChannel = Common::getRequestVar('releaseChannel', '', 'string');
if (!$releaseChannels->isValidReleaseChannelId($releaseChannel)) {
$releaseChannel = '';
}
$releaseChannels->setActiveReleaseChannelId($releaseChannel);
// Update email settings
$mail = array();
@ -309,6 +390,7 @@ class Controller extends \Piwik\Plugin\ControllerAdmin
if ($trustedHosts !== false) {
Url::saveTrustedHostnameInConfig($trustedHosts);
}
Config::getInstance()->forceSave();
$pluginUpdateCommunication = new UpdateCommunication();
@ -323,7 +405,7 @@ class Controller extends \Piwik\Plugin\ControllerAdmin
{
// Whether to display or not the general settings (cron, beta, smtp)
$view->isGeneralSettingsAdminEnabled = self::isGeneralSettingsAdminEnabled();
if($view->isGeneralSettingsAdminEnabled) {
if ($view->isGeneralSettingsAdminEnabled) {
$this->displayWarningIfConfigFileNotWritable();
}
@ -337,9 +419,22 @@ class Controller extends \Piwik\Plugin\ControllerAdmin
}
$view->showWarningCron = $showWarningCron;
$view->todayArchiveTimeToLive = $todayArchiveTimeToLive;
$view->todayArchiveTimeToLiveDefault = Rules::getTodayArchiveTimeToLiveDefault();
$view->enableBrowserTriggerArchiving = $enableBrowserTriggerArchiving;
$view->enableBetaReleaseCheck = Config::getInstance()->Debug['allow_upgrades_to_beta'];
$releaseChannels = $this->makeReleaseChannels();
$activeChannelId = $releaseChannels->getActiveReleaseChannel()->getId();
$allChannels = array();
foreach ($releaseChannels->getAllReleaseChannels() as $channel) {
$allChannels[] = array(
'id' => $channel->getId(),
'name' => $channel->getName(),
'description' => $channel->getDescription(),
'active' => $channel->getId() === $activeChannelId
);
}
$view->releaseChannels = $allChannels;
$view->mail = Config::getInstance()->mail;
$pluginUpdateCommunication = new UpdateCommunication();
@ -347,5 +442,4 @@ class Controller extends \Piwik\Plugin\ControllerAdmin
$view->enableSendPluginUpdateCommunication = $pluginUpdateCommunication->isEnabled();
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -8,15 +8,9 @@
*/
namespace Piwik\Plugins\CoreAdminHome;
use Piwik\DataAccess\ArchiveSelector;
use Piwik\DataAccess\ArchiveTableCreator;
use Piwik\Date;
use Piwik\Db;
use Piwik\Menu\MenuAdmin;
use Piwik\Piwik;
use Piwik\ScheduledTask;
use Piwik\ScheduledTime;
use Piwik\Settings\Manager as SettingsManager;
use Piwik\ProxyHttp;
use Piwik\Settings\UserSetting;
/**
@ -25,16 +19,17 @@ use Piwik\Settings\UserSetting;
class CoreAdminHome extends \Piwik\Plugin
{
/**
* @see Piwik\Plugin::getListHooksRegistered
* @see Piwik\Plugin::registerEvents
*/
public function getListHooksRegistered()
public function registerEvents()
{
return array(
'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
'AssetManager.getJavaScriptFiles' => 'getJsFiles',
'Menu.Admin.addItems' => 'addMenu',
'TaskScheduler.getScheduledTasks' => 'getScheduledTasks',
'UsersManager.deleteUser' => 'cleanupUser'
'UsersManager.deleteUser' => 'cleanupUser',
'API.DocumentationGenerator.@hideExceptForSuperUser' => 'displayOnlyForSuperUser',
'Template.jsGlobalVariables' => 'addJsGlobalVariables',
'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys'
);
}
@ -43,87 +38,50 @@ class CoreAdminHome extends \Piwik\Plugin
UserSetting::removeAllUserSettingsForUser($userLogin);
}
public function getScheduledTasks(&$tasks)
{
// general data purge on older archive tables, executed daily
$purgeArchiveTablesTask = new ScheduledTask ($this,
'purgeOutdatedArchives',
null,
ScheduledTime::factory('daily'),
ScheduledTask::HIGH_PRIORITY);
$tasks[] = $purgeArchiveTablesTask;
// lowest priority since tables should be optimized after they are modified
$optimizeArchiveTableTask = new ScheduledTask ($this,
'optimizeArchiveTable',
null,
ScheduledTime::factory('daily'),
ScheduledTask::LOWEST_PRIORITY);
$tasks[] = $optimizeArchiveTableTask;
}
public function getStylesheetFiles(&$stylesheets)
{
$stylesheets[] = "libs/jquery/themes/base/jquery-ui.css";
$stylesheets[] = "plugins/CoreAdminHome/stylesheets/menu.less";
$stylesheets[] = "plugins/Zeitgeist/stylesheets/base.less";
$stylesheets[] = "libs/jquery/themes/base/jquery-ui.min.css";
$stylesheets[] = "plugins/Morpheus/stylesheets/base.less";
$stylesheets[] = "plugins/Morpheus/stylesheets/main.less";
$stylesheets[] = "plugins/CoreAdminHome/stylesheets/generalSettings.less";
$stylesheets[] = "plugins/CoreAdminHome/stylesheets/pluginSettings.less";
}
public function getJsFiles(&$jsFiles)
{
$jsFiles[] = "libs/jquery/jquery.js";
$jsFiles[] = "libs/jquery/jquery-ui.js";
$jsFiles[] = "libs/bower_components/jquery/dist/jquery.min.js";
$jsFiles[] = "libs/bower_components/jquery-ui/ui/minified/jquery-ui.min.js";
$jsFiles[] = "libs/jquery/jquery.browser.js";
$jsFiles[] = "libs/javascript/sprintf.js";
$jsFiles[] = "plugins/Zeitgeist/javascripts/piwikHelper.js";
$jsFiles[] = "plugins/Zeitgeist/javascripts/ajaxHelper.js";
$jsFiles[] = "libs/jquery/jquery.history.js";
$jsFiles[] = "libs/bower_components/sprintf/dist/sprintf.min.js";
$jsFiles[] = "plugins/Morpheus/javascripts/piwikHelper.js";
$jsFiles[] = "plugins/Morpheus/javascripts/ajaxHelper.js";
$jsFiles[] = "plugins/Morpheus/javascripts/jquery.icheck.min.js";
$jsFiles[] = "plugins/Morpheus/javascripts/morpheus.js";
$jsFiles[] = "plugins/CoreHome/javascripts/broadcast.js";
$jsFiles[] = "plugins/CoreAdminHome/javascripts/generalSettings.js";
$jsFiles[] = "plugins/CoreHome/javascripts/donate.js";
$jsFiles[] = "plugins/CoreAdminHome/javascripts/pluginSettings.js";
$jsFiles[] = "plugins/CoreAdminHome/javascripts/protocolCheck.js";
}
function addMenu()
public function displayOnlyForSuperUser(&$hide)
{
MenuAdmin::getInstance()->add('CoreAdminHome_MenuManage', null, "", Piwik::isUserHasSomeAdminAccess(), $order = 1);
MenuAdmin::getInstance()->add('CoreAdminHome_MenuDiagnostic', null, "", Piwik::isUserHasSomeAdminAccess(), $order = 10);
MenuAdmin::getInstance()->add('General_Settings', null, "", Piwik::isUserHasSomeAdminAccess(), $order = 5);
MenuAdmin::getInstance()->add('General_Settings', 'CoreAdminHome_MenuGeneralSettings',
array('module' => 'CoreAdminHome', 'action' => 'generalSettings'),
Piwik::isUserHasSomeAdminAccess(),
$order = 6);
MenuAdmin::getInstance()->add('CoreAdminHome_MenuManage', 'CoreAdminHome_TrackingCode',
array('module' => 'CoreAdminHome', 'action' => 'trackingCodeGenerator'),
Piwik::isUserHasSomeAdminAccess(),
$order = 4);
MenuAdmin::getInstance()->add('General_Settings', 'CoreAdminHome_PluginSettings',
array('module' => 'CoreAdminHome', 'action' => 'pluginSettings'),
SettingsManager::hasPluginsSettingsForCurrentUser(),
$order = 7);
$hide = !Piwik::hasUserSuperUserAccess();
}
function purgeOutdatedArchives()
public function addJsGlobalVariables(&$out)
{
$archiveTables = ArchiveTableCreator::getTablesArchivesInstalled();
foreach ($archiveTables as $table) {
$date = ArchiveTableCreator::getDateFromTableName($table);
list($year, $month) = explode('_', $date);
// Somehow we may have archive tables created with older dates, prevent exception from being thrown
if($year > 1990) {
ArchiveSelector::purgeOutdatedArchives(Date::factory("$year-$month-15"));
}
if (ProxyHttp::isHttps()) {
$isHttps = 'true';
} else {
$isHttps = 'false';
}
$out .= "piwik.hasServerDetectedHttps = $isHttps;\n";
}
function optimizeArchiveTable()
public function getClientSideTranslationKeys(&$translationKeys)
{
$archiveTables = ArchiveTableCreator::getTablesArchivesInstalled();
Db::optimizeTables($archiveTables);
$translationKeys[] = 'CoreAdminHome_ProtocolNotDetectedCorrectly';
$translationKeys[] = 'CoreAdminHome_ProtocolNotDetectedCorrectlySolution';
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -11,16 +11,18 @@ namespace Piwik\Plugins\CoreAdminHome;
use Piwik\Config;
use Piwik\Filesystem;
use Piwik\Option;
use Piwik\Plugin\Manager;
use Piwik\SettingsPiwik;
class CustomLogo
{
const LOGO_HEIGHT = 300;
const LOGO_SMALL_HEIGHT = 100;
const FAVICON_HEIGHT = 32;
public function getLogoUrl($pathOnly = false)
{
$defaultLogo = 'plugins/Zeitgeist/images/logo.png';
$defaultLogo = 'plugins/Morpheus/images/logo.png';
$themeLogo = 'plugins/%s/images/logo.png';
$userLogo = CustomLogo::getPathUserLogo();
return $this->getPathToLogo($pathOnly, $defaultLogo, $themeLogo, $userLogo);
@ -28,7 +30,7 @@ class CustomLogo
public function getHeaderLogoUrl($pathOnly = false)
{
$defaultLogo = 'plugins/Zeitgeist/images/logo-header.png';
$defaultLogo = 'plugins/Morpheus/images/logo-header.png';
$themeLogo = 'plugins/%s/images/logo-header.png';
$customLogo = CustomLogo::getPathUserLogoSmall();
return $this->getPathToLogo($pathOnly, $defaultLogo, $themeLogo, $customLogo);
@ -36,7 +38,7 @@ class CustomLogo
public function getSVGLogoUrl($pathOnly = false)
{
$defaultLogo = 'plugins/Zeitgeist/images/logo.svg';
$defaultLogo = 'plugins/Morpheus/images/logo.svg';
$themeLogo = 'plugins/%s/images/logo.svg';
$customLogo = CustomLogo::getPathUserSvgLogo();
$svg = $this->getPathToLogo($pathOnly, $defaultLogo, $themeLogo, $customLogo);
@ -74,12 +76,20 @@ class CustomLogo
return false;
}
/**
* @return bool
*/
public function isFileUploadEnabled()
{
return ini_get('file_uploads') == 1;
}
/**
* @return bool
*/
public function isCustomLogoWritable()
{
if(Config::getInstance()->General['enable_custom_logo_check'] == 0) {
if (Config::getInstance()->General['enable_custom_logo_check'] == 0) {
return true;
}
$pathUserLogo = $this->getPathUserLogo();
@ -87,15 +97,14 @@ class CustomLogo
$directoryWritingTo = PIWIK_DOCUMENT_ROOT . '/' . dirname($pathUserLogo);
// Create directory if not already created
Filesystem::mkdir($directoryWritingTo, $denyAccess = false);
Filesystem::mkdir($directoryWritingTo);
$directoryWritable = is_writable($directoryWritingTo);
$logoFilesWriteable = is_writeable(PIWIK_DOCUMENT_ROOT . '/' . $pathUserLogo)
&& is_writeable(PIWIK_DOCUMENT_ROOT . '/' . $this->getPathUserSvgLogo())
&& is_writeable(PIWIK_DOCUMENT_ROOT . '/' . $this->getPathUserLogoSmall());;
$serverUploadEnabled = ini_get('file_uploads') == 1;
$isCustomLogoWritable = ($logoFilesWriteable || $directoryWritable) && $serverUploadEnabled;
$isCustomLogoWritable = ($logoFilesWriteable || $directoryWritable) && $this->isFileUploadEnabled();
return $isCustomLogoWritable;
}
@ -106,7 +115,12 @@ class CustomLogo
$logo = $defaultLogo;
$themeName = \Piwik\Plugin\Manager::getInstance()->getThemeEnabled()->getPluginName();
$theme = \Piwik\Plugin\Manager::getInstance()->getThemeEnabled();
if(!$theme) {
$themeName = Manager::DEFAULT_THEME;
} else {
$themeName = $theme->getPluginName();
}
$themeLogo = sprintf($themeLogo, $themeName);
if (file_exists($pathToPiwikRoot . '/' . $themeLogo)) {
@ -129,6 +143,11 @@ class CustomLogo
return self::rewritePath('misc/user/logo.png');
}
public static function getPathUserFavicon()
{
return self::rewritePath('misc/user/favicon.png');
}
public static function getPathUserSvgLogo()
{
return self::rewritePath('misc/user/logo.svg');
@ -141,62 +160,72 @@ class CustomLogo
protected static function rewritePath($path)
{
return SettingsPiwik::rewriteMiscUserPathWithHostname($path);
return SettingsPiwik::rewriteMiscUserPathWithInstanceId($path);
}
public function copyUploadedLogoToFilesystem()
{
$uploadFieldName = 'customLogo';
if (empty($_FILES['customLogo'])
|| !empty($_FILES['customLogo']['error'])
$success = $this->uploadImage($uploadFieldName, self::LOGO_SMALL_HEIGHT, $this->getPathUserLogoSmall());
$success = $success && $this->uploadImage($uploadFieldName, self::LOGO_HEIGHT, $this->getPathUserLogo());
return $success;
}
public function copyUploadedFaviconToFilesystem()
{
$uploadFieldName = 'customFavicon';
return $this->uploadImage($uploadFieldName, self::FAVICON_HEIGHT, $this->getPathUserFavicon());
}
private function uploadImage($uploadFieldName, $targetHeight, $userPath)
{
if (empty($_FILES[$uploadFieldName])
|| !empty($_FILES[$uploadFieldName]['error'])
) {
return false;
}
$file = $_FILES['customLogo']['tmp_name'];
$file = $_FILES[$uploadFieldName]['tmp_name'];
if (!file_exists($file)) {
return false;
}
list($width, $height) = getimagesize($file);
switch ($_FILES['customLogo']['type']) {
switch ($_FILES[$uploadFieldName]['type']) {
case 'image/jpeg':
$image = imagecreatefromjpeg($file);
$image = @imagecreatefromjpeg($file);
break;
case 'image/png':
$image = imagecreatefrompng($file);
$image = @imagecreatefrompng($file);
break;
case 'image/gif':
$image = imagecreatefromgif($file);
$image = @imagecreatefromgif ($file);
break;
default:
return false;
}
$widthExpected = round($width * self::LOGO_HEIGHT / $height);
$smallWidthExpected = round($width * self::LOGO_SMALL_HEIGHT / $height);
$logo = imagecreatetruecolor($widthExpected, self::LOGO_HEIGHT);
$logoSmall = imagecreatetruecolor($smallWidthExpected, self::LOGO_SMALL_HEIGHT);
// Handle transparency
$background = imagecolorallocate($logo, 0, 0, 0);
$backgroundSmall = imagecolorallocate($logoSmall, 0, 0, 0);
imagecolortransparent($logo, $background);
imagecolortransparent($logoSmall, $backgroundSmall);
if ($_FILES['customLogo']['type'] == 'image/png') {
imagealphablending($logo, false);
imagealphablending($logoSmall, false);
imagesavealpha($logo, true);
imagesavealpha($logoSmall, true);
if (!is_resource($image)) {
return false;
}
imagecopyresized($logo, $image, 0, 0, 0, 0, $widthExpected, self::LOGO_HEIGHT, $width, $height);
imagecopyresized($logoSmall, $image, 0, 0, 0, 0, $smallWidthExpected, self::LOGO_SMALL_HEIGHT, $width, $height);
$targetWidth = round($width * $targetHeight / $height);
imagepng($logo, PIWIK_DOCUMENT_ROOT . '/' . $this->getPathUserLogo(), 3);
imagepng($logoSmall, PIWIK_DOCUMENT_ROOT . '/' . $this->getPathUserLogoSmall(), 3);
$newImage = imagecreatetruecolor($targetWidth, $targetHeight);
if ($_FILES[$uploadFieldName]['type'] == 'image/png') {
imagealphablending($newImage, false);
imagesavealpha($newImage, true);
}
$backgroundColor = imagecolorallocate($newImage, 0, 0, 0);
imagecolortransparent($newImage, $backgroundColor);
imagecopyresampled($newImage, $image, 0, 0, 0, 0, $targetWidth, $targetHeight, $width, $height);
imagepng($newImage, PIWIK_DOCUMENT_ROOT . '/' . $userPath, 3);
return true;
}

View file

@ -0,0 +1,74 @@
<?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\CoreAdminHome;
use Piwik\Db;
use Piwik\Menu\MenuAdmin;
use Piwik\Menu\MenuTop;
use Piwik\Menu\MenuUser;
use Piwik\Piwik;
use Piwik\Settings\Manager as SettingsManager;
class Menu extends \Piwik\Plugin\Menu
{
public function configureAdminMenu(MenuAdmin $menu)
{
$hasAdminAccess = Piwik::isUserHasSomeAdminAccess();
if ($hasAdminAccess) {
$menu->addManageItem(null, array(), $order = 1);
$menu->addSettingsItem(null, array(), $order = 5);
$menu->addDiagnosticItem(null, array(), $order = 10);
$menu->addDevelopmentItem(null, array(), $order = 15);
if (Piwik::hasUserSuperUserAccess()) {
$menu->addSettingsItem('General_General',
$this->urlForAction('generalSettings'),
$order = 6);
}
}
if (Piwik::hasUserSuperUserAccess() && SettingsManager::hasSystemPluginsSettingsForCurrentUser()) {
$menu->addSettingsItem('CoreAdminHome_PluginSettings',
$this->urlForAction('adminPluginSettings'),
$order = 7);
}
}
public function configureTopMenu(MenuTop $menu)
{
if (Piwik::isUserHasSomeAdminAccess()) {
$url = $this->urlForModuleAction('SitesManager', 'index');
if (Piwik::hasUserSuperUserAccess()) {
$url = $this->urlForAction('generalSettings');
}
$menu->registerMenuIcon('CoreAdminHome_Administration', 'icon-configure');
$menu->addItem('CoreAdminHome_Administration', null, $url, 980, Piwik::translate('CoreAdminHome_Administration'));
}
}
public function configureUserMenu(MenuUser $menu)
{
if (!Piwik::isUserIsAnonymous()) {
$menu->addManageItem('CoreAdminHome_TrackingCode',
$this->urlForAction('trackingCodeGenerator'),
$order = 20);
if (SettingsManager::hasUserPluginsSettingsForCurrentUser()) {
$menu->addPersonalItem('CoreAdminHome_PluginSettings',
$this->urlForAction('userPluginSettings'),
$order = 15);
}
}
}
}

View file

@ -0,0 +1,188 @@
<?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\CoreAdminHome\Model;
use Piwik\Common;
use Piwik\Container\StaticContainer;
use Piwik\DataAccess\TableMetadata;
use Piwik\Db;
use Psr\Log\LoggerInterface;
/**
* Provides methods to find duplicate actions and fix duplicate action references in tables
* that reference log_action rows.
*/
class DuplicateActionRemover
{
/**
* The tables that contain idaction reference columns.
*
* @var string[]
*/
public static $tablesWithIdActionColumns = array(
'log_link_visit_action',
'log_conversion',
'log_conversion_item'
);
/**
* DAO used to get idaction column names in tables that reference log_action rows.
*
* @var TableMetadata
*/
private $tableMetadataAccess;
/**
* @var LoggerInterface
*/
private $logger;
/**
* List of idaction columns in each table in $tablesWithIdActionColumns. idaction
* columns are table columns with the string `"idaction"` in them.
*
* @var string[]
*/
private $idactionColumns = null;
/**
* Constructor.
*
* @param TableMetadata $tableMetadataAccess
* @param LoggerInterface $logger
*/
public function __construct(TableMetadata $tableMetadataAccess = null, LoggerInterface $logger = null)
{
$this->tableMetadataAccess = $tableMetadataAccess ?: new TableMetadata();
$this->logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface');
}
/**
* Returns list of all duplicate actions in the log_action table by name and the lowest action ID.
* The duplicate actions are returned with each action.
*
* @return array Contains the following elements:
*
* * **name**: The action's name.
* * **idaction**: The action's ID.
* * **duplicateIdActions**: An array of duplicate action IDs.
*/
public function getDuplicateIdActions()
{
$sql = "SELECT name, COUNT(*) AS count, GROUP_CONCAT(idaction ORDER BY idaction ASC SEPARATOR ',') as idactions
FROM " . Common::prefixTable('log_action') . "
GROUP BY name, hash, type HAVING count > 1";
$result = array();
foreach (Db::fetchAll($sql) as $row) {
$dupeInfo = array('name' => $row['name']);
$idActions = explode(",", $row['idactions']);
$dupeInfo['idaction'] = array_shift($idActions);
$dupeInfo['duplicateIdActions'] = $idActions;
$result[] = $dupeInfo;
}
return $result;
}
/**
* Executes one SQL statement that sets all idaction columns in a table to a single value, if the
* values of those columns are in the specified set (`$duplicateIdActions`).
*
* Notes:
*
* The SQL will look like:
*
* UPDATE $table SET
* col1 = IF((col1 IN ($duplicateIdActions)), $realIdAction, col1),
* col2 = IF((col2 IN ($duplicateIdActions)), $realIdAction, col2),
* ...
* WHERE col1 IN ($duplicateIdActions) OR col2 IN ($duplicateIdActions) OR ...
*
* @param string $table
* @param int $realIdAction The idaction to set column values to.
* @param int[] $duplicateIdActions The idaction values that should be changed.
*/
public function fixDuplicateActionsInTable($table, $realIdAction, $duplicateIdActions)
{
$idactionColumns = $this->getIdActionTableColumnsFromMetadata();
$idactionColumns = array_values($idactionColumns[$table]);
$table = Common::prefixTable($table);
$inFromIdsExpression = $this->getInFromIdsExpression($duplicateIdActions);
$setExpression = "%1\$s = IF(($inFromIdsExpression), $realIdAction, %1\$s)";
$sql = "UPDATE $table SET\n";
foreach ($idactionColumns as $index => $column) {
if ($index != 0) {
$sql .= ",\n";
}
$sql .= sprintf($setExpression, $column);
}
$sql .= $this->getWhereToGetRowsUsingDuplicateActions($idactionColumns, $duplicateIdActions);
Db::query($sql);
}
/**
* Returns the server time and idsite of rows in a log table that reference at least one action
* in a set.
*
* @param string $table
* @param int[] $duplicateIdActions
* @return array with two elements **idsite** and **server_time**. idsite is the site ID and server_time
* is the date of the log.
*/
public function getSitesAndDatesOfRowsUsingDuplicates($table, $duplicateIdActions)
{
$idactionColumns = $this->getIdActionTableColumnsFromMetadata();
$idactionColumns = array_values($idactionColumns[$table]);
$table = Common::prefixTable($table);
$sql = "SELECT idsite, DATE(server_time) as server_time FROM $table ";
$sql .= $this->getWhereToGetRowsUsingDuplicateActions($idactionColumns, $duplicateIdActions);
return Db::fetchAll($sql);
}
private function getIdActionTableColumnsFromMetadata()
{
if ($this->idactionColumns === null) {
$this->idactionColumns = array();
foreach (self::$tablesWithIdActionColumns as $table) {
$columns = $this->tableMetadataAccess->getIdActionColumnNames(Common::prefixTable($table));
$this->logger->debug("Found following idactions in {table}: {columns}", array(
'table' => $table,
'columns' => implode(',', $columns)
));
$this->idactionColumns[$table] = $columns;
}
}
return $this->idactionColumns;
}
private function getWhereToGetRowsUsingDuplicateActions($idactionColumns, $fromIdActions)
{
$sql = "WHERE ";
foreach ($idactionColumns as $index => $column) {
if ($index != 0) {
$sql .= "OR ";
}
$sql .= sprintf($this->getInFromIdsExpression($fromIdActions), $column) . " ";
}
return $sql;
}
private function getInFromIdsExpression($fromIdActions)
{
return "%1\$s IN (" . implode(',', $fromIdActions) . ")";
}
}

View file

@ -0,0 +1,225 @@
<?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\CoreAdminHome;
use Piwik\Common;
use Piwik\Nonce;
use Piwik\Plugins\LanguagesManager\API as APILanguagesManager;
use Piwik\Plugins\LanguagesManager\LanguagesManager;
use Piwik\Plugins\PrivacyManager\DoNotTrackHeaderChecker;
use Piwik\Tracker\IgnoreCookie;
use Piwik\Url;
use Piwik\View;
class OptOutManager
{
/** @var DoNotTrackHeaderChecker */
private $doNotTrackHeaderChecker;
/** @var array */
private $javascripts;
/** @var array */
private $stylesheets;
/** @var string */
private $title;
/** @var View|null */
private $view;
/** @var array */
private $queryParameters = array();
/**
* @param DoNotTrackHeaderChecker $doNotTrackHeaderChecker
*/
public function __construct(DoNotTrackHeaderChecker $doNotTrackHeaderChecker = null)
{
$this->doNotTrackHeaderChecker = $doNotTrackHeaderChecker ?: new DoNotTrackHeaderChecker();
$this->javascripts = array(
'inline' => array(),
'external' => array(),
);
$this->stylesheets = array(
'inline' => array(),
'external' => array(),
);
}
/**
* Add a javascript file|code into the OptOut View
* Note: This method will not escape the inline javascript code!
*
* @param string $javascript
* @param bool $inline
*/
public function addJavascript($javascript, $inline = true)
{
$type = $inline ? 'inline' : 'external';
$this->javascripts[$type][] = $javascript;
}
/**
* @return array
*/
public function getJavascripts()
{
return $this->javascripts;
}
/**
* Add a stylesheet file|code into the OptOut View
* Note: This method will not escape the inline css code!
*
* @param string $stylesheet Escaped stylesheet
* @param bool $inline
*/
public function addStylesheet($stylesheet, $inline = true)
{
$type = $inline ? 'inline' : 'external';
$this->stylesheets[$type][] = $stylesheet;
}
/**
* @return array
*/
public function getStylesheets()
{
return $this->stylesheets;
}
/**
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* @param string $title
*/
public function setTitle($title)
{
$this->title = $title;
}
/**
* @param string $key
* @param string $value
* @param bool $override
*
* @return bool
*/
public function addQueryParameter($key, $value, $override = true)
{
if (!isset($this->queryParameters[$key]) || true === $override) {
$this->queryParameters[$key] = $value;
return true;
}
return false;
}
/**
* @param array $items
* @param bool|true $override
*/
public function addQueryParameters(array $items, $override = true)
{
foreach ($items as $key => $value) {
$this->addQueryParameter($key, $value, $override);
}
}
/**
* @param $key
*/
public function removeQueryParameter($key)
{
unset($this->queryParameters[$key]);
}
/**
* @return array
*/
public function getQueryParameters()
{
return $this->queryParameters;
}
/**
* @return View
* @throws \Exception
*/
public function getOptOutView()
{
if ($this->view) {
return $this->view;
}
$trackVisits = !IgnoreCookie::isIgnoreCookieFound();
$dntFound = $this->getDoNotTrackHeaderChecker()->isDoNotTrackFound();
$setCookieInNewWindow = Common::getRequestVar('setCookieInNewWindow', false, 'int');
if ($setCookieInNewWindow) {
$reloadUrl = Url::getCurrentQueryStringWithParametersModified(array(
'showConfirmOnly' => 1,
'setCookieInNewWindow' => 0,
));
} else {
$reloadUrl = false;
$nonce = Common::getRequestVar('nonce', false);
if ($nonce !== false && Nonce::verifyNonce('Piwik_OptOut', $nonce)) {
Nonce::discardNonce('Piwik_OptOut');
IgnoreCookie::setIgnoreCookie();
$trackVisits = !$trackVisits;
}
}
$language = Common::getRequestVar('language', '');
$lang = APILanguagesManager::getInstance()->isLanguageAvailable($language)
? $language
: LanguagesManager::getLanguageCodeForCurrentUser();
$this->addQueryParameters(array(
'module' => 'CoreAdminHome',
'action' => 'optOut',
'language' => $lang,
'setCookieInNewWindow' => 1
), false);
$this->view = new View("@CoreAdminHome/optOut");
$this->view->setXFrameOptions('allow');
$this->view->dntFound = $dntFound;
$this->view->trackVisits = $trackVisits;
$this->view->nonce = Nonce::getNonce('Piwik_OptOut', 3600);
$this->view->language = $lang;
$this->view->showConfirmOnly = Common::getRequestVar('showConfirmOnly', false, 'int');
$this->view->reloadUrl = $reloadUrl;
$this->view->javascripts = $this->getJavascripts();
$this->view->stylesheets = $this->getStylesheets();
$this->view->title = $this->getTitle();
$this->view->queryParameters = $this->getQueryParameters();
return $this->view;
}
/**
* @return DoNotTrackHeaderChecker
*/
protected function getDoNotTrackHeaderChecker()
{
return $this->doNotTrackHeaderChecker;
}
}

View file

@ -0,0 +1,146 @@
<?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\CoreAdminHome;
use Piwik\ArchiveProcessor\Rules;
use Piwik\Archive\ArchivePurger;
use Piwik\DataAccess\ArchiveTableCreator;
use Piwik\Date;
use Piwik\Db;
use Piwik\Http;
use Piwik\Option;
use Piwik\Plugins\CoreAdminHome\Tasks\ArchivesToPurgeDistributedList;
use Piwik\Tracker\Visit\ReferrerSpamFilter;
use Psr\Log\LoggerInterface;
class Tasks extends \Piwik\Plugin\Tasks
{
/**
* @var ArchivePurger
*/
private $archivePurger;
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(ArchivePurger $archivePurger, LoggerInterface $logger)
{
$this->archivePurger = $archivePurger;
$this->logger = $logger;
}
public function schedule()
{
// general data purge on older archive tables, executed daily
$this->daily('purgeOutdatedArchives', null, self::HIGH_PRIORITY);
// general data purge on invalidated archive records, executed daily
$this->daily('purgeInvalidatedArchives', null, self::LOW_PRIORITY);
// lowest priority since tables should be optimized after they are modified
$this->daily('optimizeArchiveTable', null, self::LOWEST_PRIORITY);
$this->weekly('updateSpammerBlacklist');
}
/**
* @return bool `true` if the purge was executed, `false` if it was skipped.
* @throws \Exception
*/
public function purgeOutdatedArchives()
{
if ($this->willPurgingCausePotentialProblemInUI()) {
$this->logger->info("Purging temporary archives: skipped (browser triggered archiving not enabled & not running after core:archive)");
return false;
}
$archiveTables = ArchiveTableCreator::getTablesArchivesInstalled();
$this->logger->info("Purging archives in {tableCount} archive tables.", array('tableCount' => count($archiveTables)));
// keep track of dates we purge for, since getTablesArchivesInstalled() will return numeric & blob
// tables (so dates will appear two times, and we should only purge once per date)
$datesPurged = array();
foreach ($archiveTables as $table) {
$date = ArchiveTableCreator::getDateFromTableName($table);
list($year, $month) = explode('_', $date);
// Somehow we may have archive tables created with older dates, prevent exception from being thrown
if ($year > 1990) {
if (empty($datesPurged[$date])) {
$dateObj = Date::factory("$year-$month-15");
$this->archivePurger->purgeOutdatedArchives($dateObj);
$this->archivePurger->purgeArchivesWithPeriodRange($dateObj);
$datesPurged[$date] = true;
} else {
$this->logger->debug("Date {date} already purged.", array('date' => $date));
}
} else {
$this->logger->info("Skipping purging of archive tables *_{year}_{month}, year <= 1990.", array('year' => $year, 'month' => $month));
}
}
return true;
}
public function purgeInvalidatedArchives()
{
$archivesToPurge = new ArchivesToPurgeDistributedList();
foreach ($archivesToPurge->getAllAsDates() as $date) {
$this->archivePurger->purgeInvalidatedArchivesFrom($date);
$archivesToPurge->removeDate($date);
}
}
public function optimizeArchiveTable()
{
$archiveTables = ArchiveTableCreator::getTablesArchivesInstalled();
Db::optimizeTables($archiveTables);
}
/**
* Update the referrer spam blacklist
*
* @see https://github.com/piwik/referrer-spam-blacklist
*/
public function updateSpammerBlacklist()
{
$url = 'https://raw.githubusercontent.com/piwik/referrer-spam-blacklist/master/spammers.txt';
$list = Http::sendHttpRequest($url, 30);
$list = preg_split("/\r\n|\n|\r/", $list);
if (count($list) < 10) {
throw new \Exception(sprintf(
'The spammers list downloaded from %s contains less than 10 entries, considering it a fail',
$url
));
}
Option::set(ReferrerSpamFilter::OPTION_STORAGE_NAME, serialize($list));
}
/**
* we should only purge outdated & custom range archives if we know cron archiving has just run,
* or if browser triggered archiving is enabled. if cron archiving has run, then we know the latest
* archives are in the database, and we can remove temporary ones. if browser triggered archiving is
* enabled, then we know any archives that are wrongly purged, can be re-archived on demand.
* this prevents some situations where "no data" is displayed for reports that should have data.
*
* @return bool
*/
private function willPurgingCausePotentialProblemInUI()
{
return !Rules::isRequestAuthorizedToArchive();
}
}

View file

@ -0,0 +1,87 @@
<?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\CoreAdminHome\Tasks;
use Piwik\Concurrency\DistributedList;
use Piwik\Date;
/**
* Distributed list that holds a list of year-month archive table identifiers (eg, 2015_01 or 2014_11). Each item in the
* list is expected to identify a pair of archive tables that contain invalidated archives.
*
* The archiving purging scheduled task will read items in this list when executing the daily purge.
*
* This class is necessary in order to keep the archive purging scheduled task fast. W/o a way to keep track of
* tables w/ invalid data, the task would have to iterate over every table, which is not desired for a task that
* is executed daily.
*
* If users find other tables contain invalidated archives, they can use the core:purge-old-archive-data command
* to manually purge them.
*/
class ArchivesToPurgeDistributedList extends DistributedList
{
const OPTION_INVALIDATED_DATES_SITES_TO_PURGE = 'InvalidatedOldReports_DatesWebsiteIds';
public function __construct()
{
parent::__construct(self::OPTION_INVALIDATED_DATES_SITES_TO_PURGE);
}
/**
* @inheritdoc
*/
public function setAll($yearMonths)
{
$yearMonths = array_unique($yearMonths, SORT_REGULAR);
parent::setAll($yearMonths);
}
protected function getListOptionValue()
{
$result = parent::getListOptionValue();
$this->convertOldDistributedList($result);
return $result;
}
public function getAllAsDates()
{
$dates = array();
foreach ($this->getAll() as $yearMonth) {
try {
$date = Date::factory(str_replace('_', '-', $yearMonth) . '-01');
} catch (\Exception $ex) {
continue; // invalid year month in distributed list
}
$dates[] = $date;
}
return $dates;
}
public function removeDate(Date $date)
{
$yearMonth = $date->toString('Y_m');
$this->remove($yearMonth);
}
/**
* Before 2.12.0 Piwik stored this list as an array mapping year months to arrays of site IDs. If this is
* found in the DB, we convert the array to an array of year months to avoid errors and to make sure
* the correct tables are still purged.
*/
private function convertOldDistributedList(&$yearMonths)
{
foreach ($yearMonths as $key => $value) {
if (preg_match("/^[0-9]{4}_[0-9]{2}$/", $key)) {
unset($yearMonths[$key]);
$yearMonths[] = $key;
}
}
}
}

View file

@ -1,5 +1,5 @@
/*!
* Piwik - Web Analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -8,7 +8,7 @@
function sendGeneralSettingsAJAX() {
var enableBrowserTriggerArchiving = $('input[name=enableBrowserTriggerArchiving]:checked').val();
var enablePluginUpdateCommunication = $('input[name=enablePluginUpdateCommunication]:checked').val();
var enableBetaReleaseCheck = $('input[name=enableBetaReleaseCheck]:checked').val();
var releaseChannel = $('input[name=releaseChannel]:checked').val();
var todayArchiveTimeToLive = $('#todayArchiveTimeToLive').val();
var trustedHosts = [];
@ -22,7 +22,7 @@ function sendGeneralSettingsAJAX() {
format: 'json',
enableBrowserTriggerArchiving: enableBrowserTriggerArchiving,
enablePluginUpdateCommunication: enablePluginUpdateCommunication,
enableBetaReleaseCheck: enableBetaReleaseCheck,
releaseChannel: releaseChannel,
todayArchiveTimeToLive: todayArchiveTimeToLive,
mailUseSmtp: isSmtpEnabled(),
mailPort: $('#mailPort').val(),
@ -55,10 +55,14 @@ function isCustomLogoEnabled() {
}
function refreshCustomLogo() {
var imageDiv = $("#currentLogo");
if (imageDiv && imageDiv.attr("src")) {
var logoUrl = imageDiv.attr("src").split("?")[0];
imageDiv.attr("src", logoUrl + "?" + (new Date()).getTime());
var selectors = ['#currentLogo', '#currentFavicon'];
var index;
for (index = 0; index < selectors.length; index++) {
var imageDiv = $(selectors[index]);
if (imageDiv && imageDiv.attr("src")) {
var logoUrl = imageDiv.attr("src").split("?")[0];
imageDiv.attr("src", logoUrl + "?" + (new Date()).getTime());
}
}
}
@ -67,7 +71,7 @@ $(document).ready(function () {
showSmtpSettings(isSmtpEnabled());
showCustomLogoSettings(isCustomLogoEnabled());
$('#generalSettingsSubmit').click(function () {
$('.generalSettingsSubmit').click(function () {
var doSubmit = function () {
sendGeneralSettingsAJAX();
};
@ -102,27 +106,41 @@ $(document).ready(function () {
$('input').keypress(function (e) {
var key = e.keyCode || e.which;
if (key == 13) {
$('#generalSettingsSubmit').click();
$('.generalSettingsSubmit').click();
}
}
);
$("#logoUploadForm").submit(function (data) {
var submittingForm = $(this);
$('.uploaderror').fadeOut();
var frameName = "upload" + (new Date()).getTime();
var uploadFrame = $("<iframe name=\"" + frameName + "\" />");
uploadFrame.css("display", "none");
uploadFrame.load(function (data) {
setTimeout(function () {
refreshCustomLogo();
uploadFrame.remove();
var frameContent = $(uploadFrame.contents()).find('body').html();
frameContent = $.trim(frameContent);
if ('0' === frameContent) {
$('.uploaderror').show();
}
if ('1' === frameContent || '0' === frameContent) {
uploadFrame.remove();
}
}, 1000);
});
$("body:first").append(uploadFrame);
submittingForm.attr("target", frameName);
});
$('#customLogo').change(function () {$("#logoUploadForm").submit()});
$('#customLogo,#customFavicon').change(function () {
$("#logoUploadForm").submit();
$(this).val('');
});
// trusted hosts event handling
var trustedHostSettings = $('#trustedHostSettings');
@ -136,7 +154,8 @@ $(document).ready(function () {
// append new row to the table
trustedHostSettings.find('ul').append(trustedHostSettings.find('li:last').clone());
trustedHostSettings.find('li:last input').val('');
trustedHostSettings.find('li:last input').val('').focus();
return false;
});
});

View file

@ -1,5 +1,5 @@
/*!
* Piwik - Web Analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -13,7 +13,7 @@
/**
* This class is deprecated. Use server-side events instead.
*
*
* @deprecated
*/
var TrackingCodeGenerator = function () {
@ -150,7 +150,7 @@
// function that generates JS code
var generateJsCodeAjax = null,
generateJsCode = function () {
generateJsCode = function (trackingCodeChangedManually) {
// get params used to generate JS code
var params = {
piwikUrl: piwikHost + piwikPath,
@ -162,6 +162,7 @@
customCampaignNameQueryParam: null,
customCampaignKeywordParam: null,
doNotTrack: $('#javascript-tracking-do-not-track').is(':checked') ? 1 : 0,
disableCookies: $('#javascript-tracking-disable-cookies').is(':checked') ? 1 : 0
};
if ($('#custom-campaign-query-params-check').is(':checked')) {
@ -184,24 +185,30 @@
generateJsCodeAjax.setCallback(function (response) {
generateJsCodeAjax = null;
$('#javascript-text').find('textarea').val(response.value);
var jsCodeTextarea = $('#javascript-text').find('textarea');
jsCodeTextarea.val(response.value);
if(trackingCodeChangedManually) {
jsCodeTextarea.effect("highlight", {}, 1500);
}
});
generateJsCodeAjax.send();
};
// function that generates image tracker link
var generateImageTrackingAjax = null,
generateImageTrackerLink = function () {
generateImageTrackerLink = function (trackingCodeChangedManually) {
// get data used to generate the link
var generateDataParams = {
piwikUrl: piwikHost + piwikPath,
actionName: $('#image-tracker-action-name').val(),
actionName: $('#image-tracker-action-name').val()
};
if ($('#image-tracking-goal-check').is(':checked')) {
generateDataParams.idGoal = $('#image-tracker-goal').val();
if (generateDataParams.idGoal) {
generateDataParams.revenue = $('#image-tracker-advanced-options').find('.revenue').val();
generateDataParams.revenue = $('#image-goal-picker-extra').find('.revenue').val();
}
}
@ -220,7 +227,12 @@
generateImageTrackingAjax.setCallback(function (response) {
generateImageTrackingAjax = null;
$('#image-tracking-text').find('textarea').val(response.value);
var jsCodeTextarea = $('#image-tracking-text').find('textarea');
jsCodeTextarea.val(response.value);
if(trackingCodeChangedManually) {
jsCodeTextarea.effect("highlight", {}, 1500);
}
});
generateImageTrackingAjax.send();
};
@ -229,7 +241,7 @@
$('#image-tracker-website').bind('change', function (e, site) {
getSiteData(site.id, '#image-tracking-code-options', function () {
resetGoalSelectItems(site.id, 'image-tracker-goal');
generateImageTrackerLink();
generateImageTrackerLink(true);
});
});
@ -249,7 +261,7 @@
$('.current-site-alias').text(siteUrls[site.id][1] || defaultAliasUrl);
resetGoalSelectItems(site.id, 'js-tracker-goal');
generateJsCode();
generateJsCode(true);
});
});
@ -259,9 +271,7 @@
e.preventDefault();
var newRow = '<tr>\
<td>&nbsp;</td>\
<td><input type="textbox" class="custom-variable-name"/></td>\
<td>&nbsp;</td>\
<td><input type="textbox" class="custom-variable-value"/></td>\
</tr>',
row = $(this).closest('tr');
@ -279,20 +289,20 @@
// when any input in the JS tracking options section changes, regenerate JS code
$('#optional-js-tracking-options').on('change', 'input', function () {
generateJsCode();
generateJsCode(true);
});
// when any input/select in the image tracking options section changes, regenerate
// image tracker link
$('#image-tracking-section').on('change', 'input,select', function () {
generateImageTrackerLink();
generateImageTrackerLink(true);
});
// on click generated code textareas, select the text so it can be easily copied
$('#javascript-text>textarea,#image-tracking-text>textarea').click(function () {
$(this).select();
});
// initial generation
getSiteData(
$('#js-tracker-website').attr('siteid'),

View file

@ -1,5 +1,5 @@
/*!
* Piwik - Web Analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -17,6 +17,8 @@ $(document).ready(function () {
function updatePluginSettings()
{
$submit.prop('disabled', true);
var $nonce = $('[name="setpluginsettingsnonce"]');
var nonceValue = '';
@ -34,7 +36,10 @@ $(document).ready(function () {
ajaxHandler.redirectOnSuccess();
ajaxHandler.setLoadingElement(getLoadingElement());
ajaxHandler.setErrorElement(getErrorElement());
ajaxHandler.send(true);
ajaxHandler.setCompleteCallback(function () {
$submit.prop('disabled', false);
});
ajaxHandler.send();
}
function getSettings()

Some files were not shown because too many files have changed in this diff Show more