diff --git a/modules/system/classes/PluginBase.php b/modules/system/classes/PluginBase.php
index 30af4fbf88..d07d18db02 100644
--- a/modules/system/classes/PluginBase.php
+++ b/modules/system/classes/PluginBase.php
@@ -326,7 +326,7 @@ protected function getConfigurationFromYaml($exceptionMessage = null)
$this->loadedYamlConfiguration = [];
}
else {
- $this->loadedYamlConfiguration = Yaml::parse(file_get_contents($yamlFilePath));
+ $this->loadedYamlConfiguration = Yaml::parseFile($yamlFilePath);
if (!is_array($this->loadedYamlConfiguration)) {
throw new SystemException(sprintf('Invalid format of the plugin configuration file: %s. The file should define an array.', $yamlFilePath));
}
@@ -404,8 +404,6 @@ public function getPluginIdentifier(): string
/**
* Returns the absolute path to this plugin's directory
- *
- * @return string
*/
public function getPluginPath(): string
{
@@ -414,7 +412,7 @@ public function getPluginPath(): string
}
$reflection = new ReflectionClass($this);
- $this->path = dirname($reflection->getFileName());
+ $this->path = File::normalizePath(dirname($reflection->getFileName()));
return $this->path;
}
@@ -435,7 +433,7 @@ public function getPluginVersion(): string
if (
!File::isFile($versionFile)
|| !($versionInfo = Yaml::withProcessor(new VersionYamlProcessor, function ($yaml) use ($versionFile) {
- return $yaml->parse(file_get_contents($versionFile));
+ return $yaml->parseFile($versionFile);
}))
|| !is_array($versionInfo)
) {
@@ -448,4 +446,25 @@ public function getPluginVersion(): string
return $this->version = trim(key(array_slice($versionInfo, -1, 1)));
}
+
+ /**
+ * Verifies the plugin's dependencies are present and enabled
+ */
+ public function checkDependencies(PluginManager $manager): bool
+ {
+ $required = $manager->getDependencies($this);
+ if (empty($required)) {
+ return true;
+ }
+
+ foreach ($required as $require) {
+ $requiredPlugin = $manager->findByIdentifier($require);
+
+ if (!$requiredPlugin || $manager->isDisabled($requiredPlugin)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
}
diff --git a/modules/system/classes/PluginManager.php b/modules/system/classes/PluginManager.php
index 6ca9d0a65b..f2ba9cdbbc 100644
--- a/modules/system/classes/PluginManager.php
+++ b/modules/system/classes/PluginManager.php
@@ -7,12 +7,15 @@
use File;
use Lang;
use View;
+use Cache;
use Config;
use Schema;
-use Storage;
use SystemException;
+use FilesystemIterator;
use RecursiveIteratorIterator;
use RecursiveDirectoryIterator;
+use System\Models\PluginVersion;
+use Winter\Storm\Foundation\Application;
use Winter\Storm\Support\ClassLoader;
use Backend\Classes\NavigationManager;
@@ -26,20 +29,42 @@ class PluginManager
{
use \Winter\Storm\Support\Traits\Singleton;
+ //
+ // Disabled by system
+ //
+
+ public const DISABLED_MISSING = 'disabled-missing';
+ public const DISABLED_REPLACED = 'disabled-replaced';
+ public const DISABLED_REPLACEMENT_FAILED = 'disabled-replacement-failed';
+ public const DISABLED_MISSING_DEPENDENCIES = 'disabled-dependencies';
+
+ //
+ // Explicitly disabled for a reason
+ //
+
+ public const DISABLED_REQUEST = 'disabled-request';
+ public const DISABLED_BY_USER = 'disabled-user';
+ public const DISABLED_BY_CONFIG = 'disabled-config';
+
/**
* The application instance, since Plugins are an extension of a Service Provider
*/
- protected $app;
+ protected Application $app;
+
+ /**
+ * @var PluginBase[] Container array used for storing plugin information objects.
+ */
+ protected $plugins = [];
/**
- * @var array Container array used for storing plugin information objects.
+ * @var array Array of plugin codes that contain any flags currently associated with the plugin
*/
- protected $plugins;
+ protected $pluginFlags = [];
/**
- * @var array A map of plugins and their directory paths.
+ * @var PluginVersion[] Local cache of loaded PluginVersion records keyed by plugin code
*/
- protected $pathMap = [];
+ protected $pluginRecords = [];
/**
* @var array A map of normalized plugin identifiers [lowercase.identifier => Normalized.Identifier]
@@ -66,16 +91,6 @@ class PluginManager
*/
protected $booted = false;
- /**
- * @var string Path to the JSON encoded file containing the disabled plugins.
- */
- protected $metaFile;
-
- /**
- * @var array Array of disabled plugins
- */
- protected $disabledPlugins = [];
-
/**
* @var array Cache of registration method results.
*/
@@ -89,36 +104,25 @@ class PluginManager
/**
* Initializes the plugin manager
*/
- protected function init()
+ protected function init(): void
{
- $this->bindContainerObjects();
- $this->metaFile = 'cms/disabled.json';
- $this->loadDisabled();
- $this->loadPlugins();
+ $this->app = App::make('app');
- if ($this->app->runningInBackend()) {
- $this->loadDependencies();
- }
+ // Load the plugins from the filesystem and sort them by dependencies
+ $this->loadPlugins();
- $this->registerReplacedPlugins();
- }
+ // Loads the plugin flags (disabled & replacement states) from the cache
+ // regenerating them if required.
+ $this->loadPluginFlags();
- /**
- * These objects are "soft singletons" and may be lost when
- * the IoC container reboots. This provides a way to rebuild
- * for the purposes of unit testing.
- */
- public function bindContainerObjects()
- {
- $this->app = App::make('app');
+ // Register plugin replacements
+ $this->registerPluginReplacements();
}
/**
* Finds all available plugins and loads them in to the $this->plugins array.
- *
- * @return array
*/
- public function loadPlugins()
+ public function loadPlugins(): array
{
$this->plugins = [];
@@ -129,7 +133,8 @@ public function loadPlugins()
$this->loadPlugin($namespace, $path);
}
- $this->sortDependencies();
+ // Sort all the plugins by number of dependencies
+ $this->sortByDependencies();
return $this->plugins;
}
@@ -139,9 +144,8 @@ public function loadPlugins()
*
* @param string $namespace Eg: Acme\Blog
* @param string $path Eg: plugins_path().'/acme/blog';
- * @return void
*/
- public function loadPlugin($namespace, $path)
+ public function loadPlugin(string $namespace, string $path): ?PluginBase
{
$className = $namespace . '\Plugin';
$classPath = $path . '/Plugin.php';
@@ -154,10 +158,10 @@ public function loadPlugin($namespace, $path)
// Not a valid plugin!
if (!class_exists($className)) {
- return;
+ return null;
}
- $classObj = new $className($this->app);
+ $pluginObj = new $className($this->app);
} catch (\Throwable $e) {
Log::error('Plugin ' . $className . ' could not be instantiated.', [
'message' => $e->getMessage(),
@@ -165,39 +169,80 @@ public function loadPlugin($namespace, $path)
'line' => $e->getLine(),
'trace' => $e->getTraceAsString()
]);
- return;
+ return null;
}
- $classId = $this->getIdentifier($classObj);
-
- /*
- * Check for disabled plugins
- */
- if ($this->isDisabled($classId)) {
- $classObj->disabled = true;
- }
+ $classId = $this->getIdentifier($pluginObj);
- $this->plugins[$classId] = $classObj;
- $this->pathMap[$classId] = $path;
+ $this->plugins[$classId] = $pluginObj;
$this->normalizedMap[strtolower($classId)] = $classId;
- $replaces = $classObj->getReplaces();
+ $replaces = $pluginObj->getReplaces();
if ($replaces) {
foreach ($replaces as $replace) {
$this->replacementMap[$replace] = $classId;
}
}
- return $classObj;
+ return $pluginObj;
+ }
+
+ /**
+ * Get the cache key for the current plugin manager state
+ */
+ protected function getFlagCacheKey(): string
+ {
+ $loadedPlugins = array_keys($this->plugins);
+ $configDisabledPlugins = Config::get('cms.disablePlugins', []);
+ if (!is_array($configDisabledPlugins)) {
+ $configDisabledPlugins = [];
+ }
+ $plugins = $loadedPlugins + $configDisabledPlugins;
+
+ return 'system.pluginmanager.state.' . md5(implode('.', $plugins));
+ }
+
+ /**
+ * Loads the plugin flags (disabled & replacement states) from the cache
+ * regenerating them if required.
+ */
+ public function loadPluginFlags(): void
+ {
+ // Cache the data for a month so that stale keys can be autocleaned if necessary
+ $data = Cache::remember($this->getFlagCacheKey(), now()->addMonths(1), function () {
+ // Check the config files & database for plugins to disable
+ $this->loadDisabled();
+
+ // Check plugin dependencies for plugins to disable
+ $this->loadDependencies();
+
+ // Check plugin replacments for plugins to disable
+ $this->detectPluginReplacements();
+
+ return [
+ $this->pluginFlags,
+ $this->replacementMap,
+ $this->activeReplacementMap,
+ ];
+ });
+
+ list($this->pluginFlag, $this->replacementMap, $this->activeReplacementMap) = $data;
+ }
+
+ /**
+ * Reset the plugin flag cache
+ */
+ public function clearFlagCache(): void
+ {
+ Cache::forget($this->getFlagCacheKey());
}
/**
* Runs the register() method on all plugins. Can only be called once.
*
* @param bool $force Defaults to false, if true will force the re-registration of all plugins. Use unregisterAll() instead.
- * @return void
*/
- public function registerAll($force = false)
+ public function registerAll(bool $force = false): void
{
if ($this->registered && !$force) {
return;
@@ -220,10 +265,8 @@ public function registerAll($force = false)
/**
* Unregisters all plugins: the inverse of registerAll().
- *
- * @return void
*/
- public function unregisterAll()
+ public function unregisterAll(): void
{
$this->registered = false;
$this->plugins = [];
@@ -232,12 +275,8 @@ public function unregisterAll()
/**
* Registers a single plugin object.
- *
- * @param PluginBase $plugin The instantiated Plugin object
- * @param string $pluginId The string identifier for the plugin
- * @return void
*/
- public function registerPlugin($plugin, $pluginId = null)
+ public function registerPlugin(PluginBase $plugin, ?string $pluginId = null): void
{
if (!$pluginId) {
$pluginId = $this->getIdentifier($plugin);
@@ -257,7 +296,7 @@ public function registerPlugin($plugin, $pluginId = null)
/**
* Prevent autoloaders from loading if plugin is disabled
*/
- if ($plugin->disabled) {
+ if ($this->isDisabled($pluginId)) {
return;
}
@@ -292,7 +331,7 @@ public function registerPlugin($plugin, $pluginId = null)
foreach ($replaces as $replace) {
$replaceNamespace = $this->getNamespace($replace);
- App::make(ClassLoader::class)->addNamespaceAliases([
+ $this->app->make(ClassLoader::class)->addNamespaceAliases([
// class_alias() expects order to be $real, $alias
$this->getNamespace($pluginId) => $replaceNamespace,
]);
@@ -332,9 +371,8 @@ public function registerPlugin($plugin, $pluginId = null)
* Runs the boot() method on all plugins. Can only be called once.
*
* @param bool $force Defaults to false, if true will force the re-booting of all plugins
- * @return void
*/
- public function bootAll($force = false)
+ public function bootAll(bool $force = false): void
{
if ($this->booted && !$force) {
return;
@@ -349,13 +387,10 @@ public function bootAll($force = false)
/**
* Boots the provided plugin object.
- *
- * @param PluginBase $plugin
- * @return void
*/
- public function bootPlugin($plugin)
+ public function bootPlugin(PluginBase $plugin): void
{
- if (!$plugin || $plugin->disabled || (self::$noInit && !$plugin->elevated)) {
+ if ((self::$noInit && !$plugin->elevated) || $this->isDisabled($plugin)) {
return;
}
@@ -364,19 +399,10 @@ public function bootPlugin($plugin)
/**
* Returns the directory path to a plugin
- *
- * @param PluginBase|string $id The plugin to get the path for
- * @return string|null
*/
- public function getPluginPath($id)
+ public function getPluginPath(PluginBase|string $plugin): ?string
{
- $classId = $this->getIdentifier($id);
- $classId = $this->normalizeIdentifier($classId);
- if (!isset($this->pathMap[$classId])) {
- return null;
- }
-
- return File::normalizePath($this->pathMap[$classId]);
+ return $this->findByIdentifier($plugin, true)?->getPluginPath();
}
/**
@@ -385,9 +411,9 @@ public function getPluginPath($id)
* @param string $id Plugin identifier, eg: Namespace.PluginName
* @return bool
*/
- public function exists($id)
+ public function exists(PluginBase|string $plugin): bool
{
- return $this->findByIdentifier($id) && !$this->isDisabled($id);
+ return $this->findByIdentifier($plugin) && !$this->isDisabled($plugin);
}
/**
@@ -395,9 +421,9 @@ public function exists($id)
*
* @return array [$code => $pluginObj]
*/
- public function getPlugins()
+ public function getPlugins(): array
{
- return array_diff_key($this->plugins, $this->disabledPlugins);
+ return array_diff_key($this->plugins, $this->pluginFlags);
}
/**
@@ -405,18 +431,15 @@ public function getPlugins()
*
* @return array [$code => $pluginObj]
*/
- public function getAllPlugins()
+ public function getAllPlugins(): array
{
return $this->plugins;
}
/**
* Returns a plugin registration class based on its namespace (Author\Plugin).
- *
- * @param string $namespace
- * @return PluginBase|null
*/
- public function findByNamespace($namespace)
+ public function findByNamespace(string $namespace): ?PluginBase
{
$identifier = $this->getIdentifier($namespace);
@@ -425,20 +448,19 @@ public function findByNamespace($namespace)
/**
* Returns a plugin registration class based on its identifier (Author.Plugin).
- *
- * @param string|PluginBase $identifier
- * @param bool $ignoreReplacements
- * @return PluginBase|null
*/
- public function findByIdentifier($identifier, bool $ignoreReplacements = false)
+ public function findByIdentifier(PluginBase|string $identifier, bool $ignoreReplacements = false): ?PluginBase
{
+ if ($identifier instanceof PluginBase) {
+ return $identifier;
+ }
+
if (!$ignoreReplacements && is_string($identifier) && isset($this->replacementMap[$identifier])) {
$identifier = $this->replacementMap[$identifier];
}
if (!isset($this->plugins[$identifier])) {
- $code = $this->getIdentifier($identifier);
- $identifier = $this->normalizeIdentifier($code);
+ $identifier = $this->getNormalizedIdentifier($identifier);
}
return $this->plugins[$identifier] ?? null;
@@ -446,30 +468,25 @@ public function findByIdentifier($identifier, bool $ignoreReplacements = false)
/**
* Checks to see if a plugin has been registered.
- *
- * @param string|PluginBase
- * @return bool
*/
- public function hasPlugin($namespace)
+ public function hasPlugin(PluginBase|string $plugin): bool
{
- $classId = $this->getIdentifier($namespace);
- $normalized = $this->normalizeIdentifier($classId);
+ $normalized = $this->getNormalizedIdentifier($plugin);
return isset($this->plugins[$normalized]) || isset($this->replacementMap[$normalized]);
}
/**
* Returns a flat array of vendor plugin namespaces and their paths
- *
- * @return array ['Author\Plugin' => 'plugins/author/plugin']
+ * ['Author\Plugin' => 'plugins/author/plugin']
*/
- public function getPluginNamespaces()
+ public function getPluginNamespaces(): array
{
$classNames = [];
foreach ($this->getVendorAndPluginNames() as $vendorName => $vendorList) {
foreach ($vendorList as $pluginName => $pluginPath) {
- $namespace = '\\'.$vendorName.'\\'.$pluginName;
+ $namespace = '\\' . $vendorName . '\\' . $pluginName;
$namespace = Str::normalizeClassName($namespace);
$classNames[$namespace] = $pluginPath;
}
@@ -480,26 +497,25 @@ public function getPluginNamespaces()
/**
* Returns a 2 dimensional array of vendors and their plugins.
- *
- * @return array ['vendor' => ['author' => 'plugins/author/plugin']]
+ * ['vendor' => ['author' => 'plugins/author/plugin']]
*/
- public function getVendorAndPluginNames()
+ public function getVendorAndPluginNames(): array
{
$plugins = [];
- $dirPath = plugins_path();
+ $dirPath = $this->app->pluginsPath();
if (!File::isDirectory($dirPath)) {
return $plugins;
}
$it = new RecursiveIteratorIterator(
- new RecursiveDirectoryIterator($dirPath, RecursiveDirectoryIterator::FOLLOW_SYMLINKS)
+ new RecursiveDirectoryIterator($dirPath, FilesystemIterator::FOLLOW_SYMLINKS)
);
$it->setMaxDepth(2);
$it->rewind();
while ($it->valid()) {
- if (($it->getDepth() > 1) && $it->isFile() && (strtolower($it->getFilename()) == "plugin.php")) {
+ if (($it->getDepth() > 1) && $it->isFile() && (strtolower($it->getFilename()) === "plugin.php")) {
$filePath = dirname($it->getPathname());
$pluginName = basename($filePath);
$vendorName = basename(dirname($filePath));
@@ -513,14 +529,12 @@ public function getVendorAndPluginNames()
}
/**
- * Resolves a plugin identifier (Author.Plugin) from a plugin class name or object.
- *
- * @param mixed Plugin class name or object
- * @return string Identifier in format of Author.Plugin
+ * Resolves a plugin identifier (Author.Plugin) from a plugin class name
+ * (Author\Plugin) or PluginBase instance.
*/
- public function getIdentifier($namespace)
+ public function getIdentifier(PluginBase|string $plugin): string
{
- $namespace = Str::normalizeClassName($namespace);
+ $namespace = Str::normalizeClassName($plugin);
if (strpos($namespace, '\\') === null) {
return $namespace;
}
@@ -533,51 +547,48 @@ public function getIdentifier($namespace)
}
/**
- * Resolves a plugin namespace (Author\Plugin) from a plugin class name, identifier or object.
- *
- * @param mixed Plugin class name, identifier or object
- * @return string Namespace in format of Author\Plugin
+ * Resolves a plugin namespace (Author\Plugin) from a plugin class name
+ * (Author\Plugin\Classes\Example), identifier (Author.Plugin), or
+ * PluginBase instance.
*/
- public function getNamespace($identifier)
+ public function getNamespace(PluginBase|string $plugin): string
{
- if (
- is_object($identifier)
- || (is_string($identifier) && strpos($identifier, '.') === null)
- ) {
- return Str::normalizeClassName($identifier);
- }
+ if (is_string($plugin) && strpos($plugin, '.') !== null) {
+ $parts = explode('.', $plugin);
+ $slice = array_slice($parts, 0, 2);
+ $namespace = implode('\\', $slice);
- $parts = explode('.', $identifier);
- $slice = array_slice($parts, 0, 2);
- $namespace = implode('\\', $slice);
+ return Str::normalizeClassName($namespace);
+ }
- return Str::normalizeClassName($namespace);
+ return Str::normalizeClassName($plugin);
}
/**
- * Takes a human plugin code (acme.blog) and makes it authentic (Acme.Blog)
+ * Normalizes the provided plugin identifier (author.plugin) and resolves
+ * it case-insensitively to the normalized identifier (Author.Plugin)
* Returns the provided identifier if a match isn't found
- *
- * @param string $identifier
- * @return string
*/
- public function normalizeIdentifier($identifier)
+ public function normalizeIdentifier(string $code): string
{
- $id = strtolower($identifier);
- if (isset($this->normalizedMap[$id])) {
- return $this->normalizedMap[$id];
- }
+ $code = strtolower($code);
+ return $this->normalizedMap[$code] ?? $code;
+ }
- return $identifier;
+ /**
+ * Returns the normalized identifier (i.e. Winter.Blog) from the provided
+ * string or PluginBase instance.
+ */
+ public function getNormalizedIdentifier(PluginBase|string $plugin): string
+ {
+ return $this->normalizeIdentifier($this->getIdentifier($plugin));
}
/**
- * Spins over every plugin object and collects the results of a method call. Results are cached in memory.
- *
- * @param string $methodName
- * @return array
+ * Spins over every plugin object and collects the results of the provided
+ * method call. Results are cached in memory.
*/
- public function getRegistrationMethodValues($methodName)
+ public function getRegistrationMethodValues(string $methodName): array
{
if (isset($this->registrationMethodCache[$methodName])) {
return $this->registrationMethodCache[$methodName];
@@ -598,157 +609,138 @@ public function getRegistrationMethodValues($methodName)
}
//
- // Disability
+ // State Management (Disable, Enable, Freeze, Unfreeze)
//
+ public function getPluginFlags(PluginBase|string $plugin): array
+ {
+ $code = $this->getNormalizedIdentifier($plugin);
+ return $this->pluginFlags[$code] ?? [];
+ }
+
/**
- * Clears the disabled plugins cache file
- *
- * @return void
+ * Sets the provided flag on the provided plugin
*/
- public function clearDisabledCache()
+ protected function flagPlugin(PluginBase|string $plugin, string $flag): void
{
- Storage::delete($this->metaFile);
- $this->disabledPlugins = [];
+ $code = $this->getNormalizedIdentifier($plugin);
+ $this->pluginFlags[$code][$flag] = true;
}
/**
- * Loads all disabled plugins from the cached JSON file.
- *
- * @return void
+ * Removes the provided flag from the provided plugin
*/
- protected function loadDisabled()
+ protected function unflagPlugin(PluginBase|string $plugin, string $flag): void
{
- $path = $this->metaFile;
+ $code = $this->getNormalizedIdentifier($plugin);
+ unset($this->pluginFlags[$code][$flag]);
+ }
+ /**
+ * Loads all disabled plugins from the cached JSON file.
+ */
+ protected function loadDisabled(): void
+ {
+ // Check the config files for disabled plugins
if (($configDisabled = Config::get('cms.disablePlugins')) && is_array($configDisabled)) {
foreach ($configDisabled as $disabled) {
- $this->disabledPlugins[$disabled] = true;
+ $this->flagPlugin($disabled, static::DISABLED_BY_CONFIG);
}
}
- if (Storage::exists($path)) {
- $disabled = json_decode(Storage::get($path), true) ?: [];
- $this->disabledPlugins = array_merge($this->disabledPlugins, $disabled);
- } else {
- $this->populateDisabledPluginsFromDb();
- $this->writeDisabled();
+ // Check the database for disabled plugins
+ if (
+ $this->app->hasDatabase()
+ && Schema::hasTable('system_plugin_versions')
+ ) {
+ $userDisabled = Db::table('system_plugin_versions')->where('is_disabled', 1)->lists('code') ?? [];
+ foreach ($userDisabled as $code) {
+ $this->flagPlugin($code, static::DISABLED_BY_USER);
+ }
}
}
/**
* Determines if a plugin is disabled by looking at the meta information
* or the application configuration.
- *
- * @param string|PluginBase $id
- * @return bool
- */
- public function isDisabled($id)
- {
- $code = $this->getIdentifier($id);
- $normalized = $this->normalizeIdentifier($code);
-
- return isset($this->disabledPlugins[$normalized]);
- }
-
- /**
- * Write the disabled plugins to a meta file.
- *
- * @return void
- */
- protected function writeDisabled()
- {
- Storage::put($this->metaFile, json_encode($this->disabledPlugins));
- }
-
- /**
- * Populates information about disabled plugins from database
- *
- * @return void
*/
- protected function populateDisabledPluginsFromDb()
+ public function isDisabled(PluginBase|string $plugin): bool
{
- if (!App::hasDatabase()) {
- return;
- }
+ $code = $this->getNormalizedIdentifier($plugin);
- if (!Schema::hasTable('system_plugin_versions')) {
- return;
- }
-
- $disabled = Db::table('system_plugin_versions')->where('is_disabled', 1)->lists('code');
-
- foreach ($disabled as $code) {
- $this->disabledPlugins[$code] = true;
- }
+ // @TODO: Limit this to only disabled flags if we add more than disabled flags
+ return !empty($this->pluginFlags[$code]);
}
/**
* Returns the plugin replacements defined in $this->replacementMap
- *
- * @return array
*/
- public function getReplacementMap()
+ public function getReplacementMap(): array
{
return $this->replacementMap;
}
/**
* Returns the actively replaced plugins defined in $this->activeReplacementMap
- * @param string $pluginIdentifier Plugin code/namespace
- * @return array|null
*/
- public function getActiveReplacementMap(string $pluginIdentifier = null)
+ public function getActiveReplacementMap(PluginBase|string $plugin = null): array|string|null
{
- if (!$pluginIdentifier) {
- return $this->activeReplacementMap;
- }
- return $this->activeReplacementMap[$pluginIdentifier] ?? null;
+ return $plugin
+ ? $this->activeReplacementMap[$this->getNormalizedIdentifier($plugin)] ?? null
+ : $this->activeReplacementMap;
}
/**
- * Evaluates and initializes the plugin replacements defined in $this->replacementMap
- *
- * @return void
+ * Evaluates the replacement map to determine which replacements can actually
+ * take effect
*/
- public function registerReplacedPlugins()
+ protected function detectPluginReplacements(): void
{
if (empty($this->replacementMap)) {
return;
}
foreach ($this->replacementMap as $target => $replacement) {
- // Alias the replaced plugin to the replacing plugin if the replaced plugin isn't present
+ // If the replaced plugin isn't present then assume it can be replaced
if (!isset($this->plugins[$target])) {
- $this->aliasPluginAs($replacement, $target);
continue;
}
// Only allow one of the replaced plugin or the replacing plugin to exist
// at once depending on whether the version constraints are met or not
if ($this->plugins[$replacement]->canReplacePlugin($target, $this->plugins[$target]->getPluginVersion())) {
- $this->aliasPluginAs($replacement, $target);
- $this->disablePlugin($target);
- $this->enablePlugin($replacement);
- // Register this plugin as actively replaced
+ // Set the plugin flags to disable the target plugin
+ $this->flagPlugin($target, static::DISABLED_REPLACED);
+ $this->unflagPlugin($replacement, static::DISABLED_REPLACEMENT_FAILED);
+
+ // Register this plugin as actively replaced (i.e. both are present, replaced are disabled)
$this->activeReplacementMap[$target] = $replacement;
} else {
- $this->disablePlugin($replacement);
- $this->enablePlugin($target);
+ // Set the plugin flags to disable the replacement plugin
+ $this->flagPlugin($replacement, static::DISABLED_REPLACEMENT_FAILED);
+ $this->unflagPlugin($target, static::DISABLED_REPLACED);
+
// Remove the replacement alias to prevent redirection to a disabled plugin
unset($this->replacementMap[$target]);
}
}
}
+ /**
+ * Executes the plugin replacements defined in the activeReplacementMap property
+ */
+ protected function registerPluginReplacements(): void
+ {
+ foreach ($this->replacementMap as $target => $replacement) {
+ // Alias the replaced plugin to the replacing plugin
+ $this->aliasPluginAs($replacement, $target);
+ }
+ }
+
/**
* Registers namespace aliasing for multiple subsystems
- *
- * @param string $namespace Plugin code
- * @param string $alias Plugin alias code
- * @return void
*/
- protected function aliasPluginAs(string $namespace, string $alias)
+ protected function aliasPluginAs(string $namespace, string $alias): void
{
Lang::registerNamespaceAlias($namespace, $alias);
Config::registerNamespaceAlias($namespace, $alias);
@@ -758,57 +750,101 @@ protected function aliasPluginAs(string $namespace, string $alias)
}
/**
- * Disables a single plugin in the system.
+ * Get the PluginVersion record for the provided plugin
*
- * @param string|PluginBase $id Plugin code/namespace
- * @param bool $isUser Set to true if disabled by the user, false by default
- * @return bool Returns false if the plugin was already disabled, true otherwise
+ * @throws InvalidArgumentException if unable to find the requested plugin record in the database
*/
- public function disablePlugin($id, $isUser = false)
+ protected function getPluginRecord(PluginBase|string $plugin): PluginVersion
{
- $code = $this->getIdentifier($id);
- $code = $this->normalizeIdentifier($code);
- if (isset($this->disabledPlugins[$code])) {
- return false;
+ $plugin = $this->getNormalizedIdentifier($plugin);
+ if (isset($this->pluginRecords[$plugin])) {
+ return $this->pluginRecords[$plugin];
}
- $this->disabledPlugins[$code] = $isUser;
- $this->writeDisabled();
+ $record = PluginVersion::where('code', $plugin)->first();
- if ($pluginObj = $this->findByIdentifier($code, true)) {
- $pluginObj->disabled = true;
+ if (!$record) {
+ throw new \InvalidArgumentException("$plugin was not found in the database.");
}
- return true;
+ return $this->pluginRecords[$plugin] = $record;
}
/**
- * Enables a single plugin in the system.
- *
- * @param string|PluginBase $id Plugin code/namespace
- * @param bool $isUser Set to true if enabled by the user, false by default
- * @return bool Returns false if the plugin wasn't already disabled or if the user disabled a plugin that the system is trying to re-enable, true otherwise
+ * Flags the provided plugin as "frozen" (updates cannot be downloaded / installed)
+ */
+ public function freezePlugin(PluginBase|string $plugin): void
+ {
+ $record = $this->getPluginRecord($plugin);
+ $record->is_frozen = true;
+ $record->save();
+ }
+
+ /**
+ * "Unfreezes" the provided plugin, allowing for updates to be performed
*/
- public function enablePlugin($id, $isUser = false)
+ public function unfreezePlugin(PluginBase|string $plugin): void
{
- $code = $this->getIdentifier($id);
- $code = $this->normalizeIdentifier($code);
- if (!isset($this->disabledPlugins[$code])) {
- return false;
+ $record = $this->getPluginRecord($plugin);
+ $record->is_frozen = false;
+ $record->save();
+ }
+
+ /**
+ * Disables the provided plugin using the provided flag (defaults to static::DISABLED_BY_USER)
+ */
+ public function disablePlugin(PluginBase|string $plugin, string|bool $flag = self::DISABLED_BY_USER): bool
+ {
+ // $flag used to be (bool) $byUser
+ if ($flag === true) {
+ $flag = static::DISABLED_BY_USER;
}
- // Prevent system from enabling plugins disabled by the user
- if (!$isUser && $this->disabledPlugins[$code] === true) {
- return false;
+ // Flag the plugin as disabled
+ $this->flagPlugin($plugin, $flag);
+
+ // Updates the database record for the plugin if required
+ if ($flag === static::DISABLED_BY_USER) {
+ $record = $this->getPluginRecord($plugin);
+ $record->is_disabled = true;
+ $record->save();
+
+ // Clear the cache so that the next request will regenerate the active flags
+ $this->clearFlagCache();
}
- unset($this->disabledPlugins[$code]);
- $this->writeDisabled();
+ // Clear the registration values cache
+ $this->registrationMethodCache = [];
- if ($pluginObj = $this->findByIdentifier($code, true)) {
- $pluginObj->disabled = false;
+ return true;
+ }
+
+ /**
+ * Enables the provided plugin using the provided flag (defaults to static::DISABLED_BY_USER)
+ */
+ public function enablePlugin(PluginBase|string $plugin, $flag = self::DISABLED_BY_USER): bool
+ {
+ // $flag used to be (bool) $byUser
+ if ($flag === true) {
+ $flag = static::DISABLED_BY_USER;
}
+ // Unflag the plugin as disabled
+ $this->unflagPlugin($plugin, $flag);
+
+ // Updates the database record for the plugin if required
+ if ($flag === static::DISABLED_BY_USER) {
+ $record = $this->getPluginRecord($plugin);
+ $record->is_disabled = false;
+ $record->save();
+
+ // Clear the cache so that the next request will regenerate the active flags
+ $this->clearFlagCache();
+ }
+
+ // Clear the registration values cache
+ $this->registrationMethodCache = [];
+
return true;
}
@@ -816,6 +852,24 @@ public function enablePlugin($id, $isUser = false)
// Dependencies
//
+ /**
+ * Returns the plugin identifiers that are required by the supplied plugin.
+ */
+ public function getDependencies(PluginBase|string $plugin): array
+ {
+ if (is_string($plugin) && (!$plugin = $this->findByIdentifier($plugin))) {
+ return [];
+ }
+
+ if (!isset($plugin->require) || !$plugin->require) {
+ return [];
+ }
+
+ return array_map(function ($require) {
+ return $this->replacementMap[$require] ?? $require;
+ }, is_array($plugin->require) ? $plugin->require : [$plugin->require]);
+ }
+
/**
* Scans the system plugins to locate any dependencies that are not currently
* installed. Returns an array of missing plugin codes keyed by the plugin that requires them.
@@ -824,9 +878,8 @@ public function enablePlugin($id, $isUser = false)
*
* PluginManager::instance()->findMissingDependencies();
*
- * @return array
*/
- public function findMissingDependencies()
+ public function findMissingDependencies(): array
{
$missing = [];
@@ -850,32 +903,15 @@ public function findMissingDependencies()
}
/**
- * Cross checks all plugins and their dependancies, if not met plugins
- * are disabled and vice versa.
- *
- * @return void
+ * Checks plugin dependencies and flags plugins with missing dependencies as disabled
*/
- protected function loadDependencies()
+ protected function loadDependencies(): void
{
foreach ($this->plugins as $id => $plugin) {
- if (!$required = $this->getDependencies($plugin)) {
- continue;
- }
-
- $disable = false;
-
- foreach ($required as $require) {
- if (!$pluginObj = $this->findByIdentifier($require)) {
- $disable = true;
- } elseif ($pluginObj->disabled) {
- $disable = true;
- }
- }
-
- if ($disable) {
- $this->disablePlugin($id);
+ if (!$plugin->checkDependencies($this)) {
+ $this->flagPlugin($id, static::DISABLED_MISSING_DEPENDENCIES);
} else {
- $this->enablePlugin($id);
+ $this->unflagPlugin($id, static::DISABLED_MISSING_DEPENDENCIES);
}
}
}
@@ -887,7 +923,7 @@ protected function loadDependencies()
* @return array Array of sorted plugin identifiers and instantiated classes ['Author.Plugin' => PluginBase]
* @throws SystemException If a possible circular dependency is detected
*/
- protected function sortDependencies()
+ protected function sortByDependencies(): array
{
ksort($this->plugins);
@@ -959,49 +995,14 @@ protected function sortDependencies()
return $this->plugins = $sortedPlugins;
}
- /**
- * Returns the plugin identifiers that are required by the supplied plugin.
- *
- * @param string $plugin Plugin identifier, object or class
- * @return array
- */
- public function getDependencies($plugin)
- {
- if (is_string($plugin) && (!$plugin = $this->findByIdentifier($plugin))) {
- return [];
- }
-
- if (!isset($plugin->require) || !$plugin->require) {
- return [];
- }
-
- return array_map(function ($require) {
- return $this->replacementMap[$require] ?? $require;
- }, is_array($plugin->require) ? $plugin->require : [$plugin->require]);
- }
-
- /**
- * @deprecated Plugins are now sorted by default. See getPlugins()
- * Remove if year >= 2022
- */
- public function sortByDependencies($plugins = null)
- {
- traceLog('PluginManager::sortByDependencies is deprecated. Plugins are now sorted by default. Use PluginManager::getPlugins()');
-
- return array_keys($plugins ?: $this->getPlugins());
- }
-
//
// Management
//
/**
* Completely roll back and delete a plugin from the system.
- *
- * @param string $id Plugin code/namespace
- * @return void
*/
- public function deletePlugin($id)
+ public function deletePlugin(string $id): void
{
/*
* Rollback plugin
@@ -1013,16 +1014,19 @@ public function deletePlugin($id)
*/
if ($pluginPath = self::instance()->getPluginPath($id)) {
File::deleteDirectory($pluginPath);
+
+ // Clear the registration values cache
+ $this->registrationMethodCache = [];
+
+ // Clear the plugin flag cache
+ $this->clearFlagCache();
}
}
/**
* Tears down a plugin's database tables and rebuilds them.
- *
- * @param string $id Plugin code/namespace
- * @return void
*/
- public function refreshPlugin($id)
+ public function refreshPlugin(string $id): void
{
$manager = UpdateManager::instance();
$manager->rollbackPlugin($id);
diff --git a/modules/system/classes/UpdateManager.php b/modules/system/classes/UpdateManager.php
index 714cddbf76..a3bd2e0c7f 100644
--- a/modules/system/classes/UpdateManager.php
+++ b/modules/system/classes/UpdateManager.php
@@ -999,7 +999,8 @@ protected function applyHttpAttributes($http, $postData)
$postData['server'] = base64_encode(serialize([
'php' => PHP_VERSION,
'url' => Url::to('/'),
- 'since' => PluginVersion::orderBy('created_at')->value('created_at')
+ // TODO: Store system boot date in `Parameter`
+ 'since' => PluginVersion::orderBy('created_at')->first()->created_at
]));
if ($projectId = Parameter::get('system::project.id')) {
diff --git a/modules/system/classes/VersionManager.php b/modules/system/classes/VersionManager.php
index 0cb080cd48..a760e30ddb 100644
--- a/modules/system/classes/VersionManager.php
+++ b/modules/system/classes/VersionManager.php
@@ -346,7 +346,7 @@ protected function getFileVersions($code)
$versionFile = $this->getVersionFile($code);
$versionInfo = Yaml::withProcessor(new VersionYamlProcessor, function ($yaml) use ($versionFile) {
- return $yaml->parse(file_get_contents($versionFile));
+ return $yaml->parseFile($versionFile);
});
if (!is_array($versionInfo)) {
diff --git a/modules/system/console/PluginDisable.php b/modules/system/console/PluginDisable.php
index 0169fb813a..f531ed3112 100644
--- a/modules/system/console/PluginDisable.php
+++ b/modules/system/console/PluginDisable.php
@@ -41,10 +41,6 @@ public function handle()
// Disable this plugin
$pluginManager->disablePlugin($pluginName);
- $plugin = PluginVersion::where('code', $pluginName)->first();
- $plugin->is_disabled = true;
- $plugin->save();
- $pluginManager->clearDisabledCache();
$this->output->writeln(sprintf('%s: disabled.', $pluginName));
}
diff --git a/modules/system/console/PluginEnable.php b/modules/system/console/PluginEnable.php
index eae8d3c772..f2ec3ae3e5 100644
--- a/modules/system/console/PluginEnable.php
+++ b/modules/system/console/PluginEnable.php
@@ -46,10 +46,6 @@ public function handle()
// Enable this plugin
$pluginManager->enablePlugin($pluginName);
- $plugin = PluginVersion::where('code', $pluginName)->first();
- $plugin->is_disabled = false;
- $plugin->save();
- $pluginManager->clearDisabledCache();
$this->output->writeln(sprintf('%s: enabled.', $pluginName));
}
diff --git a/modules/system/controllers/Updates.php b/modules/system/controllers/Updates.php
index 69b730c444..c953ec9f53 100644
--- a/modules/system/controllers/Updates.php
+++ b/modules/system/controllers/Updates.php
@@ -91,7 +91,7 @@ public function index()
public function manage()
{
$this->pageTitle = 'system::lang.plugins.manage';
- PluginManager::instance()->clearDisabledCache();
+ PluginManager::instance()->clearFlagCache();
return $this->asExtension('ListController')->index();
}
@@ -238,6 +238,10 @@ protected function getWarnings()
$warnings = [];
$missingDependencies = PluginManager::instance()->findMissingDependencies();
+ if (!empty($missingDependencies)) {
+ PluginManager::instance()->clearFlagCache();
+ }
+
foreach ($missingDependencies as $pluginCode => $plugin) {
foreach ($plugin as $missingPluginCode) {
$warnings[] = Lang::get('system::lang.updates.update_warnings_plugin_missing', [
@@ -845,52 +849,45 @@ public function onBulkAction()
count($checkedIds)
) {
$manager = PluginManager::instance();
+ $codes = PluginVersion::lists('code', 'id');
- foreach ($checkedIds as $pluginId) {
- if (!$plugin = PluginVersion::find($pluginId)) {
+ foreach ($checkedIds as $id) {
+ $code = $codes[$id] ?? null;
+ if (!$code) {
continue;
}
- $savePlugin = true;
switch ($bulkAction) {
// Enables plugin's updates.
case 'freeze':
- $plugin->is_frozen = 1;
+ $manager->freezePlugin($code);
break;
// Disables plugin's updates.
case 'unfreeze':
- $plugin->is_frozen = 0;
+ $manager->unfreezePlugin($code);
break;
// Disables plugin on the system.
case 'disable':
- $plugin->is_disabled = 1;
- $manager->disablePlugin($plugin->code, true);
+ $manager->disablePlugin($code);
break;
// Enables plugin on the system.
case 'enable':
- $plugin->is_disabled = 0;
- $manager->enablePlugin($plugin->code, true);
+ $manager->enablePlugin($code);
break;
// Rebuilds plugin database migrations.
case 'refresh':
- $savePlugin = false;
- $manager->refreshPlugin($plugin->code);
+ $manager->refreshPlugin($code);
break;
// Rollback and remove plugins from the system.
case 'remove':
- $savePlugin = false;
- $manager->deletePlugin($plugin->code);
+ $manager->deletePlugin($code);
break;
}
-
- if ($savePlugin) {
- $plugin->save();
- }
}
}
diff --git a/modules/system/models/PluginVersion.php b/modules/system/models/PluginVersion.php
index 30b8ece054..14f2155dc8 100644
--- a/modules/system/models/PluginVersion.php
+++ b/modules/system/models/PluginVersion.php
@@ -2,7 +2,6 @@
use Lang;
use Model;
-use Config;
use System\Classes\PluginManager;
/**
@@ -96,17 +95,22 @@ public function afterFetch()
}
}
- if ($this->is_disabled) {
- $manager->disablePlugin($this->code, true);
- }
- else {
- $manager->enablePlugin($this->code, true);
- }
-
- $this->disabledBySystem = $pluginObj->disabled;
-
- if (($configDisabled = Config::get('cms.disablePlugins')) && is_array($configDisabled)) {
- $this->disabledByConfig = in_array($this->code, $configDisabled);
+ $activeFlags = $manager->getPluginFlags($pluginObj);
+ if (!empty($activeFlags)) {
+ foreach ($activeFlags as $flag => $enabled) {
+ if (in_array($flag, [
+ PluginManager::DISABLED_MISSING,
+ PluginManager::DISABLED_REPLACED,
+ PluginManager::DISABLED_REPLACEMENT_FAILED,
+ PluginManager::DISABLED_MISSING_DEPENDENCIES,
+ ])) {
+ $this->disabledBySystem = true;
+ }
+
+ if ($flag === PluginManager::DISABLED_BY_CONFIG) {
+ $this->disabledByConfig = true;
+ }
+ }
}
}
else {
@@ -118,9 +122,8 @@ public function afterFetch()
/**
* Returns true if the plugin should be updated by the system.
- * @return bool
*/
- public function getIsUpdatableAttribute()
+ public function getIsUpdatableAttribute(): bool
{
return !$this->is_disabled && !$this->disabledBySystem && !$this->disabledByConfig;
}
@@ -128,7 +131,7 @@ public function getIsUpdatableAttribute()
/**
* Only include enabled plugins
* @param $query
- * @return mixed
+ * @return QueryBuilder
*/
public function scopeApplyEnabled($query)
{
@@ -137,10 +140,8 @@ public function scopeApplyEnabled($query)
/**
* Returns the current version for a plugin
- * @param string $pluginCode Plugin code. Eg: Acme.Blog
- * @return string
*/
- public static function getVersion($pluginCode)
+ public static function getVersion(string $pluginCode): ?string
{
if (self::$versionCache === null) {
self::$versionCache = self::lists('version', 'code');
@@ -152,7 +153,7 @@ public static function getVersion($pluginCode)
/**
* Provides the slug attribute.
*/
- public function getSlugAttribute()
+ public function getSlugAttribute(): string
{
return self::makeSlug($this->code);
}
@@ -160,7 +161,7 @@ public function getSlugAttribute()
/**
* Generates a slug for the plugin.
*/
- public static function makeSlug($code)
+ public static function makeSlug(string $code): string
{
return strtolower(str_replace('.', '-', $code));
}
diff --git a/modules/system/providers.php b/modules/system/providers.php
index 9fb16b4893..fa44814b39 100644
--- a/modules/system/providers.php
+++ b/modules/system/providers.php
@@ -7,7 +7,6 @@
*/
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
- Illuminate\Cache\CacheServiceProvider::class,
Illuminate\Cookie\CookieServiceProvider::class,
Illuminate\Encryption\EncryptionServiceProvider::class,
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
@@ -22,6 +21,7 @@
/*
* Winter Storm providers
*/
+ Winter\Storm\Cache\CacheServiceProvider::class,
Winter\Storm\Foundation\Providers\ConsoleSupportServiceProvider::class,
Winter\Storm\Database\DatabaseServiceProvider::class,
Winter\Storm\Halcyon\HalcyonServiceProvider::class,
diff --git a/modules/system/reportwidgets/Status.php b/modules/system/reportwidgets/Status.php
index f078642fea..157606b721 100644
--- a/modules/system/reportwidgets/Status.php
+++ b/modules/system/reportwidgets/Status.php
@@ -69,7 +69,8 @@ protected function loadData()
$this->vars['requestLog'] = RequestLog::count();
$this->vars['requestLogMsg'] = LogSetting::get('log_requests', false) ? false : true;
- $this->vars['appBirthday'] = PluginVersion::orderBy('created_at')->value('created_at');
+ // TODO: Store system boot date in `Parameter`
+ $this->vars['appBirthday'] = PluginVersion::orderBy('created_at')->first()->created_at;
}
public function onLoadWarningsForm()
diff --git a/tests/unit/system/classes/PluginManagerTest.php b/tests/unit/system/classes/PluginManagerTest.php
index 8fe3a02a33..10667f5d29 100644
--- a/tests/unit/system/classes/PluginManagerTest.php
+++ b/tests/unit/system/classes/PluginManagerTest.php
@@ -1,22 +1,118 @@
set('database.connections.testing', [
+ 'driver' => 'sqlite',
+ 'database' => ':memory:',
+ ]);
+ $app['config']->set('database.default', 'testing');
+ }
+
+ return $app;
+ }
+ /**
+ * Perform test case set up.
+ * @return void
+ */
public function setUp() : void
{
+ /*
+ * Force reload of Winter singletons
+ */
+ PluginManager::forgetInstance();
+ UpdateManager::forgetInstance();
+
+ // Forces plugin migrations to be run again on every test
+ VersionManager::forgetInstance();
+
+ $this->output = new \Symfony\Component\Console\Output\BufferedOutput();
+
parent::setUp();
+ /*
+ * Ensure system is up to date
+ */
+ $this->runWinterUpCommand();
+
$manager = PluginManager::instance();
self::callProtectedMethod($manager, 'loadDisabled');
$manager->loadPlugins();
self::callProtectedMethod($manager, 'loadDependencies');
-
$this->manager = $manager;
}
+ /**
+ * Flush event listeners and collect garbage.
+ * @return void
+ */
+ public function tearDown() : void
+ {
+ $this->flushModelEventListeners();
+ parent::tearDown();
+ unset($this->app);
+ }
+
+ /**
+ * Migrate database using winter:up command.
+ * @return void
+ */
+ protected function runWinterUpCommand()
+ {
+ UpdateManager::instance()
+ ->setNotesOutput($this->output)
+ ->update();
+ }
+
+ /**
+ * The models in Winter use a static property to store their events, these
+ * will need to be targeted and reset ready for a new test cycle.
+ * Pivot models are an exception since they are internally managed.
+ * @return void
+ */
+ protected function flushModelEventListeners()
+ {
+ foreach (get_declared_classes() as $class) {
+ if ($class === 'Winter\Storm\Database\Pivot' || strtolower($class) === 'october\rain\database\pivot') {
+ continue;
+ }
+
+ $reflectClass = new ReflectionClass($class);
+ if (
+ !$reflectClass->isInstantiable() ||
+ !$reflectClass->isSubclassOf('Winter\Storm\Database\Model') ||
+ $reflectClass->isSubclassOf('Winter\Storm\Database\Pivot')
+ ) {
+ continue;
+ }
+
+ $class::flushEventListeners();
+ }
+
+ ActiveRecord::flushEventListeners();
+ }
+
//
// Tests
//
@@ -290,4 +386,56 @@ public function testActiveReplacementMap()
$this->assertEquals('Winter.Replacement', $this->manager->getActiveReplacementMap('Winter.Original'));
$this->assertNull($this->manager->getActiveReplacementMap('Winter.InvalidReplacement'));
}
+
+ public function testFlagDisableStatus()
+ {
+ $plugin = $this->manager->findByIdentifier('DependencyTest.Dependency');
+ $flags = $this->manager->getPluginFlags($plugin);
+ $this->assertEmpty($flags);
+
+ $plugin = $this->manager->findByIdentifier('DependencyTest.NotFound');
+ $flags = $this->manager->getPluginFlags($plugin);
+ $this->assertCount(1, $flags);
+ $this->assertArrayHasKey(PluginManager::DISABLED_MISSING_DEPENDENCIES, $flags);
+
+ $plugin = $this->manager->findByIdentifier('Winter.InvalidReplacement');
+ $flags = $this->manager->getPluginFlags($plugin);
+ $this->assertCount(1, $flags);
+ $this->assertArrayHasKey(PluginManager::DISABLED_REPLACEMENT_FAILED, $flags);
+
+ $plugin = $this->manager->findByIdentifier('Winter.Original', true);
+ $flags = $this->manager->getPluginFlags($plugin);
+ $this->assertCount(1, $flags);
+ $this->assertArrayHasKey(PluginManager::DISABLED_REPLACED, $flags);
+ }
+
+ public function testFlagDisabling()
+ {
+ $plugin = $this->manager->findByIdentifier('Winter.Tester', true);
+
+ $flags = $this->manager->getPluginFlags($plugin);
+ $this->assertEmpty($flags);
+
+ $this->manager->disablePlugin($plugin);
+
+ $flags = $this->manager->getPluginFlags($plugin);
+ $this->assertCount(1, $flags);
+ $this->assertArrayHasKey(PluginManager::DISABLED_BY_USER, $flags);
+
+ $this->manager->enablePlugin($plugin);
+
+ $flags = $this->manager->getPluginFlags($plugin);
+ $this->assertEmpty($flags);
+
+ $this->manager->disablePlugin($plugin, PluginManager::DISABLED_BY_CONFIG);
+
+ $flags = $this->manager->getPluginFlags($plugin);
+ $this->assertCount(1, $flags);
+ $this->assertArrayHasKey(PluginManager::DISABLED_BY_CONFIG, $flags);
+
+ $this->manager->enablePlugin($plugin, PluginManager::DISABLED_BY_CONFIG);
+
+ $flags = $this->manager->getPluginFlags($plugin);
+ $this->assertEmpty($flags);
+ }
}