update Piwik to version 2.16 (fixes #91)

This commit is contained in:
oliver 2016-04-10 18:55:57 +02:00
commit 47263617c5
27 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,21 +9,22 @@
namespace Piwik\Plugin;
use Piwik\Singleton;
use Piwik\Container\StaticContainer;
use Psr\Log\LoggerInterface;
/**
* The base class of all API singletons.
*
*
* Plugins that want to expose functionality through the Reporting API should create a class
* that extends this one. Every public method in that class that is not annotated with **@ignore**
* will be callable through Piwik's Web API.
*
*
* _Note: If your plugin calculates and stores reports, they should be made available through the API._
*
*
* ### Examples
*
*
* **Defining an API for a plugin**
*
*
* class API extends \Piwik\Plugin\API
* {
* public function myMethod($idSite, $period, $date, $segment = false)
@ -32,14 +33,66 @@ use Piwik\Singleton;
* return $dataTable;
* }
* }
*
*
* **Linking to an API method**
*
*
* <a href="?module=API&method=MyPlugin.myMethod&idSite=1&period=day&date=2013-10-23">Link</a>
*
*
* @api
*/
abstract class API extends Singleton
abstract class API
{
private static $instances;
/**
* Returns the singleton instance for the derived class. If the singleton instance
* has not been created, this method will create it.
*
* @return static
*/
public static function getInstance()
{
$class = get_called_class();
if (!isset(self::$instances[$class])) {
$container = StaticContainer::getContainer();
$refl = new \ReflectionClass($class);
if (!$refl->getConstructor() || $refl->getConstructor()->isPublic()) {
self::$instances[$class] = $container->get($class);
} else {
/** @var LoggerInterface $logger */
$logger = $container->get('Psr\Log\LoggerInterface');
// BC with API defining a protected constructor
$logger->notice('The API class {class} defines a protected constructor which is deprecated, make the constructor public instead', array('class' => $class));
self::$instances[$class] = new $class;
}
}
return self::$instances[$class];
}
/**
* Used in tests only
* @ignore
* @deprecated
*/
public static function unsetInstance()
{
$class = get_called_class();
unset(self::$instances[$class]);
}
/**
* Sets the singleton instance. For testing purposes.
* @ignore
* @deprecated
*/
public static function setSingletonInstance($instance)
{
$class = get_called_class();
self::$instances[$class] = $instance;
}
}

View file

@ -0,0 +1,21 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugin;
/**
* Base type for metric metadata classes that describe aggregated metrics. These metrics are
* computed in the backend data store and are aggregated in PHP when Piwik archives period reports.
*
* Note: This class is a placeholder. It will be filled out at a later date. Right now, only
* processed metrics can be defined this way.
*/
abstract class AggregatedMetric extends Metric
{
// stub, to be filled out later
}

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
@ -15,41 +15,41 @@ use Piwik\Config as PiwikConfig;
/**
* The base class that should be extended by plugins that compute their own
* analytics data.
*
*
* Descendants should implement the {@link aggregateDayReport()} and {@link aggregateMultipleReports()}
* methods.
*
*
* Both of these methods should persist analytics data using the {@link \Piwik\ArchiveProcessor}
* instance returned by {@link getProcessor()}. The {@link aggregateDayReport()} method should
* compute analytics data using the {@link \Piwik\DataAccess\LogAggregator} instance
* returned by {@link getLogAggregator()}.
*
*
* ### Examples
*
*
* **Extending Archiver**
*
*
* class MyArchiver extends Archiver
* {
* public function aggregateDayReport()
* {
* $logAggregator = $this->getLogAggregator();
*
*
* $data = $logAggregator->queryVisitsByDimension(...);
*
*
* $dataTable = new DataTable();
* $dataTable->addRowsFromSimpleArray($data);
*
*
* $archiveProcessor = $this->getProcessor();
* $archiveProcessor->insertBlobRecords('MyPlugin_myReport', $dataTable->getSerialized(500));
* }
*
*
* public function aggregateMultipleReports()
* {
* $archiveProcessor = $this->getProcessor();
* $archiveProcessor->aggregateDataTableRecords('MyPlugin_myReport', 500);
* }
* }
*
*
* @api
*/
abstract class Archiver
@ -61,7 +61,7 @@ abstract class Archiver
/**
* Constructor.
*
*
* @param ArchiveProcessor $processor The ArchiveProcessor instance to use when persisting archive
* data.
*/
@ -73,12 +73,12 @@ abstract class Archiver
/**
* Archives data for a day period.
*
*
* Implementations of this method should do more computation intensive activities such
* as aggregating data across log tables. Since this method only deals w/ data logged for a day,
* aggregating individual log table rows isn't a problem. Doing this for any larger period,
* however, would cause performance degradation.
*
*
* Aggregate log table rows using a {@link Piwik\DataAccess\LogAggregator} instance. Get a
* {@link Piwik\DataAccess\LogAggregator} instance using the {@link getLogAggregator()} method.
*/
@ -86,11 +86,11 @@ abstract class Archiver
/**
* Archives data for a non-day period.
*
*
* Implementations of this method should only aggregate existing reports of subperiods of the
* current period. For example, it is more efficient to aggregate reports for each day of a
* week than to aggregate each log entry of the week.
*
*
* Use {@link Piwik\ArchiveProcessor::aggregateNumericMetrics()} and {@link Piwik\ArchiveProcessor::aggregateDataTableRecords()}
* to aggregate archived reports. Get the {@link Piwik\ArchiveProcessor} instance using the {@link getProcessor()}
* method.
@ -100,7 +100,7 @@ abstract class Archiver
/**
* Returns a {@link Piwik\ArchiveProcessor} instance that can be used to insert archive data for
* the period, segment and site we are archiving data for.
*
*
* @return \Piwik\ArchiveProcessor
* @api
*/
@ -112,7 +112,7 @@ abstract class Archiver
/**
* Returns a {@link Piwik\DataAccess\LogAggregator} instance that can be used to aggregate log table rows
* for this period, segment and site.
*
*
* @return \Piwik\DataAccess\LogAggregator
* @api
*/

View file

@ -0,0 +1,131 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugin;
use Piwik\Log;
use Piwik\Plugin\Manager as PluginManager;
use Exception;
/**
* Factory class with methods to find and instantiate Plugin components.
*/
class ComponentFactory
{
/**
* Create a component instance that exists within a specific plugin. Uses the component's
* unqualified class name and expected base type.
*
* This method will only create a class if it is located within the component type's
* associated subdirectory.
*
* @param string $pluginName The name of the plugin the component is expected to belong to,
* eg, `'DevicesDetection'`.
* @param string $componentClassSimpleName The component's class name w/o namespace, eg,
* `"GetKeywords"`.
* @param string $componentTypeClass The fully qualified class name of the component type, eg,
* `"Piwik\Plugin\Report"`.
* @return mixed|null A new instance of the desired component or null if not found. If the
* plugin is not loaded or activated or the component is not located in
* in the sub-namespace specified by `$componentTypeClass::COMPONENT_SUBNAMESPACE`,
* this method will return null.
*/
public static function factory($pluginName, $componentClassSimpleName, $componentTypeClass)
{
if (empty($pluginName) || empty($componentClassSimpleName)) {
Log::debug("ComponentFactory::%s: empty plugin name or component simple name requested (%s, %s)",
__FUNCTION__, $pluginName, $componentClassSimpleName);
return null;
}
$plugin = self::getActivatedPlugin(__FUNCTION__, $pluginName);
if (empty($plugin)) {
return null;
}
$subnamespace = $componentTypeClass::COMPONENT_SUBNAMESPACE;
$desiredComponentClass = 'Piwik\\Plugins\\' . $pluginName . '\\' . $subnamespace . '\\' . $componentClassSimpleName;
$components = $plugin->findMultipleComponents($subnamespace, $componentTypeClass);
foreach ($components as $class) {
if ($class == $desiredComponentClass) {
return new $class();
}
}
Log::debug("ComponentFactory::%s: Could not find requested component (args = ['%s', '%s', '%s']).",
__FUNCTION__, $pluginName, $componentClassSimpleName, $componentTypeClass);
return null;
}
/**
* Finds a component instance that satisfies a given predicate.
*
* @param string $componentTypeClass The fully qualified class name of the component type, eg,
* `"Piwik\Plugin\Report"`.
* @param string $pluginName|false The name of the plugin the component is expected to belong to,
* eg, `'DevicesDetection'`.
* @param callback $predicate
* @return mixed The component that satisfies $predicate or null if not found.
*/
public static function getComponentIf($componentTypeClass, $pluginName, $predicate)
{
$pluginManager = PluginManager::getInstance();
// get components to search through
$subnamespace = $componentTypeClass::COMPONENT_SUBNAMESPACE;
if (empty($pluginName)) {
$components = $pluginManager->findMultipleComponents($subnamespace, $componentTypeClass);
} else {
$plugin = self::getActivatedPlugin(__FUNCTION__, $pluginName);
if (empty($plugin)) {
return null;
}
$components = $plugin->findMultipleComponents($subnamespace, $componentTypeClass);
}
// find component that satisfieds predicate
foreach ($components as $class) {
$component = new $class();
if ($predicate($component)) {
return $component;
}
}
Log::debug("ComponentFactory::%s: Could not find component that satisfies predicate (args = ['%s', '%s', '%s']).",
__FUNCTION__, $componentTypeClass, $pluginName, get_class($predicate));
return null;
}
/**
* @param string $function
* @param string $pluginName
* @return null|\Piwik\Plugin
*/
private static function getActivatedPlugin($function, $pluginName)
{
$pluginManager = PluginManager::getInstance();
try {
if (!$pluginManager->isPluginActivated($pluginName)) {
Log::debug("ComponentFactory::%s: component for deactivated plugin ('%s') requested.",
$function, $pluginName);
return null;
}
return $pluginManager->getLoadedPlugin($pluginName);
} catch (Exception $e) {
Log::debug($e);
return null;
}
}
}

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,32 +8,17 @@
*/
namespace Piwik\Plugin;
use Piwik\Common;
use Symfony\Component\Console\Command\Command as SymfonyCommand;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* The base class for console commands.
*
*
* @api
*/
class ConsoleCommand extends SymfonyCommand
{
/**
* Constructor.
*
* @param string|null $name The name of the command, eg, `'generate:api'`.
*/
public function __construct($name = null)
{
if (!Common::isPhpCliMode()) {
throw new \RuntimeException('Only executable in CLI mode');
}
parent::__construct($name);
}
public function writeSuccessMessage(OutputInterface $output, $messages)
{
$lengths = array_map('strlen', $messages);

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
@ -14,21 +14,25 @@ use Piwik\API\Proxy;
use Piwik\API\Request;
use Piwik\Common;
use Piwik\Config as PiwikConfig;
use Piwik\Config;
use Piwik\DataTable\Filter\CalculateEvolutionFilter;
use Piwik\Date;
use Piwik\Exception\NoPrivilegesException;
use Piwik\Exception\NoWebsiteFoundException;
use Piwik\FrontController;
use Piwik\Menu\MenuTop;
use Piwik\Menu\MenuUser;
use Piwik\NoAccessException;
use Piwik\Notification\Manager as NotificationManager;
use Piwik\NumberFormatter;
use Piwik\Period\Month;
use Piwik\Period;
use Piwik\Period\PeriodValidator;
use Piwik\Period\Range;
use Piwik\Piwik;
use Piwik\Plugins\CoreAdminHome\CustomLogo;
use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph\Evolution;
use Piwik\Plugins\LanguagesManager\LanguagesManager;
use Piwik\Plugins\SitesManager\API as APISitesManager;
use Piwik\Plugins\UsersManager\API as APIUsersManager;
use Piwik\Registry;
use Piwik\SettingsPiwik;
use Piwik\Site;
use Piwik\Url;
@ -38,18 +42,18 @@ use Piwik\ViewDataTable\Factory as ViewDataTableFactory;
/**
* Base class of all plugin Controllers.
*
*
* Plugins that wish to add display HTML should create a Controller that either
* extends from this class or from {@link ControllerAdmin}. Every public method in
* the controller will be exposed as a controller method and can be invoked via
* an HTTP request.
*
*
* Learn more about Piwik's MVC system [here](/guides/mvc-in-piwik).
*
*
* ### Examples
*
*
* **Defining a controller**
*
*
* class Controller extends \Piwik\Plugin\Controller
* {
* public function index()
@ -59,17 +63,17 @@ use Piwik\ViewDataTable\Factory as ViewDataTableFactory;
* return $view->render();
* }
* }
*
*
* **Linking to a controller action**
*
* <a href="?module=MyPlugin&action=index&idSite=1&period=day&date=2013-10-10">Link</a>
*
*
*/
abstract class Controller
{
/**
* The plugin name, eg. `'Referrers'`.
*
*
* @var string
* @api
*/
@ -93,7 +97,7 @@ abstract class Controller
/**
* The value of the **idSite** query parameter.
*
*
* @var int
* @api
*/
@ -101,7 +105,7 @@ abstract class Controller
/**
* The Site object created with {@link $idSite}.
*
*
* @var Site
* @api
*/
@ -109,7 +113,7 @@ abstract class Controller
/**
* Constructor.
*
*
* @api
*/
public function __construct()
@ -177,6 +181,50 @@ abstract class Controller
$this->strDate = $date->toString();
}
/**
* Returns values that are enabled for the parameter &period=
* @return array eg. array('day', 'week', 'month', 'year', 'range')
*/
protected static function getEnabledPeriodsInUI()
{
$periodValidator = new PeriodValidator();
return $periodValidator->getPeriodsAllowedForUI();
}
/**
* @return array
*/
private static function getEnabledPeriodsNames()
{
$availablePeriods = self::getEnabledPeriodsInUI();
$periodNames = array(
'day' => array(
'singular' => Piwik::translate('Intl_PeriodDay'),
'plural' => Piwik::translate('Intl_PeriodDays')
),
'week' => array(
'singular' => Piwik::translate('Intl_PeriodWeek'),
'plural' => Piwik::translate('Intl_PeriodWeeks')
),
'month' => array(
'singular' => Piwik::translate('Intl_PeriodMonth'),
'plural' => Piwik::translate('Intl_PeriodMonths')
),
'year' => array(
'singular' => Piwik::translate('Intl_PeriodYear'),
'plural' => Piwik::translate('Intl_PeriodYears')
),
// Note: plural is not used for date range
'range' => array(
'singular' => Piwik::translate('General_DateRangeInPeriodList'),
'plural' => Piwik::translate('General_DateRangeInPeriodList')
),
);
$periodNames = array_intersect_key($periodNames, array_fill_keys($availablePeriods, true));
return $periodNames;
}
/**
* Returns the name of the default method that will be called
* when visiting: index.php?module=PluginName without the action parameter.
@ -200,10 +248,60 @@ abstract class Controller
return $view->render();
}
/**
* Assigns the given variables to the template and renders it.
*
* Example:
*
* public function myControllerAction () {
* return $this->renderTemplate('index', array(
* 'answerToLife' => '42'
* ));
* }
*
* This will render the 'index.twig' file within the plugin templates folder and assign the view variable
* `answerToLife` to `42`.
*
* @param string $template The name of the template file. If only a name is given it will automatically use
* the template within the plugin folder. For instance 'myTemplate' will result in
* '@$pluginName/myTemplate.twig'. Alternatively you can include the full path:
* '@anyOtherFolder/otherTemplate'. The trailing '.twig' is not needed.
* @param array $variables For instance array('myViewVar' => 'myValue'). In template you can use {{ myViewVar }}
* @return string
* @since 2.5.0
* @api
*/
protected function renderTemplate($template, array $variables = array())
{
if (false === strpos($template, '@') || false === strpos($template, '/')) {
$template = '@' . $this->pluginName . '/' . $template;
}
$view = new View($template);
// alternatively we could check whether the templates extends either admin.twig or dashboard.twig and based on
// that call the correct method. This will be needed once we unify Controller and ControllerAdmin see
// https://github.com/piwik/piwik/issues/6151
if ($this instanceof ControllerAdmin) {
$this->setBasicVariablesView($view);
} elseif (empty($this->site) || empty($this->idSite)) {
$this->setBasicVariablesView($view);
} else {
$this->setGeneralVariablesView($view);
}
foreach ($variables as $key => $value) {
$view->$key = $value;
}
return $view->render();
}
/**
* Convenience method that creates and renders a ViewDataTable for a API method.
*
* @param string $apiAction The name of the API action (eg, `'getResolution'`).
* @param string|\Piwik\Plugin\Report $apiAction The name of the API action (eg, `'getResolution'`) or
* an instance of an report.
* @param bool $controllerAction The name of the Controller action name that is rendering the report. Defaults
* to the `$apiAction`.
* @param bool $fetch If `true`, the rendered string is returned, if `false` it is `echo`'d.
@ -214,6 +312,21 @@ abstract class Controller
*/
protected function renderReport($apiAction, $controllerAction = false)
{
if (empty($controllerAction) && is_string($apiAction)) {
$report = Report::factory($this->pluginName, $apiAction);
if (!empty($report)) {
$apiAction = $report;
}
}
if ($apiAction instanceof Report) {
$this->checkSitePermission();
$apiAction->checkIsEnabled();
return $apiAction->render();
}
$pluginName = $this->pluginName;
/** @var Proxy $apiProxy */
@ -250,7 +363,7 @@ abstract class Controller
protected function getLastUnitGraph($currentModuleName, $currentControllerAction, $apiMethod)
{
$view = ViewDataTableFactory::build(
'graphEvolution', $apiMethod, $currentModuleName . '.' . $currentControllerAction, $forceDefault = true);
Evolution::ID, $apiMethod, $currentModuleName . '.' . $currentControllerAction, $forceDefault = true);
$view->config->show_goals = false;
return $view;
}
@ -282,7 +395,7 @@ abstract class Controller
$date = Common::getRequestVar('date');
$meta = \Piwik\Plugins\API\API::getInstance()->getReportMetadata($idSite, $period, $date);
$columns = array_merge($columnsToDisplay, $selectableColumns);
$columns = array_merge($columnsToDisplay ? $columnsToDisplay : array(), $selectableColumns);
$translations = array_combine($columns, $columns);
foreach ($meta as $reportMeta) {
if ($reportMeta['action'] == 'get' && !isset($reportMeta['parameters'])) {
@ -296,6 +409,7 @@ abstract class Controller
// initialize the graph and load the data
$view = $this->getLastUnitGraph($currentModuleName, $currentControllerAction, $apiMethod);
if ($columnsToDisplay !== false) {
$view->config->columns_to_display = $columnsToDisplay;
}
@ -379,11 +493,11 @@ abstract class Controller
/**
* Returns a URL to a sparkline image for a report served by the current plugin.
*
*
* The result of this URL should be used with the [sparkline()](/api-reference/Piwik/View#twig) twig function.
*
*
* The current site ID and period will be used.
*
*
* @param string $action Method name of the controller that serves the report.
* @param array $customParameters The array of query parameter name/value pairs that
* should be set in result URL.
@ -439,9 +553,9 @@ abstract class Controller
/**
* Assigns variables to {@link Piwik\View} instances that display an entire page.
*
*
* The following variables assigned:
*
*
* **date** - The value of the **date** query parameter.
* **idSite** - The value of the **idSite** query parameter.
* **rawDate** - The value of the **date** query parameter.
@ -454,81 +568,105 @@ abstract class Controller
* **config_action_url_category_delimiter** - The value of the `[General] action_url_category_delimiter`
* INI config option.
* **topMenu** - The result of `MenuTop::getInstance()->getMenu()`.
*
*
* As well as the variables set by {@link setPeriodVariablesView()}.
*
*
* Will exit on error.
*
*
* @param View $view
* @return void
* @api
*/
protected function setGeneralVariablesView($view)
{
$view->idSite = $this->idSite;
$this->checkSitePermission();
$this->setPeriodVariablesView($view);
$view->siteName = $this->site->getName();
$view->siteMainUrl = $this->site->getMainUrl();
$siteTimezone = $this->site->getTimezone();
$datetimeMinDate = $this->site->getCreationDate()->getDatetime();
$minDate = Date::factory($datetimeMinDate, $siteTimezone);
$this->setMinDateView($minDate, $view);
$maxDate = Date::factory('now', $siteTimezone);
$this->setMaxDateView($maxDate, $view);
$rawDate = Common::getRequestVar('date');
Period::checkDateFormat($rawDate);
$periodStr = Common::getRequestVar('period');
if ($periodStr != 'range') {
$date = Date::factory($this->strDate);
$validDate = $this->getValidDate($date, $minDate, $maxDate);
$period = Period\Factory::build($periodStr, $validDate);
if ($date->toString() !== $validDate->toString()) {
// we to not always change date since it could convert a strDate "today" to "YYYY-MM-DD"
// only change $this->strDate if it was not valid before
$this->setDate($validDate);
}
} else {
$period = new Range($periodStr, $rawDate, $siteTimezone);
}
// Setting current period start & end dates, for pre-setting the calendar when "Date Range" is selected
$dateStart = $period->getDateStart();
$dateStart = $this->getValidDate($dateStart, $minDate, $maxDate);
$dateEnd = $period->getDateEnd();
$dateEnd = $this->getValidDate($dateEnd, $minDate, $maxDate);
if ($periodStr == 'range') {
// make sure we actually display the correct calendar pretty date
$newRawDate = $dateStart->toString() . ',' . $dateEnd->toString();
$period = new Range($periodStr, $newRawDate, $siteTimezone);
}
$view->date = $this->strDate;
$view->prettyDate = self::getCalendarPrettyDate($period);
$view->prettyDateLong = $period->getLocalizedLongString();
$view->rawDate = $rawDate;
$view->startDate = $dateStart;
$view->endDate = $dateEnd;
try {
$view->idSite = $this->idSite;
if (empty($this->site) || empty($this->idSite)) {
throw new Exception("The requested website idSite is not found in the request, or is invalid.
Please check that you are logged in Piwik and have permission to access the specified website.");
}
$this->setPeriodVariablesView($view);
$language = LanguagesManager::getLanguageForSession();
$view->language = !empty($language) ? $language : LanguagesManager::getLanguageCodeForCurrentUser();
$rawDate = Common::getRequestVar('date');
$periodStr = Common::getRequestVar('period');
if ($periodStr != 'range') {
$date = Date::factory($this->strDate);
$period = Period::factory($periodStr, $date);
} else {
$period = new Range($periodStr, $rawDate, $this->site->getTimezone());
}
$view->rawDate = $rawDate;
$view->prettyDate = self::getCalendarPrettyDate($period);
$this->setBasicVariablesView($view);
$view->siteName = $this->site->getName();
$view->siteMainUrl = $this->site->getMainUrl();
$view->topMenu = MenuTop::getInstance()->getMenu();
$view->userMenu = MenuUser::getInstance()->getMenu();
$datetimeMinDate = $this->site->getCreationDate()->getDatetime();
$minDate = Date::factory($datetimeMinDate, $this->site->getTimezone());
$this->setMinDateView($minDate, $view);
$maxDate = Date::factory('now', $this->site->getTimezone());
$this->setMaxDateView($maxDate, $view);
// Setting current period start & end dates, for pre-setting the calendar when "Date Range" is selected
$dateStart = $period->getDateStart();
if ($dateStart->isEarlier($minDate)) {
$dateStart = $minDate;
}
$dateEnd = $period->getDateEnd();
if ($dateEnd->isLater($maxDate)) {
$dateEnd = $maxDate;
}
$view->startDate = $dateStart;
$view->endDate = $dateEnd;
$language = LanguagesManager::getLanguageForSession();
$view->language = !empty($language) ? $language : LanguagesManager::getLanguageCodeForCurrentUser();
$this->setBasicVariablesView($view);
$view->topMenu = MenuTop::getInstance()->getMenu();
$notifications = $view->notifications;
if (empty($notifications)) {
$view->notifications = NotificationManager::getAllNotificationsToDisplay();
NotificationManager::cancelAllNonPersistent();
} catch (Exception $e) {
Piwik_ExitWithMessage($e->getMessage(), $e->getTraceAsString());
}
}
private function getValidDate(Date $date, Date $minDate, Date $maxDate)
{
if ($date->isEarlier($minDate)) {
$date = $minDate;
}
if ($date->isLater($maxDate)) {
$date = $maxDate;
}
return $date;
}
/**
* Assigns a set of generally useful variables to a {@link Piwik\View} instance.
*
*
* The following variables assigned:
*
* **debugTrackVisitsInsidePiwikUI** - The value of the `[Debug] track_visits_inside_piwik_ui`
* INI config option.
*
* **isSuperUser** - True if the current user is the Super User, false if otherwise.
* **hasSomeAdminAccess** - True if the current user has admin access to at least one site,
* false if otherwise.
@ -539,7 +677,7 @@ abstract class Controller
* **hasSVGLogo** - True if there is a SVG logo, false if otherwise.
* **enableFrames** - The value of the `[General] enable_framed_pages` INI config option. If
* true, {@link Piwik\View::setXFrameOptions()} is called on the view.
*
*
* Also calls {@link setHostValidationVariablesView()}.
*
* @param View $view
@ -548,15 +686,17 @@ abstract class Controller
protected function setBasicVariablesView($view)
{
$view->clientSideConfig = PiwikConfig::getInstance()->getClientSideOptions();
$view->debugTrackVisitsInsidePiwikUI = PiwikConfig::getInstance()->Debug['track_visits_inside_piwik_ui'];
$view->isSuperUser = Access::getInstance()->hasSuperUserAccess();
$view->hasSomeAdminAccess = Piwik::isUserHasSomeAdminAccess();
$view->hasSomeViewAccess = Piwik::isUserHasSomeViewAccess();
$view->isUserIsAnonymous = Piwik::isUserIsAnonymous();
$view->hasSuperUserAccess = Piwik::hasUserSuperUserAccess();
$customLogo = new CustomLogo();
$view->isCustomLogo = $customLogo->isEnabled();
if (!Piwik::isUserIsAnonymous()) {
$view->emailSuperUser = implode(',', Piwik::getAllSuperUserAccessEmailAddresses());
}
$this->addCustomLogoInfo($view);
$view->logoHeader = \Piwik\Plugins\API\API::getInstance()->getHeaderLogoUrl();
$view->logoLarge = \Piwik\Plugins\API\API::getInstance()->getLogoUrl();
@ -574,6 +714,13 @@ abstract class Controller
self::setHostValidationVariablesView($view);
}
protected function addCustomLogoInfo($view)
{
$customLogo = new CustomLogo();
$view->isCustomLogo = $customLogo->isEnabled();
$view->customFavicon = $customLogo->getPathUserFavicon();
}
/**
* Checks if the current host is valid and sets variables on the given view, including:
*
@ -631,7 +778,7 @@ abstract class Controller
$validHost,
'</a>'
));
} else if (Piwik::isUserIsAnonymous()) {
} elseif (Piwik::isUserIsAnonymous()) {
$view->invalidHostMessage = $warningStart . ' '
. Piwik::translate('CoreHome_InjectedHostNonSuperUserWarning', array(
"<br/><a href=\"$validUrl\">",
@ -660,7 +807,7 @@ abstract class Controller
/**
* Sets general period variables on a view, including:
*
*
* - **displayUniqueVisitors** - Whether unique visitors should be displayed for the current
* period.
* - **period** - The value of the **period** query parameter.
@ -677,33 +824,27 @@ abstract class Controller
return;
}
$periodValidator = new PeriodValidator();
$currentPeriod = Common::getRequestVar('period');
$view->displayUniqueVisitors = SettingsPiwik::isUniqueVisitorsEnabled($currentPeriod);
$availablePeriods = array('day', 'week', 'month', 'year', 'range');
if (!in_array($currentPeriod, $availablePeriods)) {
throw new Exception("Period must be one of: " . implode(",", $availablePeriods));
$availablePeriods = $periodValidator->getPeriodsAllowedForUI();
if (! $periodValidator->isPeriodAllowedForUI($currentPeriod)) {
throw new Exception("Period must be one of: " . implode(", ", $availablePeriods));
}
$periodNames = array(
'day' => array('singular' => Piwik::translate('CoreHome_PeriodDay'), 'plural' => Piwik::translate('CoreHome_PeriodDays')),
'week' => array('singular' => Piwik::translate('CoreHome_PeriodWeek'), 'plural' => Piwik::translate('CoreHome_PeriodWeeks')),
'month' => array('singular' => Piwik::translate('CoreHome_PeriodMonth'), 'plural' => Piwik::translate('CoreHome_PeriodMonths')),
'year' => array('singular' => Piwik::translate('CoreHome_PeriodYear'), 'plural' => Piwik::translate('CoreHome_PeriodYears')),
// Note: plural is not used for date range
'range' => array('singular' => Piwik::translate('General_DateRangeInPeriodList'), 'plural' => Piwik::translate('General_DateRangeInPeriodList')),
);
$found = array_search($currentPeriod, $availablePeriods);
if ($found !== false) {
unset($availablePeriods[$found]);
}
unset($availablePeriods[$found]);
$view->period = $currentPeriod;
$view->otherPeriods = $availablePeriods;
$view->periodsNames = $periodNames;
$view->periodsNames = self::getEnabledPeriodsNames();
}
/**
* Helper method used to redirect the current HTTP request to another module/action.
*
*
* This function will exit immediately after executing.
*
* @param string $moduleToRedirect The plugin to redirect to, eg. `"MultiSites"`.
@ -717,132 +858,46 @@ abstract class Controller
public function redirectToIndex($moduleToRedirect, $actionToRedirect, $websiteId = null, $defaultPeriod = null,
$defaultDate = null, $parameters = array())
{
if (empty($websiteId)) {
$websiteId = $this->getDefaultWebsiteId();
}
if (empty($defaultDate)) {
$defaultDate = $this->getDefaultDate();
}
if (empty($defaultPeriod)) {
$defaultPeriod = $this->getDefaultPeriod();
}
$parametersString = '';
if (!empty($parameters)) {
$parametersString = '&' . Url::getQueryStringFromParameters($parameters);
}
if ($websiteId) {
$url = "Location: index.php?module=" . $moduleToRedirect
. "&action=" . $actionToRedirect
. "&idSite=" . $websiteId
. "&period=" . $defaultPeriod
. "&date=" . $defaultDate
. $parametersString;
header($url);
exit;
try {
$this->doRedirectToUrl($moduleToRedirect, $actionToRedirect, $websiteId, $defaultPeriod, $defaultDate, $parameters);
} catch (Exception $e) {
// no website ID to default to, so could not redirect
}
if (Piwik::hasUserSuperUserAccess()) {
Piwik_ExitWithMessage("Error: no website was found in this Piwik installation.
<br />Check the table '" . Common::prefixTable('site') . "' in your database, it should contain your Piwik websites.", false, true);
$siteTableName = Common::prefixTable('site');
$message = "Error: no website was found in this Piwik installation.
<br />Check the table '$siteTableName' in your database, it should contain your Piwik websites.";
$ex = new NoWebsiteFoundException($message);
$ex->setIsHtmlMessage();
throw $ex;
}
$currentLogin = Piwik::getCurrentUserLogin();
if (!empty($currentLogin)
&& $currentLogin != 'anonymous'
) {
if (!Piwik::isUserIsAnonymous()) {
$currentLogin = Piwik::getCurrentUserLogin();
$emails = implode(',', Piwik::getAllSuperUserAccessEmailAddresses());
$errorMessage = sprintf(Piwik::translate('CoreHome_NoPrivilegesAskPiwikAdmin'), $currentLogin, "<br/><a href='mailto:" . $emails . "?subject=Access to Piwik for user $currentLogin'>", "</a>");
$errorMessage .= "<br /><br />&nbsp;&nbsp;&nbsp;<b><a href='index.php?module=" . Registry::get('auth')->getName() . "&amp;action=logout'>&rsaquo; " . Piwik::translate('General_Logout') . "</a></b><br />";
Piwik_ExitWithMessage($errorMessage, false, true);
$errorMessage = sprintf(Piwik::translate('CoreHome_NoPrivilegesAskPiwikAdmin'), $currentLogin, "<br/><a href='mailto:" . $emails . "?subject=Access to Piwik for user $currentLogin'>", "</a>");
$errorMessage .= "<br /><br />&nbsp;&nbsp;&nbsp;<b><a href='index.php?module=" . Piwik::getLoginPluginName() . "&amp;action=logout'>&rsaquo; " . Piwik::translate('General_Logout') . "</a></b><br />";
$ex = new NoPrivilegesException($errorMessage);
$ex->setIsHtmlMessage();
throw $ex;
}
echo FrontController::getInstance()->dispatch(Piwik::getLoginPluginName(), false);
exit;
}
/**
* Returns default site ID that Piwik should load.
*
* _Note: This value is a Piwik setting set by each user._
*
* @return bool|int
* @api
*/
protected function getDefaultWebsiteId()
{
$defaultWebsiteId = false;
// User preference: default website ID to load
$defaultReport = APIUsersManager::getInstance()->getUserPreference(Piwik::getCurrentUserLogin(), APIUsersManager::PREFERENCE_DEFAULT_REPORT);
if (is_numeric($defaultReport)) {
$defaultWebsiteId = $defaultReport;
}
if ($defaultWebsiteId && Piwik::isUserHasViewAccess($defaultWebsiteId)) {
return $defaultWebsiteId;
}
$sitesId = APISitesManager::getInstance()->getSitesIdWithAtLeastViewAccess();
if (!empty($sitesId)) {
return $sitesId[0];
}
return false;
}
/**
* Returns default date for Piwik reports.
*
* _Note: This value is a Piwik setting set by each user._
*
* @return string `'today'`, `'2010-01-01'`, etc.
* @api
*/
protected function getDefaultDate()
{
// NOTE: a change in this function might mean a change in plugins/UsersManager/javascripts/usersSettings.js as well
$userSettingsDate = APIUsersManager::getInstance()->getUserPreference(Piwik::getCurrentUserLogin(), APIUsersManager::PREFERENCE_DEFAULT_REPORT_DATE);
if ($userSettingsDate == 'yesterday') {
return $userSettingsDate;
}
// if last7, last30, etc.
if (strpos($userSettingsDate, 'last') === 0
|| strpos($userSettingsDate, 'previous') === 0
) {
return $userSettingsDate;
}
return 'today';
}
/**
* Returns default period type for Piwik reports.
*
* @return string `'day'`, `'week'`, `'month'`, `'year'` or `'range'`
* @api
*/
protected function getDefaultPeriod()
{
$userSettingsDate = APIUsersManager::getInstance()->getUserPreference(Piwik::getCurrentUserLogin(), APIUsersManager::PREFERENCE_DEFAULT_REPORT_DATE);
if ($userSettingsDate === false) {
return PiwikConfig::getInstance()->General['default_period'];
}
if (in_array($userSettingsDate, array('today', 'yesterday'))) {
return 'day';
}
if (strpos($userSettingsDate, 'last') === 0
|| strpos($userSettingsDate, 'previous') === 0
) {
return 'range';
}
return $userSettingsDate;
}
/**
* Checks that the token_auth in the URL matches the currently logged-in user's token_auth.
*
*
* This is a protection against CSRF and should be used in all controller
* methods that modify Piwik or any user settings.
*
*
* **The token_auth should never appear in the browser's address bar.**
*
* @throws \Piwik\NoAccessException If the token doesn't match.
@ -850,7 +905,14 @@ abstract class Controller
*/
protected function checkTokenInUrl()
{
if (Common::getRequestVar('token_auth', false) != Piwik::getCurrentUserTokenAuth()) {
$tokenRequest = Common::getRequestVar('token_auth', false);
$tokenUser = Piwik::getCurrentUserTokenAuth();
if (empty($tokenRequest) && empty($tokenUser)) {
return; // UI tests
}
if ($tokenRequest !== $tokenUser) {
throw new NoAccessException(Piwik::translate('General_ExceptionInvalidToken'));
}
}
@ -864,8 +926,9 @@ abstract class Controller
*/
public static function getCalendarPrettyDate($period)
{
if ($period instanceof Month) // show month name when period is for a month
{
if ($period instanceof Month) {
// show month name when period is for a month
return $period->getLocalizedLongString();
} else {
return $period->getPrettyString();
@ -881,7 +944,7 @@ abstract class Controller
*/
public static function getPrettyDate($date, $period)
{
return self::getCalendarPrettyDate(Period::factory($period, Date::factory($date)));
return self::getCalendarPrettyDate(Period\Factory::build($period, Date::factory($date)));
}
/**
@ -914,7 +977,7 @@ abstract class Controller
if ($evolutionPercent < 0) {
$class = "negative-evolution";
$img = "arrow_down.png";
} else if ($evolutionPercent == 0) {
} elseif ($evolutionPercent == 0) {
$class = "neutral-evolution";
$img = "stop.png";
} else {
@ -923,6 +986,9 @@ abstract class Controller
$titleEvolutionPercent = '+' . $titleEvolutionPercent;
}
$currentValue = NumberFormatter::getInstance()->format($currentValue);
$pastValue = NumberFormatter::getInstance()->format($pastValue);
$title = Piwik::translate('General_EvolutionSummaryGeneric', array(
Piwik::translate('General_NVisits', $currentValue),
$date,
@ -941,4 +1007,38 @@ abstract class Controller
return $result;
}
protected function checkSitePermission()
{
if (!empty($this->idSite) && empty($this->site)) {
throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAccessWebsite', array("'view'", $this->idSite)));
} elseif (empty($this->site) || empty($this->idSite)) {
throw new Exception("The requested website idSite is not found in the request, or is invalid.
Please check that you are logged in Piwik and have permission to access the specified website.");
}
}
/**
* @param $moduleToRedirect
* @param $actionToRedirect
* @param $websiteId
* @param $defaultPeriod
* @param $defaultDate
* @param $parameters
* @throws Exception
*/
private function doRedirectToUrl($moduleToRedirect, $actionToRedirect, $websiteId, $defaultPeriod, $defaultDate, $parameters)
{
$menu = new Menu();
$parameters = array_merge(
$menu->urlForDefaultUserParams($websiteId, $defaultPeriod, $defaultDate),
$parameters
);
$queryParams = !empty($parameters) ? '&' . Url::getQueryStringFromParameters($parameters) : '';
$url = "index.php?module=%s&action=%s";
$url = sprintf($url, $moduleToRedirect, $actionToRedirect);
$url = $url . $queryParams;
Url::redirectToUrl($url);
}
}

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
@ -10,25 +10,27 @@ namespace Piwik\Plugin;
use Piwik\Config as PiwikConfig;
use Piwik\Config;
use Piwik\Development;
use Piwik\Menu\MenuAdmin;
use Piwik\Menu\MenuTop;
use Piwik\Menu\MenuUser;
use Piwik\Notification;
use Piwik\Notification\Manager as NotificationManager;
use Piwik\Piwik;
use Piwik\Tracker\TrackerConfig;
use Piwik\Url;
use Piwik\Version;
use Piwik\View;
use Piwik\ProxyHttp;
/**
* Base class of plugin controllers that provide administrative functionality.
*
*
* See {@link Controller} to learn more about Piwik controllers.
*
*
*/
abstract class ControllerAdmin extends Controller
{
private static $isEacceleratorUsed = false;
private static function notifyWhenTrackingStatisticsDisabled()
{
$statsEnabled = PiwikConfig::getInstance()->Tracker['record_statistics'];
@ -42,6 +44,7 @@ abstract class ControllerAdmin extends Controller
private static function notifyAnyInvalidPlugin()
{
$missingPlugins = \Piwik\Plugin\Manager::getInstance()->getMissingPlugins();
if (empty($missingPlugins)) {
return;
}
@ -49,12 +52,15 @@ abstract class ControllerAdmin extends Controller
if (!Piwik::hasUserSuperUserAccess()) {
return;
}
$pluginsLink = Url::getCurrentQueryStringWithParametersModified(array(
'module' => 'CorePluginsAdmin', 'action' => 'plugins'
));
$invalidPluginsWarning = Piwik::translate('CoreAdminHome_InvalidPluginsWarning', array(
self::getPiwikVersion(),
'<strong>' . implode('</strong>,&nbsp;<strong>', $missingPlugins) . '</strong>'))
. "<br/>"
. Piwik::translate('CoreAdminHome_InvalidPluginsYouCanUninstall', array(
'<a href="' . $pluginsLink . '"/>',
'</a>'
@ -63,7 +69,7 @@ abstract class ControllerAdmin extends Controller
$notification = new Notification($invalidPluginsWarning);
$notification->raw = true;
$notification->context = Notification::CONTEXT_WARNING;
$notification->title = Piwik::translate('General_Warning') . ':';
$notification->title = Piwik::translate('General_Warning');
Notification\Manager::notify('ControllerAdmin_InvalidPluginsWarning', $notification);
}
@ -81,10 +87,40 @@ abstract class ControllerAdmin extends Controller
self::setBasicVariablesAdminView($view);
}
private static function notifyIfURLIsNotSecure()
{
$isURLSecure = ProxyHttp::isHttps();
if ($isURLSecure) {
return;
}
if (!Piwik::hasUserSuperUserAccess()) {
return;
}
if(Url::isLocalHost(Url::getCurrentHost())) {
return;
}
$message = Piwik::translate('General_CurrentlyUsingUnsecureHttp');
$message .= " ";
$message .= Piwik::translate('General_ReadThisToLearnMore',
array('<a rel="noreferrer" target="_blank" href="https://piwik.org/faq/how-to/faq_91/">', '</a>')
);
$notification = new Notification($message);
$notification->context = Notification::CONTEXT_WARNING;
$notification->raw = true;
Notification\Manager::notify('ControllerAdmin_HttpIsUsed', $notification);
}
/**
* @ignore
*/
static public function displayWarningIfConfigFileNotWritable()
public static function displayWarningIfConfigFileNotWritable()
{
$isConfigFileWritable = PiwikConfig::getInstance()->isFileWritable();
@ -99,36 +135,69 @@ abstract class ControllerAdmin extends Controller
}
}
/**
* See http://dev.piwik.org/trac/ticket/4439#comment:8 and https://github.com/eaccelerator/eaccelerator/issues/12
*
* Eaccelerator does not support closures and is known to be not comptabile with Piwik. Therefore we are disabling
* it automatically. At this point it looks like Eaccelerator is no longer under development and the bug has not
* been fixed within a year.
*/
public static function disableEacceleratorIfEnabled()
{
$isEacceleratorUsed = ini_get('eaccelerator.enable');
if (!empty($isEacceleratorUsed)) {
self::$isEacceleratorUsed = true;
@ini_set('eaccelerator.enable', 0);
}
}
private static function notifyIfEAcceleratorIsUsed()
{
if (self::$isEacceleratorUsed) {
$message = sprintf("You are using the PHP accelerator & optimizer eAccelerator which is known to be not compatible with Piwik.
We have disabled eAccelerator, which might affect the performance of Piwik.
Read the %srelated ticket%s for more information and how to fix this problem.",
'<a target="_blank" href="http://dev.piwik.org/trac/ticket/4439">', '</a>');
$isEacceleratorUsed = ini_get('eaccelerator.enable');
if (empty($isEacceleratorUsed)) {
return;
}
$message = sprintf("You are using the PHP accelerator & optimizer eAccelerator which is known to be not compatible with Piwik.
We have disabled eAccelerator, which might affect the performance of Piwik.
Read the %srelated ticket%s for more information and how to fix this problem.",
'<a rel="noreferrer" target="_blank" href="https://github.com/piwik/piwik/issues/4439">', '</a>');
$notification = new Notification($message);
$notification->context = Notification::CONTEXT_WARNING;
$notification->raw = true;
Notification\Manager::notify('ControllerAdmin_EacceleratorIsUsed', $notification);
}
private static function notifyWhenPhpVersionIsEOL()
{
$deprecatedMajorPhpVersion = null;
if(self::isPhpVersion53()) {
$deprecatedMajorPhpVersion = '5.3';
} elseif(self::isPhpVersion54()) {
$deprecatedMajorPhpVersion = '5.4';
}
$notifyPhpIsEOL = Piwik::hasUserSuperUserAccess() && $deprecatedMajorPhpVersion;
if (!$notifyPhpIsEOL) {
return;
}
$nextRequiredMinimumPHP = '5.5';
$message = Piwik::translate('General_WarningPiwikWillStopSupportingPHPVersion', array($deprecatedMajorPhpVersion, $nextRequiredMinimumPHP))
. "\n "
. Piwik::translate('General_WarningPhpVersionXIsTooOld', $deprecatedMajorPhpVersion);
$notification = new Notification($message);
$notification->title = Piwik::translate('General_Warning');
$notification->priority = Notification::PRIORITY_LOW;
$notification->context = Notification::CONTEXT_WARNING;
$notification->type = Notification::TYPE_TRANSIENT;
$notification->flags = Notification::FLAG_NO_CLEAR;
NotificationManager::notify('DeprecatedPHPVersionCheck', $notification);
}
private static function notifyWhenDebugOnDemandIsEnabled($trackerSetting)
{
if (!Development::isEnabled()
&& Piwik::hasUserSuperUserAccess() &&
TrackerConfig::getConfigValue($trackerSetting)) {
$message = Piwik::translate('General_WarningDebugOnDemandEnabled');
$message = sprintf($message, '"' . $trackerSetting . '"', '"[Tracker] ' . $trackerSetting . '"', '"0"',
'"config/config.ini.php"');
$notification = new Notification($message);
$notification->title = Piwik::translate('General_Warning');
$notification->priority = Notification::PRIORITY_LOW;
$notification->context = Notification::CONTEXT_WARNING;
$notification->raw = true;
Notification\Manager::notify('ControllerAdmin_EacceleratorIsUsed', $notification);
$notification->type = Notification::TYPE_TRANSIENT;
$notification->flags = Notification::FLAG_NO_CLEAR;
NotificationManager::notify('Tracker' . $trackerSetting, $notification);
}
}
@ -140,7 +209,6 @@ abstract class ControllerAdmin extends Controller
* - **statisticsNotRecorded** - Set to true if the `[Tracker] record_statistics` INI
* config is `0`. If not `0`, this variable will not be defined.
* - **topMenu** - The result of `MenuTop::getInstance()->getMenu()`.
* - **currentAdminMenuName** - The currently selected admin menu name.
* - **enableFrames** - The value of the `[General] enable_framed_pages` INI config option. If
* true, {@link Piwik\View::setXFrameOptions()} is called on the view.
* - **isSuperUser** - Whether the current user is a superuser or not.
@ -155,17 +223,20 @@ abstract class ControllerAdmin extends Controller
* @param View $view
* @api
*/
static public function setBasicVariablesAdminView(View $view)
public static function setBasicVariablesAdminView(View $view)
{
self::notifyWhenTrackingStatisticsDisabled();
self::notifyIfEAcceleratorIsUsed();
self::notifyIfURLIsNotSecure();
$view->topMenu = MenuTop::getInstance()->getMenu();
$view->currentAdminMenuName = MenuAdmin::getInstance()->getCurrentAdminMenuName();
$view->topMenu = MenuTop::getInstance()->getMenu();
$view->userMenu = MenuUser::getInstance()->getMenu();
$view->isDataPurgeSettingsEnabled = self::isDataPurgeSettingsEnabled();
$view->enableFrames = PiwikConfig::getInstance()->General['enable_framed_settings'];
if (!$view->enableFrames) {
$enableFrames = PiwikConfig::getInstance()->General['enable_framed_settings'];
$view->enableFrames = $enableFrames;
if (!$enableFrames) {
$view->setXFrameOptions('sameorigin');
}
@ -175,19 +246,27 @@ abstract class ControllerAdmin extends Controller
self::checkPhpVersion($view);
self::notifyWhenPhpVersionIsEOL();
self::notifyWhenDebugOnDemandIsEnabled('debug');
self::notifyWhenDebugOnDemandIsEnabled('debug_on_demand');
$adminMenu = MenuAdmin::getInstance()->getMenu();
$view->adminMenu = $adminMenu;
$view->notifications = NotificationManager::getAllNotificationsToDisplay();
NotificationManager::cancelAllNonPersistent();
$notifications = $view->notifications;
if (empty($notifications)) {
$view->notifications = NotificationManager::getAllNotificationsToDisplay();
NotificationManager::cancelAllNonPersistent();
}
}
static public function isDataPurgeSettingsEnabled()
public static function isDataPurgeSettingsEnabled()
{
return (bool) Config::getInstance()->General['enable_delete_old_data_settings_admin'];
}
static protected function getPiwikVersion()
protected static function getPiwikVersion()
{
return "Piwik " . Version::VERSION;
}
@ -202,12 +281,13 @@ abstract class ControllerAdmin extends Controller
$view->phpIsNewEnough = version_compare($view->phpVersion, '5.3.0', '>=');
}
protected function getDefaultWebsiteId()
private static function isPhpVersion53()
{
$sitesId = \Piwik\Plugins\SitesManager\API::getInstance()->getSitesIdWithAdminAccess();
if (!empty($sitesId)) {
return $sitesId[0];
}
return parent::getDefaultWebsiteId();
return strpos(PHP_VERSION, '5.3') === 0;
}
private static function isPhpVersion54()
{
return strpos(PHP_VERSION, '5.4') === 0;
}
}

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,8 +8,9 @@
*/
namespace Piwik\Plugin;
use Piwik\Version;
use Piwik\Plugin\Manager as PluginManager;
use Piwik\Version;
/**
*
*/
@ -50,12 +51,11 @@ class Dependency
public function getMissingVersions($currentVersion, $requiredVersion)
{
$currentVersion = trim($currentVersion);
$requiredVersions = explode(',' , (string) $requiredVersion);
$requiredVersions = explode(',', (string) $requiredVersion);
$missingVersions = array();
foreach ($requiredVersions as $required) {
$comparison = '>=';
$required = trim($required);
@ -97,7 +97,8 @@ class Dependency
if (!empty($plugin)) {
return $plugin->getVersion();
}
} catch (\Exception $e) {}
} catch (\Exception $e) {
}
}
return '';

View file

@ -0,0 +1,254 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugin\Dimension;
use Piwik\CacheId;
use Piwik\Cache as PiwikCache;
use Piwik\Columns\Dimension;
use Piwik\Plugin\Manager as PluginManager;
use Piwik\Plugin\Segment;
use Piwik\Common;
use Piwik\Plugin;
use Piwik\Db;
use Piwik\Tracker\Action;
use Piwik\Tracker\Request;
use Piwik\Tracker\Visitor;
use Exception;
/**
* Defines a new action dimension that records any information during tracking for each action.
*
* You can record any action information by implementing one of the following events: {@link onLookupAction()} and
* {@link getActionId()} or {@link onNewAction()}. By defining a {@link $columnName} and {@link $columnType} a new
* column will be created in the database (table `log_link_visit_action`) automatically and the values you return in
* the previous mentioned events will be saved in this column.
*
* You can create a new dimension using the console command `./console generate:dimension`.
*
* @api
* @since 2.5.0
*/
abstract class ActionDimension extends Dimension
{
const INSTALLER_PREFIX = 'log_link_visit_action.';
private $tableName = 'log_link_visit_action';
/**
* Installs the action dimension in case it is not installed yet. The installation is already implemented based on
* the {@link $columnName} and {@link $columnType}. If you want to perform additional actions beside adding the
* column to the database - for instance adding an index - you can overwrite this method. We recommend to call
* this parent method to get the minimum required actions and then add further custom actions since this makes sure
* the column will be installed correctly. We also recommend to change the default install behavior only if really
* needed. FYI: We do not directly execute those alter table statements here as we group them together with several
* other alter table statements do execute those changes in one step which results in a faster installation. The
* column will be added to the `log_link_visit_action` MySQL table.
*
* Example:
* ```
public function install()
{
$changes = parent::install();
$changes['log_link_visit_action'][] = "ADD INDEX index_idsite_servertime ( idsite, server_time )";
return $changes;
}
```
*
* @return array An array containing the table name as key and an array of MySQL alter table statements that should
* be executed on the given table. Example:
* ```
array(
'log_link_visit_action' => array("ADD COLUMN `$this->columnName` $this->columnType", "ADD INDEX ...")
);
```
* @api
*/
public function install()
{
if (empty($this->columnName) || empty($this->columnType)) {
return array();
}
return array(
$this->tableName => array("ADD COLUMN `$this->columnName` $this->columnType")
);
}
/**
* Updates the action dimension in case the {@link $columnType} has changed. The update is already implemented based
* on the {@link $columnName} and {@link $columnType}. This method is intended not to overwritten by plugin
* developers as it is only supposed to make sure the column has the correct type. Adding additional custom "alter
* table" actions would not really work since they would be executed with every {@link $columnType} change. So
* adding an index here would be executed whenever the columnType changes resulting in an error if the index already
* exists. If an index needs to be added after the first version is released a plugin update class should be
* created since this makes sure it is only executed once.
*
* @return array An array containing the table name as key and an array of MySQL alter table statements that should
* be executed on the given table. Example:
* ```
array(
'log_link_visit_action' => array("MODIFY COLUMN `$this->columnName` $this->columnType", "DROP COLUMN ...")
);
```
* @ignore
*/
public function update()
{
if (empty($this->columnName) || empty($this->columnType)) {
return array();
}
return array(
$this->tableName => array("MODIFY COLUMN `$this->columnName` $this->columnType")
);
}
/**
* Uninstalls the dimension if a {@link $columnName} and {@link columnType} is set. In case you perform any custom
* actions during {@link install()} - for instance adding an index - you should make sure to undo those actions by
* overwriting this method. Make sure to call this parent method to make sure the uninstallation of the column
* will be done.
* @throws Exception
* @api
*/
public function uninstall()
{
if (empty($this->columnName) || empty($this->columnType)) {
return;
}
try {
$sql = "ALTER TABLE `" . Common::prefixTable($this->tableName) . "` DROP COLUMN `$this->columnName`";
Db::exec($sql);
} catch (Exception $e) {
if (!Db::get()->isErrNo($e, '1091')) {
throw $e;
}
}
}
/**
* Get the version of the dimension which is used for update checks.
* @return string
* @ignore
*/
public function getVersion()
{
return $this->columnType;
}
/**
* If the value you want to save for your dimension is something like a page title or page url, you usually do not
* want to save the raw value over and over again to save bytes in the database. Instead you want to save each value
* once in the log_action table and refer to this value by its ID in the log_link_visit_action table. You can do
* this by returning an action id in "getActionId()" and by returning a value here. If a value should be ignored
* or not persisted just return boolean false. Please note if you return a value here and you implement the event
* "onNewAction" the value will be probably overwritten by the other event. So make sure to implement only one of
* those.
*
* @param Request $request
* @param Action $action
*
* @return false|mixed
* @api
*/
public function onLookupAction(Request $request, Action $action)
{
return false;
}
/**
* An action id. The value returned by the lookup action will be associated with this id in the log_action table.
* @return int
* @throws Exception in case not implemented
*/
public function getActionId()
{
throw new Exception('You need to overwrite the getActionId method in case you implement the onLookupAction method in class: ' . get_class($this));
}
/**
* This event is triggered before a new action is logged to the `log_link_visit_action` table. It overwrites any
* looked up action so it makes usually no sense to implement both methods but it sometimes does. You can assign
* any value to the column or return boolan false in case you do not want to save any value.
*
* @param Request $request
* @param Visitor $visitor
* @param Action $action
*
* @return mixed|false
* @api
*/
public function onNewAction(Request $request, Visitor $visitor, Action $action)
{
return false;
}
/**
* Adds a new segment. It automatically sets the SQL segment depending on the column name in case none is set
* already.
* @see \Piwik\Columns\Dimension::addSegment()
* @param Segment $segment
* @api
*/
protected function addSegment(Segment $segment)
{
$sqlSegment = $segment->getSqlSegment();
if (!empty($this->columnName) && empty($sqlSegment)) {
$segment->setSqlSegment($this->tableName . '.' . $this->columnName);
}
parent::addSegment($segment);
}
/**
* Get all action dimensions that are defined by all activated plugins.
* @return ActionDimension[]
* @ignore
*/
public static function getAllDimensions()
{
$cacheId = CacheId::pluginAware('ActionDimensions');
$cache = PiwikCache::getTransientCache();
if (!$cache->contains($cacheId)) {
$plugins = PluginManager::getInstance()->getPluginsLoadedAndActivated();
$instances = array();
foreach ($plugins as $plugin) {
foreach (self::getDimensions($plugin) as $instance) {
$instances[] = $instance;
}
}
$cache->save($cacheId, $instances);
}
return $cache->fetch($cacheId);
}
/**
* Get all action dimensions that are defined by the given plugin.
* @param Plugin $plugin
* @return ActionDimension[]
* @ignore
*/
public static function getDimensions(Plugin $plugin)
{
$dimensions = $plugin->findMultipleComponents('Columns', '\\Piwik\\Plugin\\Dimension\\ActionDimension');
$instances = array();
foreach ($dimensions as $dimension) {
$instances[] = new $dimension();
}
return $instances;
}
}

View file

@ -0,0 +1,247 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugin\Dimension;
use Piwik\CacheId;
use Piwik\Cache as PiwikCache;
use Piwik\Columns\Dimension;
use Piwik\Plugin\Manager as PluginManager;
use Piwik\Common;
use Piwik\Db;
use Piwik\Tracker\Action;
use Piwik\Tracker\GoalManager;
use Piwik\Tracker\Request;
use Piwik\Tracker\Visitor;
use Piwik\Plugin\Segment;
use Piwik\Plugin;
use Exception;
/**
* Defines a new conversion dimension that records any visit related information during tracking.
*
* You can record any visit information by implementing one of the following events:
* {@link onEcommerceOrderConversion()}, {@link onEcommerceCartUpdateConversion()} or {@link onGoalConversion()}.
* By defining a {@link $columnName} and {@link $columnType} a new column will be created in the database
* (table `log_conversion`) automatically and the values you return in the previous mentioned events will be saved in
* this column.
*
* You can create a new dimension using the console command `./console generate:dimension`.
*
* @api
* @since 2.5.0
*/
abstract class ConversionDimension extends Dimension
{
const INSTALLER_PREFIX = 'log_conversion.';
private $tableName = 'log_conversion';
/**
* Installs the conversion dimension in case it is not installed yet. The installation is already implemented based
* on the {@link $columnName} and {@link $columnType}. If you want to perform additional actions beside adding the
* column to the database - for instance adding an index - you can overwrite this method. We recommend to call
* this parent method to get the minimum required actions and then add further custom actions since this makes sure
* the column will be installed correctly. We also recommend to change the default install behavior only if really
* needed. FYI: We do not directly execute those alter table statements here as we group them together with several
* other alter table statements do execute those changes in one step which results in a faster installation. The
* column will be added to the `log_conversion` MySQL table.
*
* Example:
* ```
public function install()
{
$changes = parent::install();
$changes['log_conversion'][] = "ADD INDEX index_idsite_servertime ( idsite, server_time )";
return $changes;
}
```
*
* @return array An array containing the table name as key and an array of MySQL alter table statements that should
* be executed on the given table. Example:
* ```
array(
'log_conversion' => array("ADD COLUMN `$this->columnName` $this->columnType", "ADD INDEX ...")
);
```
* @api
*/
public function install()
{
if (empty($this->columnName) || empty($this->columnType)) {
return array();
}
return array(
$this->tableName => array("ADD COLUMN `$this->columnName` $this->columnType")
);
}
/**
* @see ActionDimension::update()
* @return array
* @ignore
*/
public function update()
{
if (empty($this->columnName) || empty($this->columnType)) {
return array();
}
return array(
$this->tableName => array("MODIFY COLUMN `$this->columnName` $this->columnType")
);
}
/**
* Uninstalls the dimension if a {@link $columnName} and {@link columnType} is set. In case you perform any custom
* actions during {@link install()} - for instance adding an index - you should make sure to undo those actions by
* overwriting this method. Make sure to call this parent method to make sure the uninstallation of the column
* will be done.
* @throws Exception
* @api
*/
public function uninstall()
{
if (empty($this->columnName) || empty($this->columnType)) {
return;
}
try {
$sql = "ALTER TABLE `" . Common::prefixTable($this->tableName) . "` DROP COLUMN `$this->columnName`";
Db::exec($sql);
} catch (Exception $e) {
if (!Db::get()->isErrNo($e, '1091')) {
throw $e;
}
}
}
/**
* @see ActionDimension::getVersion()
* @return string
* @ignore
*/
public function getVersion()
{
return $this->columnType;
}
/**
* Adds a new segment. It automatically sets the SQL segment depending on the column name in case none is set
* already.
*
* @see \Piwik\Columns\Dimension::addSegment()
* @param Segment $segment
* @api
*/
protected function addSegment(Segment $segment)
{
$sqlSegment = $segment->getSqlSegment();
if (!empty($this->columnName) && empty($sqlSegment)) {
$segment->setSqlSegment($this->tableName . '.' . $this->columnName);
}
parent::addSegment($segment);
}
/**
* Get all conversion dimensions that are defined by all activated plugins.
* @ignore
*/
public static function getAllDimensions()
{
$cacheId = CacheId::pluginAware('ConversionDimensions');
$cache = PiwikCache::getTransientCache();
if (!$cache->contains($cacheId)) {
$plugins = PluginManager::getInstance()->getPluginsLoadedAndActivated();
$instances = array();
foreach ($plugins as $plugin) {
foreach (self::getDimensions($plugin) as $instance) {
$instances[] = $instance;
}
}
$cache->save($cacheId, $instances);
}
return $cache->fetch($cacheId);
}
/**
* Get all conversion dimensions that are defined by the given plugin.
* @param Plugin $plugin
* @return ConversionDimension[]
* @ignore
*/
public static function getDimensions(Plugin $plugin)
{
$dimensions = $plugin->findMultipleComponents('Columns', '\\Piwik\\Plugin\\Dimension\\ConversionDimension');
$instances = array();
foreach ($dimensions as $dimension) {
$instances[] = new $dimension();
}
return $instances;
}
/**
* This event is triggered when an ecommerce order is converted. Any returned value will be persist in the database.
* Return boolean `false` if you do not want to change the value in some cases.
*
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @param GoalManager $goalManager
*
* @return mixed|false
* @api
*/
public function onEcommerceOrderConversion(Request $request, Visitor $visitor, $action, GoalManager $goalManager)
{
return false;
}
/**
* This event is triggered when an ecommerce cart update is converted. Any returned value will be persist in the
* database. Return boolean `false` if you do not want to change the value in some cases.
*
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @param GoalManager $goalManager
*
* @return mixed|false
* @api
*/
public function onEcommerceCartUpdateConversion(Request $request, Visitor $visitor, $action, GoalManager $goalManager)
{
return false;
}
/**
* This event is triggered when an any custom goal is converted. Any returned value will be persist in the
* database. Return boolean `false` if you do not want to change the value in some cases.
*
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @param GoalManager $goalManager
*
* @return mixed|false
* @api
*/
public function onGoalConversion(Request $request, Visitor $visitor, $action, GoalManager $goalManager)
{
return false;
}
}

View file

@ -0,0 +1,107 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugin\Dimension;
/**
* Provides metadata about dimensions for the LogDataPurger class.
*/
class DimensionMetadataProvider
{
/**
* Overrids for the result of the getActionReferenceColumnsByTable() method. Exists so Piwik
* instances can be monkey patched, in case there are idaction columns that this class does not
* naturally discover.
*
* @var array
*/
private $actionReferenceColumnsOverride;
public function __construct(array $actionReferenceColumnsOverride = array())
{
$this->actionReferenceColumnsOverride = $actionReferenceColumnsOverride;
}
/**
* Returns a list of idaction column names organized by table name. Uses dimension metadata
* to find idaction columns dynamically.
*
* Note: It is not currently possible to use the Piwik platform to add idaction columns to tables
* other than log_link_visit_action (w/o doing something unsupported), so idaction columns in
* other tables are hard coded.
*
* @return array[]
*/
public function getActionReferenceColumnsByTable()
{
$result = array(
'log_link_visit_action' => array('idaction_url',
'idaction_url_ref',
'idaction_name_ref'
),
'log_conversion' => array('idaction_url'),
'log_visit' => array('visit_exit_idaction_url',
'visit_exit_idaction_name',
'visit_entry_idaction_url',
'visit_entry_idaction_name'),
'log_conversion_item' => array('idaction_sku',
'idaction_name',
'idaction_category',
'idaction_category2',
'idaction_category3',
'idaction_category4',
'idaction_category5')
);
$dimensionIdActionColumns = $this->getVisitActionTableActionReferences();
$result['log_link_visit_action'] = array_unique(
array_merge($result['log_link_visit_action'], $dimensionIdActionColumns));
foreach ($this->actionReferenceColumnsOverride as $table => $columns) {
if (empty($result[$table])) {
$result[$table] = $columns;
} else {
$result[$table] = array_unique(array_merge($result[$table], $columns));
}
}
return $result;
}
private function getVisitActionTableActionReferences()
{
$idactionColumns = array();
foreach (ActionDimension::getAllDimensions() as $actionDimension) {
if ($this->isActionReference($actionDimension)) {
$idactionColumns[] = $actionDimension->getColumnName();
}
}
return $idactionColumns;
}
/**
* Returns `true` if the column for this dimension is a reference to the `log_action` table (ie, an "idaction column"),
* `false` if otherwise.
*
* @return bool
*/
private function isActionReference(ActionDimension $dimension)
{
try {
$dimension->getActionId();
return true;
} catch (\Exception $ex) {
return false;
}
}
}

View file

@ -0,0 +1,396 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugin\Dimension;
use Piwik\CacheId;
use Piwik\Cache as PiwikCache;
use Piwik\Columns\Dimension;
use Piwik\Common;
use Piwik\Db;
use Piwik\Plugin\Manager as PluginManager;
use Piwik\Plugin\Segment;
use Piwik\Tracker\Request;
use Piwik\Tracker\Visitor;
use Piwik\Tracker\Action;
use Piwik\Tracker;
use Piwik\Plugin;
use Exception;
/**
* Defines a new visit dimension that records any visit related information during tracking.
*
* You can record any visit information by implementing one of the following events: {@link onNewVisit()},
* {@link onExistingVisit()}, {@link onConvertedVisit()} or {@link onAnyGoalConversion()}. By defining a
* {@link $columnName} and {@link $columnType} a new column will be created in the database (table `log_visit`)
* automatically and the values you return in the previous mentioned events will be saved in this column.
*
* You can create a new dimension using the console command `./console generate:dimension`.
*
* @api
* @since 2.5.0
*/
abstract class VisitDimension extends Dimension
{
const INSTALLER_PREFIX = 'log_visit.';
private $tableName = 'log_visit';
/**
* Installs the visit dimension in case it is not installed yet. The installation is already implemented based on
* the {@link $columnName} and {@link $columnType}. If you want to perform additional actions beside adding the
* column to the database - for instance adding an index - you can overwrite this method. We recommend to call
* this parent method to get the minimum required actions and then add further custom actions since this makes sure
* the column will be installed correctly. We also recommend to change the default install behavior only if really
* needed. FYI: We do not directly execute those alter table statements here as we group them together with several
* other alter table statements do execute those changes in one step which results in a faster installation. The
* column will be added to the `log_visit` MySQL table.
*
* Example:
* ```
public function install()
{
$changes = parent::install();
$changes['log_visit'][] = "ADD INDEX index_idsite_servertime ( idsite, server_time )";
return $changes;
}
```
*
* @return array An array containing the table name as key and an array of MySQL alter table statements that should
* be executed on the given table. Example:
* ```
array(
'log_visit' => array("ADD COLUMN `$this->columnName` $this->columnType", "ADD INDEX ...")
);
```
* @api
*/
public function install()
{
if (empty($this->columnType) || empty($this->columnName)) {
return array();
}
$changes = array(
$this->tableName => array("ADD COLUMN `$this->columnName` $this->columnType")
);
if ($this->isHandlingLogConversion()) {
$changes['log_conversion'] = array("ADD COLUMN `$this->columnName` $this->columnType");
}
return $changes;
}
/**
* @see ActionDimension::update()
* @param array $conversionColumns An array of currently installed columns in the conversion table.
* @return array
* @ignore
*/
public function update($conversionColumns)
{
if (!$this->columnType) {
return array();
}
$changes = array();
$changes[$this->tableName] = array("MODIFY COLUMN `$this->columnName` $this->columnType");
$handlingConversion = $this->isHandlingLogConversion();
$hasConversionColumn = array_key_exists($this->columnName, $conversionColumns);
if ($hasConversionColumn && $handlingConversion) {
$changes['log_conversion'] = array("MODIFY COLUMN `$this->columnName` $this->columnType");
} elseif (!$hasConversionColumn && $handlingConversion) {
$changes['log_conversion'] = array("ADD COLUMN `$this->columnName` $this->columnType");
} elseif ($hasConversionColumn && !$handlingConversion) {
$changes['log_conversion'] = array("DROP COLUMN `$this->columnName`");
}
return $changes;
}
/**
* @see ActionDimension::getVersion()
* @return string
* @ignore
*/
public function getVersion()
{
return $this->columnType . $this->isHandlingLogConversion();
}
private function isHandlingLogConversion()
{
if (empty($this->columnName) || empty($this->columnType)) {
return false;
}
return $this->hasImplementedEvent('onAnyGoalConversion');
}
/**
* Uninstalls the dimension if a {@link $columnName} and {@link columnType} is set. In case you perform any custom
* actions during {@link install()} - for instance adding an index - you should make sure to undo those actions by
* overwriting this method. Make sure to call this parent method to make sure the uninstallation of the column
* will be done.
* @throws Exception
* @api
*/
public function uninstall()
{
if (empty($this->columnName) || empty($this->columnType)) {
return;
}
try {
$sql = "ALTER TABLE `" . Common::prefixTable($this->tableName) . "` DROP COLUMN `$this->columnName`";
Db::exec($sql);
} catch (Exception $e) {
if (!Db::get()->isErrNo($e, '1091')) {
throw $e;
}
}
try {
if (!$this->isHandlingLogConversion()) {
return;
}
$sql = "ALTER TABLE `" . Common::prefixTable('log_conversion') . "` DROP COLUMN `$this->columnName`";
Db::exec($sql);
} catch (Exception $e) {
if (!Db::get()->isErrNo($e, '1091')) {
throw $e;
}
}
}
/**
* Adds a new segment. It automatically sets the SQL segment depending on the column name in case none is set
* already.
* @see \Piwik\Columns\Dimension::addSegment()
* @param Segment $segment
* @api
*/
protected function addSegment(Segment $segment)
{
$sqlSegment = $segment->getSqlSegment();
if (!empty($this->columnName) && empty($sqlSegment)) {
$segment->setSqlSegment('log_visit.' . $this->columnName);
}
parent::addSegment($segment);
}
/**
* Sometimes you may want to make sure another dimension is executed before your dimension so you can persist
* this dimensions' value depending on the value of other dimensions. You can do this by defining an array of
* dimension names. If you access any value of any other column within your events, you should require them here.
* Otherwise those values may not be available.
* @return array
* @api
*/
public function getRequiredVisitFields()
{
return array();
}
/**
* The `onNewVisit` method is triggered when a new visitor is detected. This means you can define an initial
* value for this user here. By returning boolean `false` no value will be saved. Once the user makes another action
* the event "onExistingVisit" is executed. Meaning for each visitor this method is executed once.
*
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed|false
* @api
*/
public function onNewVisit(Request $request, Visitor $visitor, $action)
{
return false;
}
/**
* The `onExistingVisit` method is triggered when a visitor was recognized meaning it is not a new visitor.
* You can overwrite any previous value set by the event `onNewVisit` by implemting this event. By returning boolean
* `false` no value will be updated.
*
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed|false
* @api
*/
public function onExistingVisit(Request $request, Visitor $visitor, $action)
{
return false;
}
/**
* This event is executed shortly after `onNewVisit` or `onExistingVisit` in case the visitor converted a goal.
* Usually this event is not needed and you can simply remove this method therefore. An example would be for
* instance to persist the last converted action url. Return boolean `false` if you do not want to change the
* current value.
*
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed|false
* @api
*/
public function onConvertedVisit(Request $request, Visitor $visitor, $action)
{
return false;
}
/**
* By implementing this event you can persist a value to the `log_conversion` table in case a conversion happens.
* The persisted value will be logged along the conversion and will not be changed afterwards. This allows you to
* generate reports that shows for instance which url was called how often for a specific conversion. Once you
* implement this event and a $columnType is defined a column in the `log_conversion` MySQL table will be
* created automatically.
*
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed|false
* @api
*/
public function onAnyGoalConversion(Request $request, Visitor $visitor, $action)
{
return false;
}
/**
* This hook is executed by the tracker when determining if an action is the start of a new visit
* or part of an existing one. Derived classes can use it to force new visits based on dimension
* data.
*
* For example, the Campaign dimension in the Referrers plugin will force a new visit if the
* campaign information for the current action is different from the last.
*
* @param Request $request The current tracker request information.
* @param Visitor $visitor The information for the currently recognized visitor.
* @param Action|null $action The current action information (if any).
* @return bool Return true to force a visit, false if otherwise.
* @api
*/
public function shouldForceNewVisit(Request $request, Visitor $visitor, Action $action = null)
{
return false;
}
/**
* Get all visit dimensions that are defined by all activated plugins.
* @return VisitDimension[]
*/
public static function getAllDimensions()
{
$cacheId = CacheId::pluginAware('VisitDimensions');
$cache = PiwikCache::getTransientCache();
if (!$cache->contains($cacheId)) {
$plugins = PluginManager::getInstance()->getPluginsLoadedAndActivated();
$instances = array();
foreach ($plugins as $plugin) {
foreach (self::getDimensions($plugin) as $instance) {
$instances[] = $instance;
}
}
$instances = self::sortDimensions($instances);
$cache->save($cacheId, $instances);
}
return $cache->fetch($cacheId);
}
/**
* @ignore
* @param VisitDimension[] $dimensions
*/
public static function sortDimensions($dimensions)
{
$sorted = array();
$exists = array();
// we first handle all the once without dependency
foreach ($dimensions as $index => $dimension) {
$fields = $dimension->getRequiredVisitFields();
if (empty($fields)) {
$sorted[] = $dimension;
$exists[] = $dimension->getColumnName();
unset($dimensions[$index]);
}
}
// find circular references
// and remove dependencies whose column cannot be resolved because it is not installed / does not exist / is defined by core
$depenencies = array();
foreach ($dimensions as $dimension) {
$depenencies[$dimension->getColumnName()] = $dimension->getRequiredVisitFields();
}
foreach ($depenencies as $column => $fields) {
foreach ($fields as $key => $field) {
if (empty($depenencies[$field]) && !in_array($field, $exists)) {
// we cannot resolve that dependency as it does not exist
unset($depenencies[$column][$key]);
} elseif (!empty($depenencies[$field]) && in_array($column, $depenencies[$field])) {
throw new Exception("Circular reference detected for required field $field in dimension $column");
}
}
}
$count = 0;
while (count($dimensions) > 0) {
$count++;
if ($count > 1000) {
foreach ($dimensions as $dimension) {
$sorted[] = $dimension;
}
break; // to prevent an endless loop
}
foreach ($dimensions as $key => $dimension) {
$fields = $depenencies[$dimension->getColumnName()];
if (count(array_intersect($fields, $exists)) === count($fields)) {
$sorted[] = $dimension;
$exists[] = $dimension->getColumnName();
unset($dimensions[$key]);
}
}
}
return $sorted;
}
/**
* Get all visit dimensions that are defined by the given plugin.
* @param Plugin $plugin
* @return VisitDimension[]
* @ignore
*/
public static function getDimensions(Plugin $plugin)
{
$dimensions = $plugin->findMultipleComponents('Columns', '\\Piwik\\Plugin\\Dimension\\VisitDimension');
$instances = array();
foreach ($dimensions as $dimension) {
$instances[] = new $dimension();
}
return $instances;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,271 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugin;
use Piwik\Common;
use Piwik\Development;
use Piwik\Menu\MenuAdmin;
use Piwik\Menu\MenuReporting;
use Piwik\Menu\MenuTop;
use Piwik\Menu\MenuUser;
use Piwik\Plugin\Manager as PluginManager;
use Piwik\Plugins\UsersManager\UserPreferences;
/**
* Base class of all plugin menu providers. Plugins that define their own menu items can extend this class to easily
* add new items, to remove or to rename existing items.
*
* Descendants of this class can overwrite any of these methods. Each method will be executed only once per request
* and cached for any further menu requests.
*
* For an example, see the {@link https://github.com/piwik/piwik/blob/master/plugins/ExampleUI/Menu.php} plugin.
*
* @api
* @since 2.4.0
*/
class Menu
{
public function __construct()
{
// Constructor kept for BC (because called in implementations)
}
private function getModule()
{
$className = get_class($this);
$className = explode('\\', $className);
return $className[2];
}
/**
* Generates a URL for the default action of the plugin controller.
*
* Example:
* ```
* $menu->addItem('UI Framework', '', $this->urlForDefaultAction(), $orderId = 30);
* // will add a menu item that leads to the default action of the plugin controller when a user clicks on it.
* // The default action is usually the `index` action - meaning the `index()` method the controller -
* // but the default action can be customized within a controller
* ```
*
* @param array $additionalParams Optional URL parameters that will be appended to the URL
* @return array
*
* @since 2.7.0
* @api
*/
protected function urlForDefaultAction($additionalParams = array())
{
$params = (array) $additionalParams;
$params['action'] = '';
$params['module'] = $this->getModule();
return $params;
}
/**
* Generates a URL for the given action. In your plugin controller you have to create a method with the same name
* as this method will be executed when a user clicks on the menu item. If you want to generate a URL for the
* action of another module, meaning not your plugin, you should use the method {@link urlForModuleAction()}.
*
* @param string $controllerAction The name of the action that should be executed within your controller
* @param array $additionalParams Optional URL parameters that will be appended to the URL
* @return array
*
* @since 2.7.0
* @api
*/
protected function urlForAction($controllerAction, $additionalParams = array())
{
$module = $this->getModule();
$this->checkisValidCallable($module, $controllerAction);
$params = (array) $additionalParams;
$params['action'] = $controllerAction;
$params['module'] = $module;
return $params;
}
/**
* Generates a URL for the given action of the given module. We usually do not recommend to use this method as you
* should make sure the method of that module actually exists. If the plugin owner of that module changes the method
* in a future version your link might no longer work. If you want to link to an action of your controller use the
* method {@link urlForAction()}. Note: We will generate a link only if the given module is installed and activated.
*
* @param string $module The name of the module/plugin the action belongs to. The module name is case sensitive.
* @param string $controllerAction The name of the action that should be executed within your controller
* @param array $additionalParams Optional URL parameters that will be appended to the URL
* @return array|null Returns null if the given module is either not installed or not activated. Returns the array
* of query parameter names and values to the given module action otherwise.
*
* @since 2.7.0
* // not API for now
*/
protected function urlForModuleAction($module, $controllerAction, $additionalParams = array())
{
$this->checkisValidCallable($module, $controllerAction);
$pluginManager = PluginManager::getInstance();
if (!$pluginManager->isPluginLoaded($module) ||
!$pluginManager->isPluginActivated($module)) {
return null;
}
$params = (array) $additionalParams;
$params['action'] = $controllerAction;
$params['module'] = $module;
return $params;
}
/**
* Generates a URL to the given action of the current module, and it will also append some URL query parameters from the
* User preferences: idSite, period, date. If you do not need the parameters idSite, period and date to be generated
* use {@link urlForAction()} instead.
*
* @param string $controllerAction The name of the action that should be executed within your controller
* @param array $additionalParams Optional URL parameters that will be appended to the URL
* @return array Returns the array of query parameter names and values to the given module action and idSite date and period.
*
*/
protected function urlForActionWithDefaultUserParams($controllerAction, $additionalParams = array())
{
$module = $this->getModule();
return $this->urlForModuleActionWithDefaultUserParams($module, $controllerAction, $additionalParams);
}
/**
* Generates a URL to the given action of the given module, and it will also append some URL query parameters from the
* User preferences: idSite, period, date. If you do not need the parameters idSite, period and date to be generated
* use {@link urlForModuleAction()} instead.
*
* @param string $module The name of the module/plugin the action belongs to. The module name is case sensitive.
* @param string $controllerAction The name of the action that should be executed within your controller
* @param array $additionalParams Optional URL parameters that will be appended to the URL
* @return array|null Returns the array of query parameter names and values to the given module action and idSite date and period.
* Returns null if the module or action is invalid.
*
*/
protected function urlForModuleActionWithDefaultUserParams($module, $controllerAction, $additionalParams = array())
{
$urlModuleAction = $this->urlForModuleAction($module, $controllerAction);
$date = Common::getRequestVar('date', false);
if ($date) {
$urlModuleAction['date'] = $date;
}
$period = Common::getRequestVar('period', false);
if ($period) {
$urlModuleAction['period'] = $period;
}
// We want the current query parameters to override the user's defaults
return array_merge(
$this->urlForDefaultUserParams(),
$urlModuleAction,
$additionalParams
);
}
/**
* Returns the &idSite=X&period=Y&date=Z query string fragment,
* fetched from current logged-in user's preferences.
*
* @param bool $websiteId
* @param bool $defaultPeriod
* @param bool $defaultDate
* @return string eg '&idSite=1&period=week&date=today'
* @throws \Exception in case a website was not specified and a default website id could not be found
*/
public function urlForDefaultUserParams($websiteId = false, $defaultPeriod = false, $defaultDate = false)
{
$userPreferences = new UserPreferences();
if (empty($websiteId)) {
$websiteId = $userPreferences->getDefaultWebsiteId();
}
if (empty($websiteId)) {
throw new \Exception("A website ID was not specified and a website to default to could not be found.");
}
if (empty($defaultDate)) {
$defaultDate = $userPreferences->getDefaultDate();
}
if (empty($defaultPeriod)) {
$defaultPeriod = $userPreferences->getDefaultPeriod(false);
}
return array(
'idSite' => $websiteId,
'period' => $defaultPeriod,
'date' => $defaultDate,
);
}
/**
* Configures the reporting menu which should only contain links to reports of a specific site such as
* "Search Engines", "Page Titles" or "Locations & Provider".
*/
public function configureReportingMenu(MenuReporting $menu)
{
}
/**
* Configures the top menu which is supposed to contain analytics related items such as the
* "All Websites Dashboard".
*/
public function configureTopMenu(MenuTop $menu)
{
}
/**
* Configures the user menu which is supposed to contain user and help related items such as
* "User settings", "Alerts" or "Email Reports".
*/
public function configureUserMenu(MenuUser $menu)
{
}
/**
* Configures the admin menu which is supposed to contain only administration related items such as
* "Websites", "Users" or "Plugin settings".
*/
public function configureAdminMenu(MenuAdmin $menu)
{
}
private function checkisValidCallable($module, $action)
{
if (!Development::isEnabled()) {
return;
}
$prefix = 'Menu item added in ' . get_class($this) . ' will fail when being selected. ';
if (!is_string($action)) {
Development::error($prefix . 'No valid action is specified. Make sure the defined action that should be executed is a string.');
}
$reportAction = lcfirst(substr($action, 4));
if (Report::factory($module, $reportAction)) {
return;
}
$controllerClass = '\\Piwik\\Plugins\\' . $module . '\\Controller';
if (!Development::methodExists($controllerClass, $action)) {
Development::error($prefix . 'The defined action "' . $action . '" does not exist in ' . $controllerClass . '". Make sure to define such a method.');
}
if (!Development::isCallableMethod($controllerClass, $action)) {
Development::error($prefix . 'The defined action "' . $action . '" is not callable on "' . $controllerClass . '". Make sure the method is public.');
}
}
}

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,7 +9,6 @@
namespace Piwik\Plugin;
use Exception;
use Piwik\Common;
use Piwik\Piwik;
use Piwik\Version;
@ -50,9 +49,17 @@ class MetadataLoader
*/
public function load()
{
$defaults = $this->getDefaultPluginInformation();
$plugin = $this->loadPluginInfoJson();
// use translated plugin description if available
if ($defaults['description'] != Piwik::translate($defaults['description'])) {
unset($plugin['description']);
}
return array_merge(
$this->getDefaultPluginInformation(),
$this->loadPluginInfoJson()
$defaults,
$plugin
);
}
@ -67,7 +74,7 @@ class MetadataLoader
{
$descriptionKey = $this->pluginName . '_PluginDescription';
return array(
'description' => Piwik::translate($descriptionKey),
'description' => $descriptionKey,
'homepage' => 'http://piwik.org/',
'authors' => array(array('name' => 'Piwik', 'homepage' => 'http://piwik.org/')),
'license' => 'GPL v3+',
@ -95,12 +102,13 @@ class MetadataLoader
return array();
}
$info = Common::json_decode($json, $assoc = true);
$info = json_decode($json, $assoc = true);
if (!is_array($info)
|| empty($info)
) {
throw new Exception("Invalid JSON file: $path");
}
return $info;
}
}

View file

@ -0,0 +1,189 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugin;
use Piwik\DataTable;
use Piwik\DataTable\Row;
use Piwik\Metrics;
use Piwik\Metrics\Formatter;
/**
* Base type of metric metadata classes.
*
* A metric metadata class is a class that describes how a metric is described, computed and
* formatted.
*
* There are two types of metrics: aggregated and processed. An aggregated metric is computed
* in the backend datastore and aggregated in PHP when archiving period reports.
*
* Currently, only processed metrics can be defined as metric metadata classes. Support for
* aggregated metrics will be added at a later date.
*
* See {@link Piwik\Plugin\ProcessedMetric} and {@link Piwik\Plugin|AggregatedMetric}.
*
* @api
*/
abstract class Metric
{
/**
* The sub-namespace name in a plugin where Metric components are stored.
*/
const COMPONENT_SUBNAMESPACE = 'Metrics';
/**
* Returns the column name of this metric, eg, `"nb_visits"` or `"avg_time_on_site"`.
*
* This string is what appears in API output.
*
* @return string
*/
abstract public function getName();
/**
* Returns the human readable translated name of this metric, eg, `"Visits"` or `"Avg. time on site"`.
*
* This string is what appears in the UI.
*
* @return string
*/
abstract public function getTranslatedName();
/**
* Returns a string describing what the metric represents. The result will be included in report metadata
* API output, including processed reports.
*
* Implementing this method is optional.
*
* @return string
*/
public function getDocumentation()
{
return "";
}
/**
* Returns a formatted metric value. This value is what appears in API output. From within Piwik,
* (core & plugins) the computed value is used. Only when outputting to the API does a metric
* get formatted.
*
* By default, just returns the value.
*
* @param mixed $value The metric value.
* @param Formatter $formatter The formatter to use when formatting a value.
* @return mixed $value
*/
public function format($value, Formatter $formatter)
{
return $value;
}
/**
* Executed before formatting all metrics for a report. Implementers can return `false`
* to skip formatting this metric and can use this method to access information needed for
* formatting (for example, the site ID).
*
* @param Report $report
* @param DataTable $table
* @return bool Return `true` to format the metric for the table, `false` to skip formatting.
*/
public function beforeFormat($report, DataTable $table)
{
return true;
}
/**
* Helper method that will access a metric in a {@link Piwik\DataTable\Row} or array either by
* its name or by its special numerical index value.
*
* @param Row|array $row
* @param string $columnName
* @param int[]|null $mappingNameToId A custom mapping of metric names to special index values. By
* default {@link Metrics::getMappingFromNameToId()} is used.
* @return mixed The metric value or false if none exists.
*/
public static function getMetric($row, $columnName, $mappingNameToId = null)
{
if ($row instanceof Row) {
$value = $row->getColumn($columnName);
if ($value === false) {
if (empty($mappingNameToId)) {
$mappingNameToId = Metrics::getMappingFromNameToId();
}
if (isset($mappingNameToId[$columnName])) {
return $row->getColumn($mappingNameToId[$columnName]);
}
}
return $value;
} elseif (!empty($row)) {
if (array_key_exists($columnName, $row)) {
return $row[$columnName];
} else {
if (empty($mappingNameToId)) {
$mappingNameToId = Metrics::getMappingFromNameToId();
}
if (isset($mappingNameToId[$columnName])) {
$columnName = $mappingNameToId[$columnName];
if (array_key_exists($columnName, $row)) {
return $row[$columnName];
}
}
}
}
return null;
}
/**
* Helper method that will determine the actual column name for a metric in a
* {@link Piwik\DataTable} and return every column value for this name.
*
* @param DataTable $table
* @param string $columnName
* @param int[]|null $mappingNameToId A custom mapping of metric names to special index values. By
* default {@link Metrics::getMappingFromNameToId()} is used.
* @return array
*/
public static function getMetricValues(DataTable $table, $columnName, $mappingNameToId = null)
{
if (empty($mappingIdToName)) {
$mappingNameToId = Metrics::getMappingFromNameToId();
}
$columnName = self::getActualMetricColumn($table, $columnName, $mappingNameToId);
return $table->getColumn($columnName);
}
/**
* Helper method that determines the actual column for a metric in a {@link Piwik\DataTable}.
*
* @param DataTable $table
* @param string $columnName
* @param int[]|null $mappingNameToId A custom mapping of metric names to special index values. By
* default {@link Metrics::getMappingFromNameToId()} is used.
* @return string
*/
public static function getActualMetricColumn(DataTable $table, $columnName, $mappingNameToId = null)
{
if (empty($mappingIdToName)) {
$mappingNameToId = Metrics::getMappingFromNameToId();
}
$firstRow = $table->getFirstRow();
if (!empty($firstRow)
&& $firstRow->getColumn($columnName) === false
) {
$columnName = $mappingNameToId[$columnName];
}
return $columnName;
}
}

View file

@ -0,0 +1,35 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugin;
use Piwik\Common;
class PluginException extends \Exception
{
public function __construct($pluginName, $message)
{
$pluginName = Common::sanitizeInputValue($pluginName);
$message = Common::sanitizeInputValue($message);
parent::__construct("There was a problem installing the plugin $pluginName: <br /><br />
$message
<br /><br />
If you want to hide this message you must remove the following line under the [Plugins] entry in your
'config/config.ini.php' file to disable this plugin.<br />
Plugins[] = $pluginName
<br /><br />If this plugin has already been installed, you must add the following line under the
[PluginsInstalled] entry in your 'config/config.ini.php' file:<br />
PluginsInstalled[] = $pluginName");
}
public function isHtmlMessage()
{
return true;
}
}

View file

@ -0,0 +1,70 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugin;
use Piwik\DataTable;
use Piwik\DataTable\Row;
/**
* Base type for processed metrics. A processed metric is a metric that is computed using
* one or more other metrics.
*
* @api
*/
abstract class ProcessedMetric extends Metric
{
/**
* The sub-namespace name in a plugin where ProcessedMetrics are stored.
*/
const COMPONENT_SUBNAMESPACE = 'Columns\\Metrics';
/**
* Computes the metric using the values in a {@link Piwik\DataTable\Row}.
*
* The computed value should be numerical and not formatted in any way. For example, for
* a percent value, `0.14` should be returned instead of `"14%"`.
*
* @return mixed
*/
abstract public function compute(Row $row);
/**
* Returns the array of metrics that are necessary for computing this metric. The elements
* of the array are metric names.
*
* @return string[]
*/
abstract public function getDependentMetrics();
/**
* Returns the array of metrics that are necessary for computing this metric, but should not
* be displayed to the user unless explicitly requested. These metrics are intermediate
* metrics that are not really valuable to the user. On a request, if showColumns or hideColumns
* is not used, they will be removed automatically.
*
* @return string[]
*/
public function getTemporaryMetrics()
{
return array();
}
/**
* Executed before computing all processed metrics for a report. Implementers can return `false`
* to skip computing this metric.
*
* @param Report $report
* @param DataTable $table
* @return bool Return `true` to compute the metric for the table, `false` to skip computing
* this metric.
*/
public function beforeCompute($report, DataTable $table)
{
return true;
}
}

View file

@ -0,0 +1,103 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugin;
use Piwik\Config;
use Piwik\Container\StaticContainer;
use Piwik\UpdateCheck\ReleaseChannel;
/**
* Get release channels that are defined by plugins.
*/
class ReleaseChannels
{
/**
* @var Manager
*/
private $pluginManager;
public function __construct(Manager $pluginManager)
{
$this->pluginManager = $pluginManager;
}
/**
* @return ReleaseChannel[]
*/
public function getAllReleaseChannels()
{
$classNames = $this->pluginManager->findMultipleComponents('ReleaseChannel', 'Piwik\\UpdateCheck\\ReleaseChannel');
$channels = array();
foreach ($classNames as $className) {
$channels[] = StaticContainer::get($className);
}
usort($channels, function (ReleaseChannel $a, ReleaseChannel $b) {
if ($a->getOrder() === $b->getOrder()) {
return 0;
}
return ($a->getOrder() < $b->getOrder()) ? -1 : 1;
});
return $channels;
}
/**
* @return ReleaseChannel
*/
public function getActiveReleaseChannel()
{
$channel = Config::getInstance()->General['release_channel'];
$channel = $this->factory($channel);
if (!empty($channel)) {
return $channel;
}
$channels = $this->getAllReleaseChannels();
// we default to the one with lowest id
return reset($channels);
}
/**
* Sets the given release channel in config but does not save id. $config->forceSave() still needs to be called
* @param string $channel
*/
public function setActiveReleaseChannelId($channel)
{
$general = Config::getInstance()->General;
$general['release_channel'] = $channel;
Config::getInstance()->General = $general;
}
public function isValidReleaseChannelId($releaseChannelId)
{
$channel = $this->factory($releaseChannelId);
return !empty($channel);
}
/**
* @param string $releaseChannelId
* @return ReleaseChannel
*/
private function factory($releaseChannelId)
{
$releaseChannelId = strtolower($releaseChannelId);
foreach ($this->getAllReleaseChannels() as $releaseChannel) {
if ($releaseChannelId === strtolower($releaseChannel->getId())) {
return $releaseChannel;
}
}
}
}

File diff suppressed because it is too large Load diff

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