1251 lines
37 KiB
PHP
1251 lines
37 KiB
PHP
<?php
|
|
/**
|
|
* Piwik - Open source web analytics
|
|
*
|
|
* @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\Config as PiwikConfig;
|
|
use Piwik\Config;
|
|
use Piwik\EventDispatcher;
|
|
use Piwik\Filesystem;
|
|
use Piwik\Option;
|
|
use Piwik\Plugin;
|
|
use Piwik\SettingsServer;
|
|
use Piwik\Singleton;
|
|
use Piwik\Theme;
|
|
use Piwik\Tracker;
|
|
use Piwik\Translate;
|
|
use Piwik\Updater;
|
|
|
|
require_once PIWIK_INCLUDE_PATH . '/core/EventDispatcher.php';
|
|
|
|
/**
|
|
* The singleton that manages plugin loading/unloading and installation/uninstallation.
|
|
*
|
|
* @method static \Piwik\Plugin\Manager getInstance()
|
|
*/
|
|
class Manager extends Singleton
|
|
{
|
|
protected $pluginsToLoad = array();
|
|
|
|
protected $doLoadPlugins = true;
|
|
/**
|
|
* @var Plugin[]
|
|
*/
|
|
protected $loadedPlugins = array();
|
|
/**
|
|
* Default theme used in Piwik.
|
|
*/
|
|
const DEFAULT_THEME = "Zeitgeist";
|
|
|
|
protected $doLoadAlwaysActivatedPlugins = true;
|
|
|
|
// These are always activated and cannot be deactivated
|
|
protected $pluginToAlwaysActivate = array(
|
|
'CoreHome',
|
|
'CoreUpdater',
|
|
'CoreAdminHome',
|
|
'CoreConsole',
|
|
'CorePluginsAdmin',
|
|
'CoreVisualizations',
|
|
'Installation',
|
|
'SitesManager',
|
|
'UsersManager',
|
|
'API',
|
|
'Proxy',
|
|
'LanguagesManager',
|
|
|
|
// default Piwik theme, always enabled
|
|
self::DEFAULT_THEME,
|
|
);
|
|
|
|
// Plugins bundled with core package, disabled by default
|
|
protected $corePluginsDisabledByDefault = array(
|
|
'DBStats',
|
|
'DevicesDetection',
|
|
'ExampleCommand',
|
|
'ExampleSettingsPlugin',
|
|
'ExampleUI',
|
|
'ExampleVisualization',
|
|
'ExamplePluginTemplate',
|
|
);
|
|
|
|
// Themes bundled with core package, disabled by default
|
|
protected $coreThemesDisabledByDefault = array(
|
|
'ExampleTheme',
|
|
'LeftMenu',
|
|
'Zeitgeist',
|
|
);
|
|
|
|
/**
|
|
* Loads plugin that are enabled
|
|
*/
|
|
public function loadActivatedPlugins()
|
|
{
|
|
$pluginsToLoad = Config::getInstance()->Plugins['Plugins'];
|
|
$this->loadPlugins($pluginsToLoad);
|
|
}
|
|
|
|
/**
|
|
* Called during Tracker
|
|
*/
|
|
public function loadCorePluginsDuringTracker()
|
|
{
|
|
$pluginsToLoad = Config::getInstance()->Plugins['Plugins'];
|
|
$pluginsToLoad = array_diff($pluginsToLoad, Tracker::getPluginsNotToLoad());
|
|
if(defined('PIWIK_TEST_MODE')) {
|
|
$pluginsToLoad = array_intersect($pluginsToLoad, $this->getPluginsToLoadDuringTests());
|
|
}
|
|
$this->loadPlugins($pluginsToLoad);
|
|
}
|
|
|
|
/**
|
|
* @return array names of plugins that have been loaded
|
|
*/
|
|
public function loadTrackerPlugins()
|
|
{
|
|
$this->unloadPlugins();
|
|
$pluginsTracker = PiwikConfig::getInstance()->Plugins_Tracker['Plugins_Tracker'];
|
|
if (empty($pluginsTracker)) {
|
|
return array();
|
|
}
|
|
|
|
$pluginsTracker = array_diff($pluginsTracker, Tracker::getPluginsNotToLoad());
|
|
if(defined('PIWIK_TEST_MODE')) {
|
|
$pluginsTracker = array_intersect($pluginsTracker, $this->getPluginsToLoadDuringTests());
|
|
}
|
|
$this->doNotLoadAlwaysActivatedPlugins();
|
|
$this->loadPlugins($pluginsTracker);
|
|
return $pluginsTracker;
|
|
}
|
|
|
|
public function getPluginsToLoadDuringTests()
|
|
{
|
|
$toLoad = array();
|
|
|
|
$loadStandalonePluginsDuringTests = @Config::getInstance()->DebugTests['enable_load_standalone_plugins_during_tests'];
|
|
|
|
foreach($this->readPluginsDirectory() as $plugin) {
|
|
$forceDisable = array(
|
|
'ExampleVisualization', // adds an icon
|
|
'LoginHttpAuth', // other Login plugins would conflict
|
|
);
|
|
if(in_array($plugin, $forceDisable)) {
|
|
continue;
|
|
}
|
|
|
|
// Load all default plugins
|
|
$isPluginBundledWithCore = $this->isPluginBundledWithCore($plugin);
|
|
|
|
// Load plugins from submodules
|
|
$isPluginOfficiallySupported = $this->isPluginOfficialAndNotBundledWithCore($plugin);
|
|
|
|
// Also load plugins which are Git repositories (eg. being developed)
|
|
$isPluginHasGitRepository = file_exists( PIWIK_INCLUDE_PATH . '/plugins/' . $plugin . '/.git/config');
|
|
|
|
$loadPlugin = $isPluginBundledWithCore || $isPluginOfficiallySupported;
|
|
|
|
if($loadStandalonePluginsDuringTests) {
|
|
$loadPlugin = $loadPlugin || $isPluginHasGitRepository;
|
|
} else {
|
|
$loadPlugin = $loadPlugin && !$isPluginHasGitRepository;
|
|
}
|
|
|
|
// Do not enable other Themes
|
|
$disabledThemes = $this->coreThemesDisabledByDefault;
|
|
|
|
// PleineLune is officially supported, yet we don't want to enable another theme in tests (we test for Morpheus)
|
|
$disabledThemes[] = "PleineLune";
|
|
|
|
$isThemeDisabled = in_array($plugin, $disabledThemes);
|
|
|
|
$loadPlugin = $loadPlugin && !$isThemeDisabled;
|
|
if($loadPlugin) {
|
|
$toLoad[] = $plugin;
|
|
}
|
|
}
|
|
return $toLoad;
|
|
}
|
|
|
|
public function getCorePluginsDisabledByDefault()
|
|
{
|
|
return array_merge( $this->corePluginsDisabledByDefault, $this->coreThemesDisabledByDefault);
|
|
}
|
|
|
|
// If a plugin hooks onto at least an event starting with "Tracker.", we load the plugin during tracker
|
|
const TRACKER_EVENT_PREFIX = 'Tracker.';
|
|
|
|
/**
|
|
* @param $pluginName
|
|
* @return bool
|
|
*/
|
|
public function isPluginOfficialAndNotBundledWithCore($pluginName)
|
|
{
|
|
static $gitModules;
|
|
if(empty($gitModules)) {
|
|
$gitModules = file_get_contents(PIWIK_INCLUDE_PATH . '/.gitmodules');
|
|
}
|
|
// All submodules are officially maintained plugins
|
|
$isSubmodule = false !== strpos($gitModules, "plugins/" . $pluginName . "\n");
|
|
return $isSubmodule;
|
|
}
|
|
|
|
/**
|
|
* Update Plugins config
|
|
*
|
|
* @param array $plugins Plugins
|
|
*/
|
|
private function updatePluginsConfig($pluginsToLoad)
|
|
{
|
|
$section = PiwikConfig::getInstance()->Plugins;
|
|
$section['Plugins'] = $pluginsToLoad;
|
|
PiwikConfig::getInstance()->Plugins = $section;
|
|
}
|
|
|
|
/**
|
|
* Update Plugins_Tracker config
|
|
*
|
|
* @param array $plugins Plugins
|
|
*/
|
|
private function updatePluginsTrackerConfig($plugins)
|
|
{
|
|
$section = PiwikConfig::getInstance()->Plugins_Tracker;
|
|
$section['Plugins_Tracker'] = $plugins;
|
|
PiwikConfig::getInstance()->Plugins_Tracker = $section;
|
|
}
|
|
|
|
/**
|
|
* Update PluginsInstalled config
|
|
*
|
|
* @param array $plugins Plugins
|
|
*/
|
|
private function updatePluginsInstalledConfig($plugins)
|
|
{
|
|
$section = PiwikConfig::getInstance()->PluginsInstalled;
|
|
$section['PluginsInstalled'] = $plugins;
|
|
PiwikConfig::getInstance()->PluginsInstalled = $section;
|
|
}
|
|
|
|
/**
|
|
* Returns true if plugin is always activated
|
|
*
|
|
* @param string $name Name of plugin
|
|
* @return bool
|
|
*/
|
|
private function isPluginAlwaysActivated($name)
|
|
{
|
|
return in_array($name, $this->pluginToAlwaysActivate);
|
|
}
|
|
|
|
/**
|
|
* Returns true if the plugin can be uninstalled. Any non-core plugin can be uninstalled.
|
|
*
|
|
* @param $name
|
|
* @return bool
|
|
*/
|
|
private function isPluginUninstallable($name)
|
|
{
|
|
return !$this->isPluginBundledWithCore($name);
|
|
}
|
|
|
|
/**
|
|
* Returns `true` if a plugin has been activated.
|
|
*
|
|
* @param string $name Name of plugin, eg, `'Actions'`.
|
|
* @return bool
|
|
* @api
|
|
*/
|
|
public function isPluginActivated($name)
|
|
{
|
|
return in_array($name, $this->pluginsToLoad)
|
|
|| $this->isPluginAlwaysActivated($name);
|
|
}
|
|
|
|
/**
|
|
* Returns `true` if plugin is loaded (in memory).
|
|
*
|
|
* @param string $name Name of plugin, eg, `'Acions'`.
|
|
* @return bool
|
|
* @api
|
|
*/
|
|
public function isPluginLoaded($name)
|
|
{
|
|
return isset($this->loadedPlugins[$name]);
|
|
}
|
|
|
|
/**
|
|
* Reads the directories inside the plugins/ directory and returns their names in an array
|
|
*
|
|
* @return array
|
|
*/
|
|
public function readPluginsDirectory()
|
|
{
|
|
$pluginsName = _glob(self::getPluginsDirectory() . '*', GLOB_ONLYDIR);
|
|
$result = array();
|
|
if ($pluginsName != false) {
|
|
foreach ($pluginsName as $path) {
|
|
if (self::pluginStructureLooksValid($path)) {
|
|
$result[] = basename($path);
|
|
}
|
|
}
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
public static function getPluginsDirectory()
|
|
{
|
|
return PIWIK_INCLUDE_PATH . '/plugins/';
|
|
}
|
|
|
|
/**
|
|
* Deactivate plugin
|
|
*
|
|
* @param string $pluginName Name of plugin
|
|
*/
|
|
public function deactivatePlugin($pluginName)
|
|
{
|
|
// execute deactivate() to let the plugin do cleanups
|
|
$this->executePluginDeactivate($pluginName);
|
|
|
|
$this->unloadPluginFromMemory($pluginName);
|
|
|
|
$this->removePluginFromConfig($pluginName);
|
|
|
|
$this->clearCache($pluginName);
|
|
}
|
|
|
|
/**
|
|
* Uninstalls a Plugin (deletes plugin files from the disk)
|
|
* Only deactivated plugins can be uninstalled
|
|
*
|
|
* @param $pluginName
|
|
* @throws \Exception
|
|
* @return bool
|
|
*/
|
|
public function uninstallPlugin($pluginName)
|
|
{
|
|
if ($this->isPluginLoaded($pluginName)) {
|
|
throw new \Exception("To uninstall the plugin $pluginName, first disable it in Piwik > Settings > Plugins");
|
|
}
|
|
$this->returnLoadedPluginsInfo();
|
|
|
|
$this->executePluginDeactivate($pluginName);
|
|
$this->executePluginUninstall($pluginName);
|
|
|
|
$this->removePluginFromPluginsInstalledConfig($pluginName);
|
|
|
|
$this->unloadPluginFromMemory($pluginName);
|
|
|
|
$this->removePluginFromConfig($pluginName);
|
|
|
|
Option::delete('version_' . $pluginName);
|
|
\Piwik\Settings\Manager::cleanupPluginSettings($pluginName);
|
|
$this->clearCache($pluginName);
|
|
|
|
self::deletePluginFromFilesystem($pluginName);
|
|
if ($this->isPluginInFilesystem($pluginName)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param string $pluginName
|
|
*/
|
|
private function clearCache($pluginName)
|
|
{
|
|
Filesystem::deleteAllCacheOnUpdate($pluginName);
|
|
}
|
|
|
|
public static function deletePluginFromFilesystem($plugin)
|
|
{
|
|
Filesystem::unlinkRecursive(PIWIK_INCLUDE_PATH . '/plugins/' . $plugin, $deleteRootToo = true);
|
|
}
|
|
|
|
/**
|
|
* Install loaded plugins
|
|
*
|
|
* @return array Error messages of plugin install fails
|
|
*/
|
|
public function installLoadedPlugins()
|
|
{
|
|
$messages = array();
|
|
foreach ($this->getLoadedPlugins() as $plugin) {
|
|
try {
|
|
$this->installPluginIfNecessary($plugin);
|
|
} catch (\Exception $e) {
|
|
$messages[] = $e->getMessage();
|
|
}
|
|
}
|
|
return $messages;
|
|
}
|
|
|
|
/**
|
|
* Activate the specified plugin and install (if needed)
|
|
*
|
|
* @param string $pluginName Name of plugin
|
|
* @throws \Exception
|
|
*/
|
|
public function activatePlugin($pluginName)
|
|
{
|
|
$plugins = PiwikConfig::getInstance()->Plugins['Plugins'];
|
|
if (in_array($pluginName, $plugins)) {
|
|
throw new \Exception("Plugin '$pluginName' already activated.");
|
|
}
|
|
|
|
if (!$this->isPluginInFilesystem($pluginName)) {
|
|
throw new \Exception("Plugin '$pluginName' cannot be found in the filesystem in plugins/ directory.");
|
|
}
|
|
$this->deactivateThemeIfTheme($pluginName);
|
|
|
|
// Load plugin
|
|
$plugin = $this->loadPlugin($pluginName);
|
|
if ($plugin === null) {
|
|
throw new \Exception("The plugin '$pluginName' was found in the filesystem, but could not be loaded.'");
|
|
}
|
|
$this->installPluginIfNecessary($plugin);
|
|
$plugin->activate();
|
|
|
|
EventDispatcher::getInstance()->postPendingEventsTo($plugin);
|
|
|
|
$this->pluginsToLoad[] = $pluginName;
|
|
|
|
$this->updatePluginsConfig($this->pluginsToLoad);
|
|
PiwikConfig::getInstance()->forceSave();
|
|
|
|
$this->clearCache($pluginName);
|
|
|
|
}
|
|
|
|
protected function isPluginInFilesystem($pluginName)
|
|
{
|
|
$existingPlugins = $this->readPluginsDirectory();
|
|
$isPluginInFilesystem = array_search($pluginName, $existingPlugins) !== false;
|
|
return Filesystem::isValidFilename($pluginName)
|
|
&& $isPluginInFilesystem;
|
|
}
|
|
|
|
/**
|
|
* Returns the currently enabled theme.
|
|
*
|
|
* If no theme is enabled, the **Zeitgeist** plugin is returned (this is the base and default theme).
|
|
*
|
|
* @return Plugin
|
|
* @api
|
|
*/
|
|
public function getThemeEnabled()
|
|
{
|
|
$plugins = $this->getLoadedPlugins();
|
|
|
|
$theme = false;
|
|
foreach ($plugins as $plugin) {
|
|
/* @var $plugin Plugin */
|
|
if ($plugin->isTheme()
|
|
&& $this->isPluginActivated($plugin->getPluginName())
|
|
) {
|
|
if ($plugin->getPluginName() != self::DEFAULT_THEME) {
|
|
return $plugin; // enabled theme (not default)
|
|
}
|
|
$theme = $plugin; // default theme
|
|
}
|
|
}
|
|
return $theme;
|
|
}
|
|
|
|
/**
|
|
* @param string $themeName
|
|
* @throws \Exception
|
|
* @return Theme
|
|
*/
|
|
public function getTheme($themeName)
|
|
{
|
|
$plugins = $this->getLoadedPlugins();
|
|
|
|
foreach ($plugins as $plugin) {
|
|
if ($plugin->isTheme() && $plugin->getPluginName() == $themeName) {
|
|
return new Theme($plugin);
|
|
}
|
|
}
|
|
throw new \Exception('Theme not found : ' . $themeName);
|
|
}
|
|
|
|
public function getNumberOfActivatedPlugins()
|
|
{
|
|
$counter = 0;
|
|
|
|
$pluginNames = $this->getLoadedPluginsName();
|
|
foreach ($pluginNames as $pluginName) {
|
|
if ($this->isPluginActivated($pluginName)) {
|
|
$counter++;
|
|
}
|
|
}
|
|
|
|
return $counter;
|
|
}
|
|
|
|
/**
|
|
* Returns info regarding all plugins. Loads plugins that can be loaded.
|
|
*
|
|
* @return array An array that maps plugin names with arrays of plugin information. Plugin
|
|
* information consists of the following entries:
|
|
*
|
|
* - **activated**: Whether the plugin is activated.
|
|
* - **alwaysActivated**: Whether the plugin should always be activated,
|
|
* or not.
|
|
* - **uninstallable**: Whether the plugin is uninstallable or not.
|
|
* - **invalid**: If the plugin is invalid, this property will be set to true.
|
|
* If the plugin is not invalid, this property will not exist.
|
|
* - **info**: If the plugin was loaded, will hold the plugin information.
|
|
* See {@link Piwik\Plugin::getInformation()}.
|
|
* @api
|
|
*/
|
|
public function returnLoadedPluginsInfo()
|
|
{
|
|
$language = Translate::getLanguageToLoad();
|
|
|
|
$plugins = array();
|
|
|
|
$listPlugins = array_merge(
|
|
$this->readPluginsDirectory(),
|
|
PiwikConfig::getInstance()->Plugins['Plugins']
|
|
);
|
|
$listPlugins = array_unique($listPlugins);
|
|
foreach ($listPlugins as $pluginName) {
|
|
// Hide plugins that are never going to be used
|
|
if($this->isPluginBogus($pluginName)) {
|
|
continue;
|
|
}
|
|
|
|
// If the plugin is not core and looks bogus, do not load
|
|
if ($this->isPluginThirdPartyAndBogus($pluginName)) {
|
|
$info = array(
|
|
'invalid' => true,
|
|
'activated' => false,
|
|
'alwaysActivated' => false,
|
|
'uninstallable' => true,
|
|
);
|
|
} else {
|
|
$this->loadTranslation($pluginName, $language);
|
|
$this->loadPlugin($pluginName);
|
|
$info = array(
|
|
'activated' => $this->isPluginActivated($pluginName),
|
|
'alwaysActivated' => $this->isPluginAlwaysActivated($pluginName),
|
|
'uninstallable' => $this->isPluginUninstallable($pluginName),
|
|
);
|
|
}
|
|
|
|
$plugins[$pluginName] = $info;
|
|
}
|
|
$this->loadPluginTranslations();
|
|
|
|
$loadedPlugins = $this->getLoadedPlugins();
|
|
foreach ($loadedPlugins as $oPlugin) {
|
|
$pluginName = $oPlugin->getPluginName();
|
|
|
|
$info = array(
|
|
'info' => $oPlugin->getInformation(),
|
|
'activated' => $this->isPluginActivated($pluginName),
|
|
'alwaysActivated' => $this->isPluginAlwaysActivated($pluginName),
|
|
'missingRequirements' => $oPlugin->getMissingDependencies(),
|
|
'uninstallable' => $this->isPluginUninstallable($pluginName),
|
|
);
|
|
$plugins[$pluginName] = $info;
|
|
}
|
|
return $plugins;
|
|
}
|
|
|
|
|
|
protected static function isManifestFileFound($path)
|
|
{
|
|
return file_exists($path . "/" . MetadataLoader::PLUGIN_JSON_FILENAME);
|
|
}
|
|
|
|
/**
|
|
* Returns `true` if the plugin is bundled with core or `false` if it is third party.
|
|
*
|
|
* @param string $name The name of the plugin, eg, `'Actions'`.
|
|
* @return bool
|
|
*/
|
|
public function isPluginBundledWithCore($name)
|
|
{
|
|
// Reading the plugins from the global.ini.php config file
|
|
$pluginsBundledWithPiwik = PiwikConfig::getInstance()->getFromGlobalConfig('Plugins');
|
|
$pluginsBundledWithPiwik = $pluginsBundledWithPiwik['Plugins'];
|
|
|
|
return (!empty($pluginsBundledWithPiwik)
|
|
&& in_array($name, $pluginsBundledWithPiwik))
|
|
|| in_array($name, $this->getCorePluginsDisabledByDefault())
|
|
|| $name == self::DEFAULT_THEME;
|
|
}
|
|
|
|
protected function isPluginThirdPartyAndBogus($pluginName)
|
|
{
|
|
if($this->isPluginBundledWithCore($pluginName)) {
|
|
return false;
|
|
}
|
|
if($this->isPluginBogus($pluginName)) {
|
|
return true;
|
|
}
|
|
|
|
$path = $this->getPluginsDirectory() . $pluginName;
|
|
if(!$this->isManifestFileFound($path)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Load the specified plugins.
|
|
*
|
|
* @param array $pluginsToLoad Array of plugins to load.
|
|
*/
|
|
public function loadPlugins(array $pluginsToLoad)
|
|
{
|
|
$pluginsToLoad = array_unique($pluginsToLoad);
|
|
$this->pluginsToLoad = $pluginsToLoad;
|
|
$this->reloadPlugins();
|
|
}
|
|
|
|
/**
|
|
* Disable plugin loading.
|
|
*/
|
|
public function doNotLoadPlugins()
|
|
{
|
|
$this->doLoadPlugins = false;
|
|
}
|
|
|
|
/**
|
|
* Disable loading of "always activated" plugins.
|
|
*/
|
|
public function doNotLoadAlwaysActivatedPlugins()
|
|
{
|
|
$this->doLoadAlwaysActivatedPlugins = false;
|
|
}
|
|
|
|
/**
|
|
* Load translations for loaded plugins
|
|
*
|
|
* @param bool|string $language Optional language code
|
|
*/
|
|
public function loadPluginTranslations($language = false)
|
|
{
|
|
if (empty($language)) {
|
|
$language = Translate::getLanguageToLoad();
|
|
}
|
|
$plugins = $this->getLoadedPlugins();
|
|
|
|
foreach ($plugins as $plugin) {
|
|
$this->loadTranslation($plugin, $language);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute postLoad() hook for loaded plugins
|
|
*/
|
|
public function postLoadPlugins()
|
|
{
|
|
$plugins = $this->getLoadedPlugins();
|
|
foreach ($plugins as $plugin) {
|
|
$plugin->postLoad();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns an array containing the plugins class names (eg. 'UserCountry' and NOT 'UserCountry')
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getLoadedPluginsName()
|
|
{
|
|
return array_keys($this->getLoadedPlugins());
|
|
}
|
|
|
|
/**
|
|
* Returns an array mapping loaded plugin names with their plugin objects, eg,
|
|
*
|
|
* array(
|
|
* 'UserCountry' => Plugin $pluginObject,
|
|
* 'UserSettings' => Plugin $pluginObject,
|
|
* );
|
|
*
|
|
* @return Plugin[]
|
|
*/
|
|
public function getLoadedPlugins()
|
|
{
|
|
return $this->loadedPlugins;
|
|
}
|
|
|
|
/**
|
|
* @param string $piwikVersion
|
|
* @return Plugin[]
|
|
*/
|
|
public function getIncompatiblePlugins($piwikVersion)
|
|
{
|
|
$plugins = $this->getLoadedPlugins();
|
|
|
|
$incompatible = array();
|
|
foreach ($plugins as $plugin) {
|
|
if ($plugin->hasMissingDependencies($piwikVersion)) {
|
|
$incompatible[] = $plugin;
|
|
}
|
|
}
|
|
|
|
return $incompatible;
|
|
}
|
|
|
|
/**
|
|
* Returns an array of plugins that are currently loaded and activated,
|
|
* mapping loaded plugin names with their plugin objects, eg,
|
|
*
|
|
* array(
|
|
* 'UserCountry' => Plugin $pluginObject,
|
|
* 'UserSettings' => Plugin $pluginObject,
|
|
* );
|
|
*
|
|
* @return Plugin[]
|
|
*/
|
|
public function getPluginsLoadedAndActivated()
|
|
{
|
|
$plugins = $this->getLoadedPlugins();
|
|
$enabled = $this->getActivatedPlugins();
|
|
|
|
if(empty($enabled)) {
|
|
return array();
|
|
}
|
|
$enabled = array_combine($enabled, $enabled);
|
|
$plugins = array_intersect_key($plugins, $enabled);
|
|
return $plugins;
|
|
}
|
|
|
|
/**
|
|
* Returns a list of all names of currently activated plugin eg,
|
|
*
|
|
* array(
|
|
* 'UserCountry'
|
|
* 'UserSettings'
|
|
* );
|
|
*
|
|
* @return string[]
|
|
*/
|
|
public function getActivatedPlugins()
|
|
{
|
|
return $this->pluginsToLoad;
|
|
}
|
|
|
|
/**
|
|
* Returns a Plugin object by name.
|
|
*
|
|
* @param string $name The name of the plugin, eg, `'Actions'`.
|
|
* @throws \Exception If the plugin has not been loaded.
|
|
* @return Plugin
|
|
*/
|
|
public function getLoadedPlugin($name)
|
|
{
|
|
if (!isset($this->loadedPlugins[$name])) {
|
|
throw new \Exception("The plugin '$name' has not been loaded.");
|
|
}
|
|
return $this->loadedPlugins[$name];
|
|
}
|
|
|
|
/**
|
|
* Load the plugins classes installed.
|
|
* Register the observers for every plugin.
|
|
*/
|
|
private function reloadPlugins()
|
|
{
|
|
if ($this->doLoadAlwaysActivatedPlugins) {
|
|
$this->pluginsToLoad = array_merge($this->pluginsToLoad, $this->pluginToAlwaysActivate);
|
|
}
|
|
|
|
$this->pluginsToLoad = array_unique($this->pluginsToLoad);
|
|
|
|
$pluginsToPostPendingEventsTo = array();
|
|
foreach ($this->pluginsToLoad as $pluginName) {
|
|
if (!$this->isPluginLoaded($pluginName)
|
|
&& !$this->isPluginThirdPartyAndBogus($pluginName)
|
|
) {
|
|
$newPlugin = $this->loadPlugin($pluginName);
|
|
if ($newPlugin === null) {
|
|
continue;
|
|
}
|
|
|
|
if ($newPlugin->hasMissingDependencies()) {
|
|
$this->deactivatePlugin($pluginName);
|
|
continue;
|
|
}
|
|
|
|
$pluginsToPostPendingEventsTo[] = $newPlugin;
|
|
}
|
|
}
|
|
|
|
// post pending events after all plugins are successfully loaded
|
|
foreach ($pluginsToPostPendingEventsTo as $plugin) {
|
|
EventDispatcher::getInstance()->postPendingEventsTo($plugin);
|
|
}
|
|
}
|
|
|
|
public function getIgnoredBogusPlugins()
|
|
{
|
|
$ignored = array();
|
|
foreach ($this->pluginsToLoad as $pluginName) {
|
|
if ($this->isPluginThirdPartyAndBogus($pluginName)) {
|
|
$ignored[] = $pluginName;
|
|
}
|
|
}
|
|
return $ignored;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the name of all plugins found in this Piwik instance
|
|
* (including those not enabled and themes)
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function getAllPluginsNames()
|
|
{
|
|
$pluginsToLoad = array_merge(
|
|
PiwikConfig::getInstance()->Plugins['Plugins'],
|
|
self::getInstance()->readPluginsDirectory(),
|
|
self::getInstance()->getCorePluginsDisabledByDefault()
|
|
);
|
|
$pluginsToLoad = array_values(array_unique($pluginsToLoad));
|
|
return $pluginsToLoad;
|
|
}
|
|
|
|
|
|
/**
|
|
* Loads the plugin filename and instantiates the plugin with the given name, eg. UserCountry
|
|
*
|
|
* @param string $pluginName
|
|
* @throws \Exception
|
|
* @return Plugin|null
|
|
*/
|
|
public function loadPlugin($pluginName)
|
|
{
|
|
if (isset($this->loadedPlugins[$pluginName])) {
|
|
return $this->loadedPlugins[$pluginName];
|
|
}
|
|
$newPlugin = $this->makePluginClass($pluginName);
|
|
|
|
$this->addLoadedPlugin($pluginName, $newPlugin);
|
|
return $newPlugin;
|
|
}
|
|
|
|
/**
|
|
* @param $pluginName
|
|
* @return Plugin
|
|
* @throws \Exception
|
|
*/
|
|
protected function makePluginClass($pluginName)
|
|
{
|
|
$pluginFileName = sprintf("%s/%s.php", $pluginName, $pluginName);
|
|
$pluginClassName = $pluginName;
|
|
|
|
if (!Filesystem::isValidFilename($pluginName)) {
|
|
throw new \Exception(sprintf("The plugin filename '%s' is not a valid filename", $pluginFileName));
|
|
}
|
|
|
|
$path = self::getPluginsDirectory() . $pluginFileName;
|
|
|
|
|
|
if (!file_exists($path)) {
|
|
// Create the smallest minimal Piwik Plugin
|
|
// Eg. Used for Zeitgeist default theme which does not have a Zeitgeist.php file
|
|
return new Plugin($pluginName);
|
|
}
|
|
|
|
require_once $path;
|
|
|
|
$namespacedClass = $this->getClassNamePlugin($pluginName);
|
|
if (!class_exists($namespacedClass, false)) {
|
|
throw new \Exception("The class $pluginClassName couldn't be found in the file '$path'");
|
|
}
|
|
$newPlugin = new $namespacedClass;
|
|
|
|
if (!($newPlugin instanceof Plugin)) {
|
|
throw new \Exception("The plugin $pluginClassName in the file $path must inherit from Plugin.");
|
|
}
|
|
return $newPlugin;
|
|
}
|
|
|
|
protected function getClassNamePlugin($pluginName)
|
|
{
|
|
$className = $pluginName;
|
|
if ($pluginName == 'API') {
|
|
$className = 'Plugin';
|
|
}
|
|
return "\\Piwik\\Plugins\\$pluginName\\$className";
|
|
}
|
|
|
|
/**
|
|
* Unload plugin
|
|
*
|
|
* @param Plugin|string $plugin
|
|
* @throws \Exception
|
|
*/
|
|
public function unloadPlugin($plugin)
|
|
{
|
|
if (!($plugin instanceof Plugin)) {
|
|
$oPlugin = $this->loadPlugin($plugin);
|
|
if ($oPlugin === null) {
|
|
unset($this->loadedPlugins[$plugin]);
|
|
return;
|
|
}
|
|
|
|
$plugin = $oPlugin;
|
|
}
|
|
|
|
unset($this->loadedPlugins[$plugin->getPluginName()]);
|
|
}
|
|
|
|
/**
|
|
* Unload all loaded plugins
|
|
*/
|
|
public function unloadPlugins()
|
|
{
|
|
$pluginsLoaded = $this->getLoadedPlugins();
|
|
foreach ($pluginsLoaded as $plugin) {
|
|
$this->unloadPlugin($plugin);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install a specific plugin
|
|
*
|
|
* @param Plugin $plugin
|
|
* @throws \Piwik\Plugin\PluginException if installation fails
|
|
*/
|
|
private function executePluginInstall(Plugin $plugin)
|
|
{
|
|
try {
|
|
$plugin->install();
|
|
} catch (\Exception $e) {
|
|
throw new \Piwik\Plugin\PluginException($plugin->getPluginName(), $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a plugin in the loaded plugins array
|
|
*
|
|
* @param string $pluginName plugin name without prefix (eg. 'UserCountry')
|
|
* @param Plugin $newPlugin
|
|
*/
|
|
private function addLoadedPlugin($pluginName, Plugin $newPlugin)
|
|
{
|
|
$this->loadedPlugins[$pluginName] = $newPlugin;
|
|
}
|
|
|
|
/**
|
|
* Load translation
|
|
*
|
|
* @param Plugin $plugin
|
|
* @param string $langCode
|
|
* @throws \Exception
|
|
* @return bool whether the translation was found and loaded
|
|
*/
|
|
private function loadTranslation($plugin, $langCode)
|
|
{
|
|
// we are in Tracker mode if Loader is not (yet) loaded
|
|
if (!class_exists('Piwik\\Loader', false)) {
|
|
return false;
|
|
}
|
|
|
|
if (is_string($plugin)) {
|
|
$pluginName = $plugin;
|
|
} else {
|
|
$pluginName = $plugin->getPluginName();
|
|
}
|
|
|
|
$path = self::getPluginsDirectory() . $pluginName . '/lang/%s.json';
|
|
|
|
$defaultLangPath = sprintf($path, $langCode);
|
|
$defaultEnglishLangPath = sprintf($path, 'en');
|
|
|
|
$translationsLoaded = false;
|
|
|
|
// merge in english translations as default first
|
|
if (file_exists($defaultEnglishLangPath)) {
|
|
$translations = $this->getTranslationsFromFile($defaultEnglishLangPath);
|
|
$translationsLoaded = true;
|
|
if (isset($translations[$pluginName])) {
|
|
// only merge translations of plugin - prevents overwritten strings
|
|
Translate::mergeTranslationArray(array($pluginName => $translations[$pluginName]));
|
|
}
|
|
}
|
|
|
|
// merge in specific language translations (to overwrite english defaults)
|
|
if (file_exists($defaultLangPath)) {
|
|
$translations = $this->getTranslationsFromFile($defaultLangPath);
|
|
$translationsLoaded = true;
|
|
if (isset($translations[$pluginName])) {
|
|
// only merge translations of plugin - prevents overwritten strings
|
|
Translate::mergeTranslationArray(array($pluginName => $translations[$pluginName]));
|
|
}
|
|
}
|
|
|
|
return $translationsLoaded;
|
|
}
|
|
|
|
/**
|
|
* Return names of all installed plugins.
|
|
*
|
|
* @return array
|
|
* @api
|
|
*/
|
|
public function getInstalledPluginsName()
|
|
{
|
|
$pluginNames = PiwikConfig::getInstance()->PluginsInstalled['PluginsInstalled'];
|
|
return $pluginNames;
|
|
}
|
|
|
|
/**
|
|
* Returns names of plugins that should be loaded, but cannot be since their
|
|
* files cannot be found.
|
|
*
|
|
* @return array
|
|
* @api
|
|
*/
|
|
public function getMissingPlugins()
|
|
{
|
|
$missingPlugins = array();
|
|
if (isset(PiwikConfig::getInstance()->Plugins['Plugins'])) {
|
|
$plugins = PiwikConfig::getInstance()->Plugins['Plugins'];
|
|
foreach ($plugins as $pluginName) {
|
|
// if a plugin is listed in the config, but is not loaded, it does not exist in the folder
|
|
if (!self::getInstance()->isPluginLoaded($pluginName)
|
|
&& !$this->isPluginBogus($pluginName)
|
|
) {
|
|
$missingPlugins[] = $pluginName;
|
|
}
|
|
}
|
|
}
|
|
return $missingPlugins;
|
|
}
|
|
|
|
/**
|
|
* Install a plugin, if necessary
|
|
*
|
|
* @param Plugin $plugin
|
|
*/
|
|
private function installPluginIfNecessary(Plugin $plugin)
|
|
{
|
|
$pluginName = $plugin->getPluginName();
|
|
$saveConfig = false;
|
|
|
|
// is the plugin already installed or is it the first time we activate it?
|
|
$pluginsInstalled = $this->getInstalledPluginsName();
|
|
|
|
if (!$this->isPluginInstalled($pluginName)) {
|
|
$this->executePluginInstall($plugin);
|
|
$pluginsInstalled[] = $pluginName;
|
|
$this->updatePluginsInstalledConfig($pluginsInstalled);
|
|
Updater::recordComponentSuccessfullyUpdated($plugin->getPluginName(), $plugin->getVersion());
|
|
$saveConfig = true;
|
|
}
|
|
|
|
if ($this->isTrackerPlugin($plugin)) {
|
|
$pluginsTracker = PiwikConfig::getInstance()->Plugins_Tracker['Plugins_Tracker'];
|
|
if (is_null($pluginsTracker)) {
|
|
$pluginsTracker = array();
|
|
}
|
|
if (!in_array($pluginName, $pluginsTracker)) {
|
|
$pluginsTracker[] = $pluginName;
|
|
$this->updatePluginsTrackerConfig($pluginsTracker);
|
|
$saveConfig = true;
|
|
}
|
|
}
|
|
|
|
if ($saveConfig) {
|
|
PiwikConfig::getInstance()->forceSave();
|
|
}
|
|
}
|
|
|
|
public function isTrackerPlugin(Plugin $plugin)
|
|
{
|
|
$hooks = $plugin->getListHooksRegistered();
|
|
$hookNames = array_keys($hooks);
|
|
foreach ($hookNames as $name) {
|
|
if (strpos($name, self::TRACKER_EVENT_PREFIX) === 0) {
|
|
return true;
|
|
}
|
|
if ($name === 'Request.initAuthenticationObject') {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static function pluginStructureLooksValid($path)
|
|
{
|
|
$name = basename($path);
|
|
return file_exists($path . "/" . $name . ".php")
|
|
|| self::isManifestFileFound($path);
|
|
}
|
|
|
|
/**
|
|
* @param $pluginName
|
|
*/
|
|
private function removePluginFromPluginsInstalledConfig($pluginName)
|
|
{
|
|
$pluginsInstalled = PiwikConfig::getInstance()->PluginsInstalled['PluginsInstalled'];
|
|
$key = array_search($pluginName, $pluginsInstalled);
|
|
if ($key !== false) {
|
|
unset($pluginsInstalled[$key]);
|
|
}
|
|
|
|
$this->updatePluginsInstalledConfig($pluginsInstalled);
|
|
}
|
|
|
|
/**
|
|
* @param $pluginName
|
|
*/
|
|
private function removePluginFromPluginsConfig($pluginName)
|
|
{
|
|
$pluginsEnabled = PiwikConfig::getInstance()->Plugins['Plugins'];
|
|
$key = array_search($pluginName, $pluginsEnabled);
|
|
if ($key !== false) {
|
|
unset($pluginsEnabled[$key]);
|
|
}
|
|
$this->updatePluginsConfig($pluginsEnabled);
|
|
}
|
|
|
|
private function removePluginFromTrackerConfig($pluginName)
|
|
{
|
|
$pluginsTracker = PiwikConfig::getInstance()->Plugins_Tracker['Plugins_Tracker'];
|
|
if (!is_null($pluginsTracker)) {
|
|
$key = array_search($pluginName, $pluginsTracker);
|
|
if ($key !== false) {
|
|
unset($pluginsTracker[$key]);
|
|
$this->updatePluginsTrackerConfig($pluginsTracker);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $pathToTranslationFile
|
|
* @throws \Exception
|
|
* @return mixed
|
|
*/
|
|
private function getTranslationsFromFile($pathToTranslationFile)
|
|
{
|
|
$data = file_get_contents($pathToTranslationFile);
|
|
$translations = json_decode($data, true);
|
|
|
|
if (is_null($translations) && Common::hasJsonErrorOccurred()) {
|
|
$jsonError = Common::getLastJsonError();
|
|
|
|
$message = sprintf('Not able to load translation file %s: %s', $pathToTranslationFile, $jsonError);
|
|
|
|
throw new \Exception($message);
|
|
}
|
|
|
|
return $translations;
|
|
}
|
|
|
|
/**
|
|
* @param $pluginName
|
|
* @return bool
|
|
*/
|
|
private function isPluginBogus($pluginName)
|
|
{
|
|
$bogusPlugins = array(
|
|
'PluginMarketplace', //defines a plugin.json but 1.x Piwik plugin
|
|
'DoNotTrack', // Removed in 2.0.3
|
|
'AnonymizeIP', // Removed in 2.0.3
|
|
);
|
|
return in_array($pluginName, $bogusPlugins);
|
|
}
|
|
|
|
private function deactivateThemeIfTheme($pluginName)
|
|
{
|
|
// Only one theme enabled at a time
|
|
$themeEnabled = $this->getThemeEnabled();
|
|
if ($themeEnabled
|
|
&& $themeEnabled->getPluginName() != self::DEFAULT_THEME) {
|
|
$themeAlreadyEnabled = $themeEnabled->getPluginName();
|
|
|
|
$plugin = $this->loadPlugin($pluginName);
|
|
if ($plugin->isTheme()) {
|
|
$this->deactivatePlugin($themeAlreadyEnabled);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param $pluginName
|
|
*/
|
|
private function executePluginDeactivate($pluginName)
|
|
{
|
|
if (!$this->isPluginBogus($pluginName)) {
|
|
$plugin = $this->loadPlugin($pluginName);
|
|
if ($plugin !== null) {
|
|
$plugin->deactivate();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param $pluginName
|
|
*/
|
|
private function unloadPluginFromMemory($pluginName)
|
|
{
|
|
$key = array_search($pluginName, $this->pluginsToLoad);
|
|
if ($key !== false) {
|
|
unset($this->pluginsToLoad[$key]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param $pluginName
|
|
*/
|
|
private function removePluginFromConfig($pluginName)
|
|
{
|
|
$this->removePluginFromPluginsConfig($pluginName);
|
|
$this->removePluginFromTrackerConfig($pluginName);
|
|
PiwikConfig::getInstance()->forceSave();
|
|
}
|
|
|
|
/**
|
|
* @param $pluginName
|
|
*/
|
|
private function executePluginUninstall($pluginName)
|
|
{
|
|
try {
|
|
$plugin = $this->getLoadedPlugin($pluginName);
|
|
$plugin->uninstall();
|
|
} catch (\Exception $e) {
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param $pluginName
|
|
* @return bool
|
|
*/
|
|
public function isPluginInstalled($pluginName)
|
|
{
|
|
$pluginsInstalled = $this->getInstalledPluginsName();
|
|
return in_array($pluginName, $pluginsInstalled);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*/
|
|
class PluginException extends \Exception
|
|
{
|
|
function __construct($pluginName, $message)
|
|
{
|
|
parent::__construct("There was a problem installing the plugin " . $pluginName . ": " . $message . "
|
|
If this plugin has already been installed, and if you want to hide this message</b>, you must add the following line under the
|
|
[PluginsInstalled]
|
|
entry in your config/config.ini.php file:
|
|
PluginsInstalled[] = $pluginName");
|
|
}
|
|
}
|