update Piwik to version 2.16 (fixes #91)

This commit is contained in:
oliver 2016-04-10 18:55:57 +02:00
commit d885a4baa9
5833 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

View file

@ -0,0 +1,27 @@
<?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\Container\StaticContainer;
class RequestProcessors
{
public function getRequestProcessors()
{
$manager = Manager::getInstance();
$processors = $manager->findMultipleComponents('Tracker', 'Piwik\\Tracker\\RequestProcessor');
$instances = array();
foreach ($processors as $processor) {
$instances[] = StaticContainer::get($processor);
}
return $instances;
}
}

View file

@ -0,0 +1,341 @@
<?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 Exception;
/**
* Creates a new segment that can be used for instance within the {@link \Piwik\Columns\Dimension::configureSegment()}
* method. Make sure to set at least the following values: {@link setName()}, {@link setSegment()},
* {@link setSqlSegment()}, {@link setType()} and {@link setCategory()}. If you are using a segment in the context of a
* dimension the type and the SQL segment is usually set for you automatically.
*
* Example:
* ```
$segment = new \Piwik\Plugin\Segment();
$segment->setType(\Piwik\Plugin\Segment::TYPE_DIMENSION);
$segment->setName('General_EntryKeyword');
$segment->setCategory('General_Visit');
$segment->setSegment('entryKeyword');
$segment->setSqlSegment('log_visit.entry_keyword');
$segment->setAcceptedValues('Any keywords people search for on your website such as "help" or "imprint"');
```
* @api
* @since 2.5.0
*/
class Segment
{
/**
* Segment type 'dimension'. Can be used along with {@link setType()}.
* @api
*/
const TYPE_DIMENSION = 'dimension';
/**
* Segment type 'metric'. Can be used along with {@link setType()}.
* @api
*/
const TYPE_METRIC = 'metric';
private $type;
private $category;
private $name;
private $segment;
private $sqlSegment;
private $sqlFilter;
private $sqlFilterValue;
private $acceptValues;
private $permission;
private $suggestedValuesCallback;
private $unionOfSegments;
/**
* If true, this segment will only be visible to the user if the user has view access
* to one of the requested sites (see API.getSegmentsMetadata).
*
* @var bool
*/
private $requiresAtLeastViewAccess = false;
/**
* @ignore
*/
final public function __construct()
{
$this->init();
}
/**
* Here you can initialize this segment and set any default values. It is called directly after the object is
* created.
* @api
*/
protected function init()
{
}
/**
* Here you should explain which values are accepted/useful for your segment, for example:
* "1, 2, 3, etc." or "comcast.net, proxad.net, etc.". If the value needs any special encoding you should mention
* this as well. For example "Any URL including protocol. The URL must be URL encoded."
*
* @param string $acceptedValues
* @api
*/
public function setAcceptedValues($acceptedValues)
{
$this->acceptValues = $acceptedValues;
}
/**
* Set (overwrite) the category this segment belongs to. It should be a translation key such as 'General_Actions'
* or 'General_Visit'.
* @param string $category
* @api
*/
public function setCategory($category)
{
$this->category = $category;
}
/**
* Set (overwrite) the segment display name. This name will be visible in the API and the UI. It should be a
* translation key such as 'Actions_ColumnEntryPageTitle' or 'Resolution_ColumnResolution'.
* @param string $name
* @api
*/
public function setName($name)
{
$this->name = $name;
}
/**
* Set (overwrite) the name of the segment. The name should be lower case first and has to be unique. The segment
* name defined here needs to be set in the URL to actually apply this segment. Eg if the segment is 'searches'
* you need to set "&segment=searches>0" in the UI.
* @param string $segment
* @api
*/
public function setSegment($segment)
{
$this->segment = $segment;
$this->check();
}
/**
* Sometimes you want users to set values that differ from the way they are actually stored. For instance if you
* want to allow to filter by any URL than you might have to resolve this URL to an action id. Or a country name
* maybe has to be mapped to a 2 letter country code. You can do this by specifing either a callable such as
* `array('Classname', 'methodName')` or by passing a closure. There will be four values passed to the given closure
* or callable: `string $valueToMatch`, `string $segment` (see {@link setSegment()}), `string $matchType`
* (eg SegmentExpression::MATCH_EQUAL or any other match constant of this class) and `$segmentName`.
*
* If the closure returns NULL, then Piwik assumes the segment sub-string will not match any visitor.
*
* @param string|\Closure $sqlFilter
* @api
*/
public function setSqlFilter($sqlFilter)
{
$this->sqlFilter = $sqlFilter;
}
/**
* Similar to {@link setSqlFilter()} you can map a given segment value to another value. For instance you could map
* "new" to 0, 'returning' to 1 and any other value to '2'. You can either define a callable or a closure. There
* will be only one value passed to the closure or callable which contains the value a user has set for this
* segment. This callback is called shortly before {@link setSqlFilter()}.
* @param string|array $sqlFilterValue
* @api
*/
public function setSqlFilterValue($sqlFilterValue)
{
$this->sqlFilterValue = $sqlFilterValue;
}
/**
* Defines to which column in the MySQL database the segment belongs: 'mytablename.mycolumnname'. Eg
* 'log_visit.idsite'. When a segment is applied the given or filtered value will be compared with this column.
*
* @param string $sqlSegment
* @api
*/
public function setSqlSegment($sqlSegment)
{
$this->sqlSegment = $sqlSegment;
$this->check();
}
/**
* Set a list of segments that should be used instead of fetching the values from a single column.
* All set segments will be applied via an OR operator.
*
* @param array $segments
* @api
*/
public function setUnionOfSegments($segments)
{
$this->unionOfSegments = $segments;
$this->check();
}
/**
* @return array
* @ignore
*/
public function getUnionOfSegments()
{
return $this->unionOfSegments;
}
/**
* @return string
* @ignore
*/
public function getSqlSegment()
{
return $this->sqlSegment;
}
/**
* Set (overwrite) the type of this segment which is usually either a 'dimension' or a 'metric'.
* @param string $type See constansts TYPE_*
* @api
*/
public function setType($type)
{
$this->type = $type;
}
/**
* @return string
* @ignore
*/
public function getType()
{
return $this->type;
}
/**
* @return string
* @ignore
*/
public function getName()
{
return $this->name;
}
/**
* Returns the name of this segment as it should appear in segment expressions.
*
* @return string
*/
public function getSegment()
{
return $this->segment;
}
/**
* Set callback which will be executed when user will call for suggested values for segment.
*
* @param callable $suggestedValuesCallback
*/
public function setSuggestedValuesCallback($suggestedValuesCallback)
{
$this->suggestedValuesCallback = $suggestedValuesCallback;
}
/**
* You can restrict the access to this segment by passing a boolean `false`. For instance if you want to make
* a certain segment only available to users having super user access you could do the following:
* `$segment->setPermission(Piwik::hasUserSuperUserAccess());`
* @param bool $permission
* @api
*/
public function setPermission($permission)
{
$this->permission = $permission;
}
/**
* @return array
* @ignore
*/
public function toArray()
{
$segment = array(
'type' => $this->type,
'category' => $this->category,
'name' => $this->name,
'segment' => $this->segment,
'sqlSegment' => $this->sqlSegment,
);
if (!empty($this->unionOfSegments)) {
$segment['unionOfSegments'] = $this->unionOfSegments;
}
if (!empty($this->sqlFilter)) {
$segment['sqlFilter'] = $this->sqlFilter;
}
if (!empty($this->sqlFilterValue)) {
$segment['sqlFilterValue'] = $this->sqlFilterValue;
}
if (!empty($this->acceptValues)) {
$segment['acceptedValues'] = $this->acceptValues;
}
if (isset($this->permission)) {
$segment['permission'] = $this->permission;
}
if (is_callable($this->suggestedValuesCallback)) {
$segment['suggestedValuesCallback'] = $this->suggestedValuesCallback;
}
return $segment;
}
/**
* Returns true if this segment should only be visible to the user if the user has view access
* to one of the requested sites (see API.getSegmentsMetadata), false if it should always be
* visible to the user (even the anonymous user).
*
* @return boolean
* @ignore
*/
public function isRequiresAtLeastViewAccess()
{
return $this->requiresAtLeastViewAccess;
}
/**
* Sets whether the segment should only be visible if the user requesting it has view access
* to one of the requested sites and if the user is not the anonymous user.
*
* @param boolean $requiresAtLeastViewAccess
* @ignore
*/
public function setRequiresAtLeastViewAccess($requiresAtLeastViewAccess)
{
$this->requiresAtLeastViewAccess = $requiresAtLeastViewAccess;
}
private function check()
{
if ($this->sqlSegment && $this->unionOfSegments) {
throw new Exception(sprintf('Union of segments and SQL segment is set for segment "%s", use only one of them', $this->name));
}
if ($this->segment && $this->unionOfSegments && in_array($this->segment, $this->unionOfSegments, true)) {
throw new Exception(sprintf('The segment %s contains a union segment to itself', $this->name));
}
}
}

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,24 +8,24 @@
*/
namespace Piwik\Plugin;
use Piwik\Option;
use Piwik\Piwik;
use Piwik\Settings\Setting;
use Piwik\Settings\Storage;
use Piwik\Settings\StorageInterface;
use Piwik\SettingsServer;
use Piwik\Tracker\SettingsStorage;
/**
* Base class of all plugin settings providers. Plugins that define their own configuration settings
* can extend this class to easily make their settings available to Piwik users.
*
*
* Descendants of this class should implement the {@link init()} method and call the
* {@link addSetting()} method for each of the plugin's settings.
*
*
* For an example, see the {@link Piwik\Plugins\ExampleSettingsPlugin\ExampleSettingsPlugin} plugin.
*
*
* @api
*/
abstract class Settings implements StorageInterface
abstract class Settings
{
const TYPE_INT = 'integer';
const TYPE_FLOAT = 'float';
@ -48,27 +48,52 @@ abstract class Settings implements StorageInterface
*/
private $settings = array();
/**
* Array containing all plugin settings values: Array( [setting-key] => [setting-value] ).
*
* @var array
*/
private $settingsValues = array();
private $introduction;
private $pluginName;
protected $pluginName;
/**
* @var StorageInterface
*/
protected $storage;
/**
* Constructor.
*
* @param string $pluginName The name of the plugin these settings are for.
*/
public function __construct($pluginName)
public function __construct($pluginName = null)
{
$this->pluginName = $pluginName;
if (!empty($pluginName)) {
$this->pluginName = $pluginName;
} else {
$classname = get_class($this);
$parts = explode('\\', $classname);
if (3 <= count($parts)) {
$this->pluginName = $parts[2];
}
}
$this->storage = Storage\Factory::make($this->pluginName);
$this->init();
$this->loadSettings();
}
/**
* @ignore
*/
public function getPluginName()
{
return $this->pluginName;
}
/**
* @ignore
* @return Setting
*/
public function getSetting($name)
{
if (array_key_exists($name, $this->settings)) {
return $this->settings[$name];
}
}
/**
@ -90,7 +115,7 @@ abstract class Settings implements StorageInterface
/**
* Returns the introduction text for this plugin's settings.
*
*
* @return string
*/
public function getIntroduction()
@ -106,14 +131,17 @@ abstract class Settings implements StorageInterface
public function getSettingsForCurrentUser()
{
$settings = array_filter($this->getSettings(), function (Setting $setting) {
return $setting->canBeDisplayedForCurrentUser();
return $setting->isWritableByCurrentUser();
});
uasort($settings, function ($setting1, $setting2) use ($settings) {
$settings2 = $settings;
uasort($settings, function ($setting1, $setting2) use ($settings2) {
/** @var Setting $setting1 */ /** @var Setting $setting2 */
if ($setting1->getOrder() == $setting2->getOrder()) {
// preserve order for settings having same order
foreach ($settings as $setting) {
foreach ($settings2 as $setting) {
if ($setting1 === $setting) {
return -1;
}
@ -142,12 +170,57 @@ abstract class Settings implements StorageInterface
return $this->settings;
}
/**
* Makes a new plugin setting available.
*
* @param Setting $setting
* @throws \Exception If there is a setting with the same name that already exists.
* If the name contains non-alphanumeric characters.
*/
protected function addSetting(Setting $setting)
{
$name = $setting->getName();
if (!ctype_alnum(str_replace('_', '', $name))) {
$msg = sprintf('The setting name "%s" in plugin "%s" is not valid. Only underscores, alpha and numerical characters are allowed', $setting->getName(), $this->pluginName);
throw new \Exception($msg);
}
if (array_key_exists($name, $this->settings)) {
throw new \Exception(sprintf('A setting with name "%s" does already exist for plugin "%s"', $setting->getName(), $this->pluginName));
}
$this->setDefaultTypeAndFieldIfNeeded($setting);
$this->addValidatorIfNeeded($setting);
$setting->setStorage($this->storage);
$setting->setPluginName($this->pluginName);
$this->settings[$name] = $setting;
}
/**
* Saves (persists) the current setting values in the database.
*/
public function save()
{
Option::set($this->getOptionKey(), serialize($this->settingsValues));
$this->storage->save();
SettingsStorage::clearCache();
/**
* Triggered after a plugin settings have been updated.
*
* **Example**
*
* Piwik::addAction('Settings.MyPlugin.settingsUpdated', function (Settings $settings) {
* $value = $settings->someSetting->getValue();
* // Do something with the new setting value
* });
*
* @param Settings $settings The plugin settings object.
*/
Piwik::postEvent(sprintf('Settings.%s.settingsUpdated', $this->pluginName), array($this));
}
/**
@ -158,138 +231,9 @@ abstract class Settings implements StorageInterface
{
Piwik::checkUserHasSuperUserAccess();
Option::delete($this->getOptionKey());
$this->settingsValues = array();
}
$this->storage->deleteAllValues();
/**
* Returns the current value for a setting. If no value is stored, the default value
* is be returned.
*
* @param Setting $setting
* @return mixed
* @throws \Exception If the setting does not exist or if the current user is not allowed to change the value
* of this setting.
*/
public function getSettingValue(Setting $setting)
{
$this->checkIsValidSetting($setting->getName());
if (array_key_exists($setting->getKey(), $this->settingsValues)) {
return $this->settingsValues[$setting->getKey()];
}
return $setting->defaultValue;
}
/**
* Sets (overwrites) the value of a setting in memory. To persist the change, {@link save()} must be
* called afterwards, otherwise the change has no effect.
*
* Before the setting is changed, the {@link Piwik\Settings\Setting::$validate} and
* {@link Piwik\Settings\Setting::$transform} closures will be invoked (if defined). If there is no validation
* filter, the setting value will be casted to the appropriate data type.
*
* @param Setting $setting
* @param string $value
* @throws \Exception If the setting does not exist or if the current user is not allowed to change the value
* of this setting.
*/
public function setSettingValue(Setting $setting, $value)
{
$this->checkIsValidSetting($setting->getName());
if ($setting->validate && $setting->validate instanceof \Closure) {
call_user_func($setting->validate, $value, $setting);
}
if ($setting->transform && $setting->transform instanceof \Closure) {
$value = call_user_func($setting->transform, $value, $setting);
} elseif (isset($setting->type)) {
settype($value, $setting->type);
}
$this->settingsValues[$setting->getKey()] = $value;
}
/**
* Unsets a setting value in memory. To persist the change, {@link save()} must be
* called afterwards, otherwise the change has no effect.
*
* @param Setting $setting
*/
public function removeSettingValue(Setting $setting)
{
$this->checkHasEnoughPermission($setting);
$key = $setting->getKey();
if (array_key_exists($key, $this->settingsValues)) {
unset($this->settingsValues[$key]);
}
}
/**
* Makes a new plugin setting available.
*
* @param Setting $setting
* @throws \Exception If there is a setting with the same name that already exists.
* If the name contains non-alphanumeric characters.
*/
protected function addSetting(Setting $setting)
{
if (!ctype_alnum($setting->getName())) {
$msg = sprintf('The setting name "%s" in plugin "%s" is not valid. Only alpha and numerical characters are allowed', $setting->getName(), $this->pluginName);
throw new \Exception($msg);
}
if (array_key_exists($setting->getName(), $this->settings)) {
throw new \Exception(sprintf('A setting with name "%s" does already exist for plugin "%s"', $setting->getName(), $this->pluginName));
}
$this->setDefaultTypeAndFieldIfNeeded($setting);
$this->addValidatorIfNeeded($setting);
$setting->setStorage($this);
$this->settings[$setting->getName()] = $setting;
}
private function getOptionKey()
{
return 'Plugin_' . $this->pluginName . '_Settings';
}
private function loadSettings()
{
$values = Option::get($this->getOptionKey());
if (!empty($values)) {
$this->settingsValues = unserialize($values);
}
}
private function checkIsValidSetting($name)
{
$setting = $this->getSetting($name);
if (empty($setting)) {
throw new \Exception(sprintf('The setting %s does not exist', $name));
}
$this->checkHasEnoughPermission($setting);
}
/**
* @param $name
* @return Setting|null
*/
private function getSetting($name)
{
if (array_key_exists($name, $this->settings)) {
return $this->settings[$name];
}
SettingsStorage::clearCache();
}
private function getDefaultType($controlType)
@ -320,30 +264,16 @@ abstract class Settings implements StorageInterface
return $defaultControlTypes[$type];
}
/**
* @param $setting
* @throws \Exception
*/
private function checkHasEnoughPermission(Setting $setting)
{
// When the request is a Tracker request, allow plugins to read/write settings
if(SettingsServer::isTrackerApiRequest()) {
return;
}
if (!$setting->canBeDisplayedForCurrentUser()) {
$errorMsg = Piwik::translate('CoreAdminHome_PluginSettingChangeNotAllowed', array($setting->getName(), $this->pluginName));
throw new \Exception($errorMsg);
}
}
private function setDefaultTypeAndFieldIfNeeded(Setting $setting)
{
if (!is_null($setting->uiControlType) && is_null($setting->type)) {
$hasControl = !is_null($setting->uiControlType);
$hasType = !is_null($setting->type);
if ($hasControl && !$hasType) {
$setting->type = $this->getDefaultType($setting->uiControlType);
} elseif (!is_null($setting->type) && is_null($setting->uiControlType)) {
} elseif ($hasType && !$hasControl) {
$setting->uiControlType = $this->getDefaultCONTROL($setting->type);
} elseif (is_null($setting->uiControlType) && is_null($setting->type)) {
} elseif (!$hasControl && !$hasType) {
$setting->type = static::TYPE_STRING;
$setting->uiControlType = static::CONTROL_TEXT;
}
@ -360,7 +290,7 @@ abstract class Settings implements StorageInterface
$setting->validate = function ($value) use ($setting, $pluginName) {
$errorMsg = Piwik::translate('CoreAdminHome_PluginSettingsValueNotAllowed',
array($setting->title, $pluginName));
array($setting->title, $pluginName));
if (is_array($value) && $setting->type == Settings::TYPE_ARRAY) {
foreach ($value as $val) {

View file

@ -0,0 +1,154 @@
<?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\Development;
use Piwik\Scheduler\Schedule\Schedule;
use Piwik\Scheduler\Task;
/**
* Base class for all Tasks declarations.
* Tasks are usually meant as scheduled tasks that are executed regularily by Piwik in the background. For instance
* once every hour or every day. This could be for instance checking for updates, sending email reports, etc.
* Please don't mix up tasks with console commands which can be executed on the CLI.
*/
class Tasks
{
/**
* @var Task[]
*/
private $tasks = array();
const LOWEST_PRIORITY = Task::LOWEST_PRIORITY;
const LOW_PRIORITY = Task::LOW_PRIORITY;
const NORMAL_PRIORITY = Task::NORMAL_PRIORITY;
const HIGH_PRIORITY = Task::HIGH_PRIORITY;
const HIGHEST_PRIORITY = Task::HIGHEST_PRIORITY;
/**
* This method is called to collect all schedule tasks. Register all your tasks here that should be executed
* regularily such as daily or monthly.
*/
public function schedule()
{
// eg $this->daily('myMethodName')
}
/**
* @return Task[] $tasks
*/
public function getScheduledTasks()
{
return $this->tasks;
}
/**
* Schedule the given tasks/method to run once every hour.
*
* @param string $methodName The name of the method that will be called when the task is being
* exectuted. To make it work you need to create a public method having the
* given method name in your Tasks class.
* @param null|string $methodParameter Can be null if the task does not need any parameter or a string. It is not
* possible to specify multiple parameters as an array etc. If you need to
* pass multiple parameters separate them via any characters such as '###'.
* For instance '$param1###$param2###$param3'
* @param int $priority Can be any constant such as self::LOW_PRIORITY
*
* @return Schedule
* @api
*/
protected function hourly($methodName, $methodParameter = null, $priority = self::NORMAL_PRIORITY)
{
return $this->custom($this, $methodName, $methodParameter, 'hourly', $priority);
}
/**
* Schedule the given tasks/method to run once every day.
*
* See {@link hourly()}
* @api
*/
protected function daily($methodName, $methodParameter = null, $priority = self::NORMAL_PRIORITY)
{
return $this->custom($this, $methodName, $methodParameter, 'daily', $priority);
}
/**
* Schedule the given tasks/method to run once every week.
*
* See {@link hourly()}
* @api
*/
protected function weekly($methodName, $methodParameter = null, $priority = self::NORMAL_PRIORITY)
{
return $this->custom($this, $methodName, $methodParameter, 'weekly', $priority);
}
/**
* Schedule the given tasks/method to run once every month.
*
* See {@link hourly()}
* @api
*/
protected function monthly($methodName, $methodParameter = null, $priority = self::NORMAL_PRIORITY)
{
return $this->custom($this, $methodName, $methodParameter, 'monthly', $priority);
}
/**
* Schedules the given tasks/method to run depending at the given scheduled time. Unlike the convenient methods
* such as {@link hourly()} you need to specify the object on which the given method should be called. This can be
* either an instance of a class or a class name. For more information about these parameters see {@link hourly()}
*
* @param string|object $objectOrClassName
* @param string $methodName
* @param null|string $methodParameter
* @param string|Schedule $time
* @param int $priority
*
* @return \Piwik\Scheduler\Schedule\Schedule
*
* @throws \Exception If a wrong time format is given. Needs to be either a string such as 'daily', 'weekly', ...
* or an instance of {@link Piwik\Scheduler\Schedule\Schedule}
*
* @api
*/
protected function custom($objectOrClassName, $methodName, $methodParameter, $time, $priority = self::NORMAL_PRIORITY)
{
$this->checkIsValidTask($objectOrClassName, $methodName);
if (is_string($time)) {
$time = Schedule::factory($time);
}
if (!($time instanceof Schedule)) {
throw new \Exception('$time should be an instance of Schedule');
}
$this->scheduleTask(new Task($objectOrClassName, $methodName, $methodParameter, $time, $priority));
return $time;
}
/**
* In case you need very high flexibility and none of the other convenient methods such as {@link hourly()} or
* {@link custom()} suit you, you can use this method to add a custom scheduled task.
*
* @param Task $task
*/
protected function scheduleTask(Task $task)
{
$this->tasks[] = $task;
}
private function checkIsValidTask($objectOrClassName, $methodName)
{
Development::checkMethodIsCallable($objectOrClassName, $methodName, 'The registered task is not valid as the method');
}
}

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
@ -22,64 +22,64 @@ use Piwik\ViewDataTable\RequestConfig as VizRequest;
/**
* The base class of all report visualizations.
*
*
* ViewDataTable instances load analytics data via Piwik's Reporting API and then output some
* type of visualization of that data.
*
*
* Visualizations can be in any format. HTML-based visualizations should extend
* {@link Visualization}. Visualizations that use other formats, such as visualizations
* that output an image, should extend ViewDataTable directly.
*
* ### Creating ViewDataTables
*
*
* ViewDataTable instances are not created via the new operator, instead the {@link Piwik\ViewDataTable\Factory}
* class is used.
*
*
* The specific subclass to create is determined, first, by the **viewDataTable** query paramater.
* If this parameter is not set, then the default visualization type for the report being
* displayed is used.
*
* ### Configuring ViewDataTables
*
*
* **Display properties**
*
*
* ViewDataTable output can be customized by setting one of many available display
* properties. Display properties are stored as fields in {@link Piwik\ViewDataTable\Config} objects.
* ViewDataTables store a {@link Piwik\ViewDataTable\Config} object in the {@link $config} field.
*
*
* Display properties can be set at any time before rendering.
*
*
* **Request properties**
*
*
* Request properties are similar to display properties in the way they are set. They are,
* however, not used to customize ViewDataTable instances, but in the request to Piwik's
* API when loading analytics data.
*
*
* Request properties are set by setting the fields of a {@link Piwik\ViewDataTable\RequestConfig} object stored in
* the {@link $requestConfig} field. They can be set at any time before rendering.
* Setting them after data is loaded will have no effect.
*
*
* **Customizing how reports are displayed**
*
*
* Each individual report should be rendered in its own controller method. There are two
* ways to render a report within its controller method. You can either:
*
*
* 1. manually create and configure a ViewDataTable instance
* 2. invoke {@link Piwik\Plugin\Controller::renderReport} and configure the ViewDataTable instance
* in the {@hook ViewDataTable.configure} event.
*
*
* ViewDataTable instances are configured by setting and modifying display properties and request
* properties.
*
*
* ### Creating new visualizations
*
*
* New visualizations can be created by extending the ViewDataTable class or one of its
* descendants. To learn more [read our guide on creating new visualizations](/guides/visualizing-report-data#creating-new-visualizations).
*
*
* ### Examples
*
*
* **Manually configuring a ViewDataTable**
*
*
* // a controller method that displays a single report
* public function myReport()
* {
@ -89,18 +89,18 @@ use Piwik\ViewDataTable\RequestConfig as VizRequest;
* // ...
* return $view->render();
* }
*
*
* **Using {@link Piwik\Plugin\Controller::renderReport}**
*
*
* First, a controller method that displays a single report:
*
*
* public function myReport()
* {
* return $this->renderReport(__FUNCTION__);`
* }
*
*
* Then the event handler for the {@hook ViewDataTable.configure} event:
*
*
* public function configureViewDataTable(ViewDataTable $view)
* {
* switch ($view->requestConfig->apiMethodToRequestDataTable) {
@ -111,32 +111,32 @@ use Piwik\ViewDataTable\RequestConfig as VizRequest;
* break;
* }
* }
*
*
* **Using custom configuration objects in a new visualization**
*
*
* class MyVisualizationConfig extends Piwik\ViewDataTable\Config
* {
* public $my_new_property = true;
* }
*
*
* class MyVisualizationRequestConfig extends Piwik\ViewDataTable\RequestConfig
* {
* public $my_new_property = false;
* }
*
*
* class MyVisualization extends Piwik\Plugin\ViewDataTable
* {
* public static function getDefaultConfig()
* {
* return new MyVisualizationConfig();
* }
*
*
* public static function getDefaultRequestConfig()
* {
* return new MyVisualizationRequestConfig();
* }
* }
*
*
*
* @api
*/
@ -153,14 +153,14 @@ abstract class ViewDataTable implements ViewInterface
/**
* Contains display properties for this visualization.
*
*
* @var \Piwik\ViewDataTable\Config
*/
public $config;
/**
* Contains request properties for this visualization.
*
*
* @var \Piwik\ViewDataTable\RequestConfig
*/
public $requestConfig;
@ -175,7 +175,7 @@ abstract class ViewDataTable implements ViewInterface
* Posts the {@hook ViewDataTable.configure} event which plugins can use to configure the
* way reports are displayed.
*/
public function __construct($controllerAction, $apiMethodToRequestDataTable)
public function __construct($controllerAction, $apiMethodToRequestDataTable, $overrideParams = array())
{
list($controllerName, $controllerAction) = explode('.', $controllerAction);
@ -191,13 +191,53 @@ abstract class ViewDataTable implements ViewInterface
$this->requestConfig->apiMethodToRequestDataTable = $apiMethodToRequestDataTable;
$report = Report::factory($this->requestConfig->getApiModuleToRequest(), $this->requestConfig->getApiMethodToRequest());
if (!empty($report)) {
/** @var Report $report */
$subtable = $report->getActionToLoadSubTables();
if (!empty($subtable)) {
$this->config->subtable_controller_action = $subtable;
}
$this->config->show_goals = $report->hasGoalMetrics();
$relatedReports = $report->getRelatedReports();
if (!empty($relatedReports)) {
foreach ($relatedReports as $relatedReport) {
$widgetTitle = $relatedReport->getWidgetTitle();
if ($widgetTitle && Common::getRequestVar('widget', 0, 'int')) {
$relatedReportName = $widgetTitle;
} else {
$relatedReportName = $relatedReport->getName();
}
$this->config->addRelatedReport($relatedReport->getModule() . '.' . $relatedReport->getAction(),
$relatedReportName);
}
}
$metrics = $report->getMetrics();
if (!empty($metrics)) {
$this->config->addTranslations($metrics);
}
$processedMetrics = $report->getProcessedMetrics();
if (!empty($processedMetrics)) {
$this->config->addTranslations($processedMetrics);
}
$report->configureView($this);
}
/**
* Triggered during {@link ViewDataTable} construction. Subscribers should customize
* the view based on the report that is being displayed.
*
*
* Plugins that define their own reports must subscribe to this event in order to
* specify how the Piwik UI should display the report.
*
*
* **Example**
*
* // event handler
@ -210,7 +250,7 @@ abstract class ViewDataTable implements ViewInterface
* break;
* }
* }
*
*
* @param ViewDataTable $view The instance to configure.
*/
Piwik::postEvent('ViewDataTable.configure', array($this));
@ -229,16 +269,17 @@ abstract class ViewDataTable implements ViewInterface
$this->requestConfig->filter_excludelowpop_value = $function();
}
$this->overrideViewPropertiesWithParams($overrideParams);
$this->overrideViewPropertiesWithQueryParams();
}
protected function assignRelatedReportsTitle()
{
if(!empty($this->config->related_reports_title)) {
if (!empty($this->config->related_reports_title)) {
// title already assigned by a plugin
return;
}
if(count($this->config->related_reports) == 1) {
if (count($this->config->related_reports) == 1) {
$this->config->related_reports_title = Piwik::translate('General_RelatedReport') . ':';
} else {
$this->config->related_reports_title = Piwik::translate('General_RelatedReports') . ':';
@ -247,12 +288,12 @@ abstract class ViewDataTable implements ViewInterface
/**
* Returns the default config instance.
*
*
* Visualizations that define their own display properties should override this method and
* return an instance of their new {@link Piwik\ViewDataTable\Config} descendant.
*
* See the last example {@link ViewDataTable here} for more information.
*
*
* @return \Piwik\ViewDataTable\Config
*/
public static function getDefaultConfig()
@ -262,12 +303,12 @@ abstract class ViewDataTable implements ViewInterface
/**
* Returns the default request config instance.
*
*
* Visualizations that define their own request properties should override this method and
* return an instance of their new {@link Piwik\ViewDataTable\RequestConfig} descendant.
*
* See the last example {@link ViewDataTable here} for more information.
*
*
* @return \Piwik\ViewDataTable\RequestConfig
*/
public static function getDefaultRequestConfig()
@ -275,7 +316,7 @@ abstract class ViewDataTable implements ViewInterface
return new VizRequest();
}
protected function loadDataTableFromAPI($fixedRequestParams = array())
protected function loadDataTableFromAPI()
{
if (!is_null($this->dataTable)) {
// data table is already there
@ -283,14 +324,14 @@ abstract class ViewDataTable implements ViewInterface
return $this->dataTable;
}
$this->dataTable = $this->request->loadDataTableFromAPI($fixedRequestParams);
$this->dataTable = $this->request->loadDataTableFromAPI();
return $this->dataTable;
}
/**
* Returns the viewDataTable ID for this DataTable visualization.
*
*
* Derived classes should not override this method. They should instead declare a const ID field
* with the viewDataTable ID.
*
@ -306,13 +347,13 @@ abstract class ViewDataTable implements ViewInterface
throw new \Exception($message);
}
return $id;
return $id;
}
/**
* Returns `true` if this instance's or any of its ancestors' viewDataTable IDs equals the supplied ID,
* `false` if otherwise.
*
*
* Can be used to test whether a ViewDataTable object is an instance of a certain visualization or not,
* without having to know where that visualization is.
*
@ -399,7 +440,7 @@ abstract class ViewDataTable implements ViewInterface
if (property_exists($this->requestConfig, $name)) {
$this->requestConfig->$name = $this->getPropertyFromQueryParam($name, $this->requestConfig->$name);
} elseif (property_exists($this->config, $name)) {
$this->config->$name = $this->getPropertyFromQueryParam($name, $this->config->$name);
$this->config->$name = $this->getPropertyFromQueryParam($name, $this->config->$name);
}
}
@ -443,7 +484,7 @@ abstract class ViewDataTable implements ViewInterface
/**
* Returns `true` if this visualization can display some type of data or not.
*
*
* New visualization classes should override this method if they can only visualize certain
* types of data. The evolution graph visualization, for example, can only visualize
* sets of DataTables. If the API method used results in a single DataTable, the evolution
@ -456,4 +497,63 @@ abstract class ViewDataTable implements ViewInterface
{
return $view->config->show_all_views_icons;
}
private function overrideViewPropertiesWithParams($overrideParams)
{
if (empty($overrideParams)) {
return;
}
foreach ($overrideParams as $key => $value) {
if (property_exists($this->requestConfig, $key)) {
$this->requestConfig->$key = $value;
} elseif (property_exists($this->config, $key)) {
$this->config->$key = $value;
} elseif ($key != 'enable_filter_excludelowpop') {
$this->config->custom_parameters[$key] = $value;
}
}
}
/**
* Display a meaningful error message when any invalid parameter is being set.
*
* @param $overrideParams
* @throws
*/
public function throwWhenSettingNonOverridableParameter($overrideParams)
{
$nonOverridableParams = $this->getNonOverridableParams($overrideParams);
if(count($nonOverridableParams) > 0) {
throw new \Exception(sprintf(
"Setting parameters %s is not allowed. Please report this bug to the Piwik team.",
implode(" and ", $nonOverridableParams)
));
}
}
/**
* @param $overrideParams
* @return array
*/
public function getNonOverridableParams($overrideParams)
{
$paramsCannotBeOverridden = array();
foreach ($overrideParams as $paramName => $paramValue) {
if (property_exists($this->requestConfig, $paramName)) {
$allowedParams = $this->requestConfig->overridableProperties;
} elseif (property_exists($this->config, $paramName)) {
$allowedParams = $this->config->overridableProperties;
} else {
// setting Config.custom_parameters is always allowed
continue;
}
if (!in_array($paramName, $allowedParams)) {
$paramsCannotBeOverridden[] = $paramName;
}
}
return $paramsCannotBeOverridden;
}
}

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,26 +9,32 @@
namespace Piwik\Plugin;
use Piwik\API\DataTablePostProcessor;
use Piwik\API\Proxy;
use Piwik\API\ResponseBuilder;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\Date;
use Piwik\Log;
use Piwik\MetricsFormatter;
use Piwik\Metrics\Formatter\Html as HtmlFormatter;
use Piwik\NoAccessException;
use Piwik\Option;
use Piwik\Period;
use Piwik\Piwik;
use Piwik\Plugins\API\API as ApiApi;
use Piwik\Plugins\PrivacyManager\PrivacyManager;
use Piwik\View;
use Piwik\ViewDataTable\Manager as ViewDataTableManager;
use Piwik\Plugin\Manager as PluginManager;
use Piwik\API\Request as ApiRequest;
/**
* The base class for report visualizations that output HTML and use JavaScript.
*
*
* Report visualizations that extend from this class will be displayed like all others in
* the Piwik UI. The following extra UI controls will be displayed around the visualization
* itself:
*
*
* - report documentation,
* - a footer message (if {@link Piwik\ViewDataTable\Config::$show_footer_message} is set),
* - a list of links to related reports (if {@link Piwik\ViewDataTable\Config::$related_reports} is set),
@ -37,33 +43,33 @@ use Piwik\ViewDataTable\Manager as ViewDataTableManager;
* - a limit control that allows users to change the amount of rows displayed (if
* {@link Piwik\ViewDataTable\Config::$show_limit_control} is true),
* - and more depending on the visualization.
*
*
* ### Rendering Process
*
*
* The following process is used to render reports:
*
*
* - The report is loaded through Piwik's Reporting API.
* - The display and request properties that require report data in order to determine a default
* value are defaulted. These properties are:
*
*
* - {@link Piwik\ViewDataTable\Config::$columns_to_display}
* - {@link Piwik\ViewDataTable\RequestConfig::$filter_sort_column}
* - {@link Piwik\ViewDataTable\RequestConfig::$filter_sort_order}
*
*
* - Priority filters are applied to the report (see {@link Piwik\ViewDataTable\Config::$filters}).
* - The filters that are applied to every report in the Reporting API (called **generic filters**)
* are applied. (see {@link Piwik\API\Request})
* - The report's queued filters are applied.
* - A {@link Piwik\View} instance is created and rendered.
*
*
* ### Rendering Hooks
*
*
* The Visualization class defines several overridable methods that are called at specific
* points during the rendering process. Derived classes can override these methods change
* the data that is displayed or set custom properties.
*
*
* The overridable methods (called **rendering hooks**) are as follows:
*
*
* - **beforeLoadDataTable**: Called at the start of the rendering process before any data
* is loaded.
* - **beforeGenericFiltersAreAppliedToLoadedDataTable**: Called after data is loaded and after priority
@ -76,20 +82,20 @@ use Piwik\ViewDataTable\Manager as ViewDataTableManager;
* - **beforeRender**: Called immediately before a {@link Piwik\View} is created and rendered.
* - **isThereDataToDisplay**: Called after a {@link Piwik\View} is created to determine if the report has
* data or not. If not, a message is displayed to the user.
*
*
* ### The DataTable JavaScript class
*
*
* In the UI, visualization behavior is provided by logic in the **DataTable** JavaScript class.
* When creating new visualizations, the **DataTable** JavaScript class (or one of its existing
* descendants) should be extended.
*
*
* To learn more read the [Visualizing Report Data](/guides/visualizing-report-data#creating-new-visualizations)
* guide.
*
* ### Examples
*
*
* **Changing the data that is loaded**
*
*
* class MyVisualization extends Visualization
* {
* // load the previous period's data as well as the requested data. this will change
@ -101,20 +107,20 @@ use Piwik\ViewDataTable\Manager as ViewDataTableManager;
*
* $this->requestConfig->request_parameters_to_modify['date'] = $previousDate . ',' . $date;
* }
*
*
* // since we load the previous period's data too, we need to override the logic to
* // check if there is data or not.
* public function isThereDataToDisplay()
* {
* $tables = $this->dataTable->getDataTables()
* $requestedDataTable = end($tables);
*
*
* return $requestedDataTable->getRowsCount() != 0;
* }
* }
*
*
* **Force properties to be set to certain values**
*
*
* class MyVisualization extends Visualization
* {
* // ensure that some properties are set to certain values before rendering.
@ -133,9 +139,9 @@ class Visualization extends ViewDataTable
{
/**
* The Twig template file to use when rendering, eg, `"@MyPlugin/_myVisualization.twig"`.
*
*
* Must be defined by classes that extend Visualization.
*
*
* @api
*/
const TEMPLATE_FILE = '';
@ -143,8 +149,14 @@ class Visualization extends ViewDataTable
private $templateVars = array();
private $reportLastUpdatedMessage = null;
private $metadata = null;
protected $metricsFormatter = null;
final public function __construct($controllerAction, $apiMethodToRequestDataTable)
/**
* @var Report
*/
protected $report;
final public function __construct($controllerAction, $apiMethodToRequestDataTable, $params = array())
{
$templateFile = static::TEMPLATE_FILE;
@ -152,7 +164,11 @@ class Visualization extends ViewDataTable
throw new \Exception('You have not defined a constant named TEMPLATE_FILE in your visualization class.');
}
parent::__construct($controllerAction, $apiMethodToRequestDataTable);
$this->metricsFormatter = new HtmlFormatter();
parent::__construct($controllerAction, $apiMethodToRequestDataTable, $params);
$this->report = Report::factory($this->requestConfig->getApiModuleToRequest(), $this->requestConfig->getApiMethodToRequest());
}
protected function buildView()
@ -160,26 +176,29 @@ class Visualization extends ViewDataTable
$this->overrideSomeConfigPropertiesIfNeeded();
try {
$this->beforeLoadDataTable();
$this->loadDataTableFromAPI(array('disable_generic_filters' => 1));
$this->loadDataTableFromAPI();
$this->postDataTableLoadedFromAPI();
$requestPropertiesAfterLoadDataTable = $this->requestConfig->getProperties();
$this->applyFilters();
$this->addVisualizationInfoFromMetricMetadata();
$this->afterAllFiltersAreApplied();
$this->beforeRender();
$this->logMessageIfRequestPropertiesHaveChanged($requestPropertiesAfterLoadDataTable);
} catch (NoAccessException $e) {
throw $e;
} catch (\Exception $e) {
Log::warning("Failed to get data from API: " . $e->getMessage() . "\n" . $e->getTraceAsString());
Log::error("Failed to get data from API: " . $e->getMessage() . "\n" . $e->getTraceAsString());
$loadingError = array('message' => $e->getMessage());
$message = $e->getMessage();
if (\Piwik_ShouldPrintBackTraceWithMessage()) {
$message .= "\n" . $e->getTraceAsString();
}
$loadingError = array('message' => $message);
}
$view = new View("@CoreHome/_dataTable");
@ -192,6 +211,7 @@ class Visualization extends ViewDataTable
$view->visualization = $this;
$view->visualizationTemplate = static::TEMPLATE_FILE;
$view->visualizationCssClass = $this->getDefaultDataTableCssClass();
$view->reportMetdadata = $this->getReportMetadata();
if (null === $this->dataTable) {
$view->dataTable = null;
@ -216,17 +236,78 @@ class Visualization extends ViewDataTable
return $view;
}
/**
* @internal
*/
protected function loadDataTableFromAPI()
{
if (!is_null($this->dataTable)) {
// data table is already there
// this happens when setDataTable has been used
return $this->dataTable;
}
// we build the request (URL) to call the API
$request = $this->buildApiRequestArray();
$module = $this->requestConfig->getApiModuleToRequest();
$method = $this->requestConfig->getApiMethodToRequest();
PluginManager::getInstance()->checkIsPluginActivated($module);
$class = ApiRequest::getClassNameAPI($module);
$dataTable = Proxy::getInstance()->call($class, $method, $request);
$response = new ResponseBuilder($format = 'original', $request);
$response->disableSendHeader();
$response->disableDataTablePostProcessor();
$this->dataTable = $response->getResponse($dataTable, $module, $method);
}
private function getReportMetadata()
{
$request = $this->request->getRequestArray() + $_GET + $_POST;
$idSite = Common::getRequestVar('idSite', null, 'string', $request);
$module = $this->requestConfig->getApiModuleToRequest();
$action = $this->requestConfig->getApiMethodToRequest();
$apiParameters = array();
$idDimension = Common::getRequestVar('idDimension', 0, 'int');
$idGoal = Common::getRequestVar('idGoal', 0, 'int');
if ($idDimension > 0) {
$apiParameters['idDimension'] = $idDimension;
}
if ($idGoal > 0) {
$apiParameters['idGoal'] = $idGoal;
}
$metadata = ApiApi::getInstance()->getMetadata($idSite, $module, $action, $apiParameters);
if (!empty($metadata)) {
return array_shift($metadata);
}
return false;
}
private function overrideSomeConfigPropertiesIfNeeded()
{
if (empty($this->config->footer_icons)) {
$this->config->footer_icons = ViewDataTableManager::configureFooterIcons($this);
}
if (!\Piwik\Plugin\Manager::getInstance()->isPluginActivated('Goals')) {
if (!$this->isPluginActivated('Goals')) {
$this->config->show_goals = false;
}
}
private function isPluginActivated($pluginName)
{
return PluginManager::getInstance()->isPluginActivated($pluginName);
}
/**
* Assigns a template variable making it available in the Twig template specified by
* {@link TEMPLATE_FILE}.
@ -248,9 +329,9 @@ class Visualization extends ViewDataTable
/**
* Returns `true` if there is data to display, `false` if otherwise.
*
*
* Derived classes should override this method if they change the amount of data that is loaded.
*
*
* @api
*/
protected function isThereDataToDisplay()
@ -281,7 +362,7 @@ class Visualization extends ViewDataTable
}
if (empty($this->requestConfig->filter_sort_column)) {
$this->requestConfig->setDefaultSort($this->config->columns_to_display, $hasNbUniqVisitors);
$this->requestConfig->setDefaultSort($this->config->columns_to_display, $hasNbUniqVisitors, $columns);
}
// deal w/ table metadata
@ -289,38 +370,78 @@ class Visualization extends ViewDataTable
$this->metadata = $this->dataTable->getAllTableMetadata();
if (isset($this->metadata[DataTable::ARCHIVED_DATE_METADATA_NAME])) {
$this->config->report_last_updated_message = $this->makePrettyArchivedOnText();
$this->reportLastUpdatedMessage = $this->makePrettyArchivedOnText();
}
}
$pivotBy = Common::getRequestVar('pivotBy', false) ?: $this->requestConfig->pivotBy;
if (empty($pivotBy)
&& $this->dataTable instanceof DataTable
) {
$this->config->disablePivotBySubtableIfTableHasNoSubtables($this->dataTable);
}
}
private function addVisualizationInfoFromMetricMetadata()
{
$dataTable = $this->dataTable instanceof DataTable\Map ? $this->dataTable->getFirstRow() : $this->dataTable;
$metrics = Report::getMetricsForTable($dataTable, $this->report);
// TODO: instead of iterating & calling translate everywhere, maybe we can get all translated names in one place.
// may be difficult, though, since translated metrics are specific to the report.
foreach ($metrics as $metric) {
$name = $metric->getName();
if (empty($this->config->translations[$name])) {
$this->config->translations[$name] = $metric->getTranslatedName();
}
if (empty($this->config->metrics_documentation[$name])) {
$this->config->metrics_documentation[$name] = $metric->getDocumentation();
}
}
}
private function applyFilters()
{
list($priorityFilters, $otherFilters) = $this->config->getFiltersToRun();
$postProcessor = $this->makeDataTablePostProcessor(); // must be created after requestConfig is final
$self = $this;
// First, filters that delete rows
foreach ($priorityFilters as $filter) {
$this->dataTable->filter($filter[0], $filter[1]);
}
$postProcessor->setCallbackBeforeGenericFilters(function (DataTable\DataTableInterface $dataTable) use ($self, $postProcessor) {
$this->beforeGenericFiltersAreAppliedToLoadedDataTable();
$self->setDataTable($dataTable);
if (!$this->requestConfig->areGenericFiltersDisabled()) {
$this->applyGenericFilters();
}
// First, filters that delete rows
foreach ($self->config->getPriorityFilters() as $filter) {
$dataTable->filter($filter[0], $filter[1]);
}
$this->afterGenericFiltersAreAppliedToLoadedDataTable();
$self->beforeGenericFiltersAreAppliedToLoadedDataTable();
// queue other filters so they can be applied later if queued filters are disabled
foreach ($otherFilters as $filter) {
$this->dataTable->queueFilter($filter[0], $filter[1]);
}
if (!in_array($self->requestConfig->filter_sort_column, $self->config->columns_to_display)) {
$hasNbUniqVisitors = in_array('nb_uniq_visitors', $self->config->columns_to_display);
$columns = $dataTable->getColumns();
$self->requestConfig->setDefaultSort($self->config->columns_to_display, $hasNbUniqVisitors, $columns);
}
// Finally, apply datatable filters that were queued (should be 'presentation' filters that
// do not affect the number of rows)
if (!$this->requestConfig->areQueuedFiltersDisabled()) {
$this->dataTable->applyQueuedFilters();
}
$postProcessor->setRequest($self->buildApiRequestArray());
});
$postProcessor->setCallbackAfterGenericFilters(function (DataTable\DataTableInterface $dataTable) use ($self) {
$self->setDataTable($dataTable);
$self->afterGenericFiltersAreAppliedToLoadedDataTable();
// queue other filters so they can be applied later if queued filters are disabled
foreach ($self->config->getPresentationFilters() as $filter) {
$dataTable->queueFilter($filter[0], $filter[1]);
}
});
$this->dataTable = $postProcessor->process($this->dataTable);
}
private function removeEmptyColumnsFromDisplay()
@ -355,16 +476,16 @@ class Visualization extends ViewDataTable
$today = mktime(0, 0, 0);
if ($date->getTimestamp() > $today) {
$elapsedSeconds = time() - $date->getTimestamp();
$timeAgo = MetricsFormatter::getPrettyTimeFromSeconds($elapsedSeconds);
$timeAgo = $this->metricsFormatter->getPrettyTimeFromSeconds($elapsedSeconds);
return Piwik::translate('CoreHome_ReportGeneratedXAgo', $timeAgo);
}
$prettyDate = $date->getLocalized("%longYear%, %longMonth% %day%") . $date->toString('S');
$prettyDate = $date->getLocalized(Date::DATE_FORMAT_SHORT);
return Piwik::translate('CoreHome_ReportGeneratedOn', $prettyDate);
$timezoneAppend = ' (UTC)';
return Piwik::translate('CoreHome_ReportGeneratedOn', $prettyDate) . $timezoneAppend;
}
/**
@ -379,7 +500,7 @@ class Visualization extends ViewDataTable
*/
private function hasReportBeenPurged()
{
if (!\Piwik\Plugin\Manager::getInstance()->isPluginActivated('PrivacyManager')) {
if (!$this->isPluginActivated('PrivacyManager')) {
return false;
}
@ -399,7 +520,7 @@ class Visualization extends ViewDataTable
foreach ($this->config->clientSideProperties as $name) {
if (property_exists($this->requestConfig, $name)) {
$result[$name] = $this->getIntIfValueIsBool($this->requestConfig->$name);
} else if (property_exists($this->config, $name)) {
} elseif (property_exists($this->config, $name)) {
$result[$name] = $this->getIntIfValueIsBool($this->config->$name);
}
}
@ -453,7 +574,7 @@ class Visualization extends ViewDataTable
if (property_exists($this->requestConfig, $name)) {
$valueToConvert = $this->requestConfig->$name;
} else if (property_exists($this->config, $name)) {
} elseif (property_exists($this->config, $name)) {
$valueToConvert = $this->config->$name;
}
@ -481,6 +602,7 @@ class Visualization extends ViewDataTable
'filter_excludelowpop',
'filter_excludelowpop_value',
);
foreach ($deleteFromJavascriptVariables as $name) {
if (isset($javascriptVariablesToSet[$name])) {
unset($javascriptVariablesToSet[$name]);
@ -497,9 +619,11 @@ class Visualization extends ViewDataTable
/**
* Hook that is called before loading report data from the API.
*
*
* Use this method to change the request parameters that is sent to the API when requesting
* data.
*
* @api
*/
public function beforeLoadDataTable()
{
@ -507,9 +631,11 @@ class Visualization extends ViewDataTable
/**
* Hook that is executed before generic filters are applied.
*
*
* Use this method if you need access to the entire dataset (since generic filters will
* limit and truncate reports).
*
* @api
*/
public function beforeGenericFiltersAreAppliedToLoadedDataTable()
{
@ -517,6 +643,8 @@ class Visualization extends ViewDataTable
/**
* Hook that is executed after generic filters are applied.
*
* @api
*/
public function afterGenericFiltersAreAppliedToLoadedDataTable()
{
@ -525,6 +653,8 @@ class Visualization extends ViewDataTable
/**
* Hook that is executed after the report data is loaded and after all filters have been applied.
* Use this method to format the report data before the view is rendered.
*
* @api
*/
public function afterAllFiltersAreApplied()
{
@ -533,27 +663,24 @@ class Visualization extends ViewDataTable
/**
* Hook that is executed directly before rendering. Use this hook to force display properties to
* be a certain value, despite changes from plugins and query parameters.
*
* @api
*/
public function beforeRender()
{
// eg $this->config->showFooterColumns = true;
}
/**
* Second, generic filters (Sort, Limit, Replace Column Names, etc.)
*/
private function applyGenericFilters()
private function makeDataTablePostProcessor()
{
$requestArray = $this->request->getRequestArray();
$request = \Piwik\API\Request::getRequestArrayFromString($requestArray);
$request = $this->buildApiRequestArray();
$module = $this->requestConfig->getApiModuleToRequest();
$method = $this->requestConfig->getApiMethodToRequest();
if (false === $this->config->enable_sort) {
$request['filter_sort_column'] = '';
$request['filter_sort_order'] = '';
}
$processor = new DataTablePostProcessor($module, $method, $request);
$processor->setFormatter($this->metricsFormatter);
$genericFilter = new \Piwik\API\DataTableGenericFilter($request);
$genericFilter->filter($this->dataTable);
return $processor;
}
private function logMessageIfRequestPropertiesHaveChanged(array $requestPropertiesBefore)
@ -563,6 +690,15 @@ class Visualization extends ViewDataTable
$diff = array_diff_assoc($this->makeSureArrayContainsOnlyStrings($requestProperties),
$this->makeSureArrayContainsOnlyStrings($requestPropertiesBefore));
if (!empty($diff['filter_sort_column'])) {
// this here might be ok as it can be changed after data loaded but before filters applied
unset($diff['filter_sort_column']);
}
if (!empty($diff['filter_sort_order'])) {
// this here might be ok as it can be changed after data loaded but before filters applied
unset($diff['filter_sort_order']);
}
if (empty($diff)) {
return;
}
@ -592,4 +728,30 @@ class Visualization extends ViewDataTable
return $result;
}
/**
* @internal
*
* @return array
*/
public function buildApiRequestArray()
{
$requestArray = $this->request->getRequestArray();
$request = APIRequest::getRequestArrayFromString($requestArray);
if (false === $this->config->enable_sort) {
$request['filter_sort_column'] = '';
$request['filter_sort_order'] = '';
}
if (!array_key_exists('format_metrics', $request) || $request['format_metrics'] === 'bc') {
$request['format_metrics'] = '1';
}
if (!$this->requestConfig->disable_queued_filters && array_key_exists('disable_queued_filters', $request)) {
unset($request['disable_queued_filters']);
}
return $request;
}
}

View file

@ -0,0 +1,198 @@
<?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\Development;
use Piwik\Plugin\Manager as PluginManager;
use Piwik\WidgetsList;
/**
* Base class of all plugin widget providers. Plugins that define their own widgets can extend this class to easily
* add new widgets or to remove widgets defined by other plugins.
*
* For an example, see the {@link https://github.com/piwik/piwik/blob/master/plugins/ExamplePlugin/Widgets.php} plugin.
*
* @api
*/
class Widgets
{
protected $category = '';
protected $widgets = array();
public function __construct()
{
// Constructor kept for BC (because called in implementations)
}
/**
* @ignore
*/
public function getCategory()
{
return $this->category;
}
private function getModule()
{
$className = get_class($this);
$className = explode('\\', $className);
return $className[2];
}
/**
* Adds a widget. You can add a widget by calling this method and passing the name of the widget as well as a method
* name that will be executed to render the widget. The method can be defined either directly here in this widget
* class or in the controller in case you want to reuse the same action for instance in the menu etc.
* @api
*/
protected function addWidget($name, $method, $parameters = array())
{
$this->addWidgetWithCustomCategory($this->category, $name, $method, $parameters);
}
/**
* Adds a widget with a custom category. By default all widgets that you define in your class will be added under
* the same category which is defined in the {@link $category} property. Sometimes you may have a widget that
* belongs to a different category where this method comes handy. It does the same as {@link addWidget()} but
* allows you to define the category name as well.
* @api
*/
protected function addWidgetWithCustomCategory($category, $name, $method, $parameters = array())
{
$this->checkIsValidWidget($name, $method);
$this->widgets[] = array('category' => $category,
'name' => $name,
'params' => $parameters,
'method' => $method,
'module' => $this->getModule());
}
/**
* Here you can add one or multiple widgets. To do so call the method {@link addWidget()} or
* {@link addWidgetWithCustomCategory()}.
* @api
*/
protected function init()
{
}
/**
* @ignore
*/
public function getWidgets()
{
$this->widgets = array();
$this->init();
return $this->widgets;
}
/**
* Allows you to configure previously added widgets.
* For instance you can remove any widgets defined by any plugin by calling the
* {@link \Piwik\WidgetsList::remove()} method.
*
* @param WidgetsList $widgetsList
* @api
*/
public function configureWidgetsList(WidgetsList $widgetsList)
{
}
/**
* @return \Piwik\Plugin\Widgets[]
* @ignore
*/
public static function getAllWidgets()
{
return PluginManager::getInstance()->findComponents('Widgets', 'Piwik\\Plugin\\Widgets');
}
/**
* @ignore
* @return Widgets|null
*/
public static function factory($module, $action)
{
if (empty($module) || empty($action)) {
return;
}
$pluginManager = PluginManager::getInstance();
try {
if (!$pluginManager->isPluginActivated($module)) {
return;
}
$plugin = $pluginManager->getLoadedPlugin($module);
} catch (\Exception $e) {
// we are not allowed to use possible widgets, plugin is not active
return;
}
/** @var Widgets $widgetContainer */
$widgetContainer = $plugin->findComponent('Widgets', 'Piwik\\Plugin\\Widgets');
if (empty($widgetContainer)) {
// plugin does not define any widgets, we cannot do anything
return;
}
if (!is_callable(array($widgetContainer, $action))) {
// widget does not implement such a method, we cannot do anything
return;
}
// the widget class implements such an action, but we have to check whether it is actually exposed and whether
// it was maybe disabled by another plugin, this is only possible by checking the widgetslist, unfortunately
if (!WidgetsList::isDefined($module, $action)) {
return;
}
return $widgetContainer;
}
private function checkIsValidWidget($name, $method)
{
if (!Development::isEnabled()) {
return;
}
if (empty($name)) {
Development::error('No name is defined for added widget having method "' . $method . '" in ' . get_class($this));
}
if (Development::isCallableMethod($this, $method)) {
return;
}
$controllerClass = 'Piwik\\Plugins\\' . $this->getModule() . '\\Controller';
if (!Development::methodExists($this, $method) &&
!Development::methodExists($controllerClass, $method)) {
Development::error('The added method "' . $method . '" neither exists in "' . get_class($this) . '" nor "' . $controllerClass . '". Make sure to define such a method.');
}
$definedInClass = get_class($this);
if (Development::methodExists($controllerClass, $method)) {
if (Development::isCallableMethod($controllerClass, $method)) {
return;
}
$definedInClass = $controllerClass;
}
Development::error('The method "' . $method . '" is not callable on "' . $definedInClass . '". Make sure the method is public.');
}
}