diff --git a/modules/system/classes/PluginManager.php b/modules/system/classes/PluginManager.php
index 025584b7da..fc14126b80 100644
--- a/modules/system/classes/PluginManager.php
+++ b/modules/system/classes/PluginManager.php
@@ -4,7 +4,6 @@
use Backend\Classes\NavigationManager;
use FilesystemIterator;
-use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
@@ -14,7 +13,7 @@
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use System\Models\PluginVersion;
-use SystemException;
+use Winter\Storm\Exception\SystemException;
use Winter\Storm\Foundation\Application;
use Winter\Storm\Support\ClassLoader;
use Winter\Storm\Support\Facades\Config;
diff --git a/modules/system/classes/UpdateManager.php b/modules/system/classes/UpdateManager.php
index 8a9569558c..55374ac690 100644
--- a/modules/system/classes/UpdateManager.php
+++ b/modules/system/classes/UpdateManager.php
@@ -1,23 +1,31 @@
-getMigrationTableName());
@@ -212,10 +195,9 @@ public function update()
/**
* Checks for new updates and returns the amount of unapplied updates.
* Only requests from the server at a set interval (retry timer).
- * @param boolean $force Ignore the retry timer.
- * @return int Number of unapplied updates.
+ * @param $force Ignore the retry timer.
*/
- public function check($force = false)
+ public function check(bool $force = false): int
{
/*
* Already know about updates, never retry.
@@ -253,10 +235,9 @@ public function check($force = false)
/**
* Requests an update list used for checking for new updates.
- * @param boolean $force Request application and plugins hash list regardless of version.
- * @return array
+ * @param $force Request application and plugins hash list regardless of version.
*/
- public function requestUpdateList($force = false)
+ public function requestUpdateList(bool $force = false): array
{
$installed = PluginVersion::all();
$versions = $installed->lists('version', 'code');
@@ -349,19 +330,16 @@ public function requestUpdateList($force = false)
/**
* Requests details about a project based on its identifier.
- * @param string $projectId
- * @return array
*/
- public function requestProjectDetails($projectId)
+ public function requestProjectDetails(string $projectId): array
{
return $this->requestServerData('project/detail', ['id' => $projectId]);
}
/**
* Roll back all modules and plugins.
- * @return self
*/
- public function uninstall()
+ public function uninstall(): static
{
/*
* Rollback plugins
@@ -411,10 +389,9 @@ public function uninstall()
* to the code = less confidence.
* - `changes`: If $detailed is true, this will include the list of files modified, created and deleted.
*
- * @param bool $detailed If true, the list of files modified, added and deleted will be included in the result.
- * @return array
+ * @param $detailed If true, the list of files modified, added and deleted will be included in the result.
*/
- public function getBuildNumberManually($detailed = false)
+ public function getBuildNumberManually(bool $detailed = false): array
{
$source = new SourceManifest();
$manifest = new FileManifest(null, null, true);
@@ -426,10 +403,9 @@ public function getBuildNumberManually($detailed = false)
/**
* Sets the build number in the database.
*
- * @param bool $detailed If true, the list of files modified, added and deleted will be included in the result.
- * @return void
+ * @param $detailed If true, the list of files modified, added and deleted will be included in the result.
*/
- public function setBuildNumberManually($detailed = false)
+ public function setBuildNumberManually(bool $detailed = false): array
{
$build = $this->getBuildNumberManually($detailed);
@@ -446,19 +422,16 @@ public function setBuildNumberManually($detailed = false)
/**
* Returns the currently installed system hash.
- * @return string
*/
- public function getHash()
+ public function getHash(): string
{
return Parameter::get('system::core.hash', md5('NULL'));
}
/**
* Run migrations on a single module
- * @param string $module Module name
- * @return self
*/
- public function migrateModule($module)
+ public function migrateModule(string $module): static
{
if (isset($this->notesOutput)) {
$this->migrator->setOutput($this->notesOutput);
@@ -475,14 +448,12 @@ public function migrateModule($module)
/**
* Run seeds on a module
- * @param string $module Module name
- * @return self
*/
- public function seedModule($module)
+ public function seedModule(string $module): static
{
$className = '\\' . $module . '\Database\Seeds\DatabaseSeeder';
if (!class_exists($className)) {
- return;
+ return $this;
}
$this->out('', true);
@@ -503,37 +474,27 @@ public function seedModule($module)
/**
* Downloads the core from the update server.
- * @param string $hash Expected file hash.
- * @return void
+ * @param $hash Expected file hash.
*/
- public function downloadCore($hash)
+ public function downloadCore(string $hash): void
{
$this->requestServerFile('core/get', 'core', $hash, ['type' => 'update']);
}
/**
* Extracts the core after it has been downloaded.
- * @return void
*/
- public function extractCore()
+ public function extractCore(): void
{
$filePath = $this->getFilePath('core');
- if (!Zip::extract($filePath, $this->baseDirectory)) {
- throw new ApplicationException(Lang::get('system::lang.zip.extract_failed', ['file' => $filePath]));
- }
-
- @unlink($filePath);
+ $this->extractArchive($filePath, $this->baseDirectory);
}
/**
* Sets the build number and hash
- * @param string $hash
- * @param string $build
- * @param bool $modified
- * @return void
*/
- public function setBuild($build, $hash = null, $modified = false)
+ public function setBuild(string $build, ?string $hash = null, bool $modified = false): void
{
$params = [
'system::core.build' => $build,
@@ -553,37 +514,31 @@ public function setBuild($build, $hash = null, $modified = false)
/**
* Looks up a plugin from the update server.
- * @param string $name Plugin name.
- * @return array Details about the plugin.
*/
- public function requestPluginDetails($name)
+ public function requestPluginDetails(string $name): array
{
return $this->requestServerData('plugin/detail', ['name' => $name]);
}
/**
* Looks up content for a plugin from the update server.
- * @param string $name Plugin name.
- * @return array Content for the plugin.
*/
- public function requestPluginContent($name)
+ public function requestPluginContent(string $name): array
{
return $this->requestServerData('plugin/content', ['name' => $name]);
}
/**
* Runs update on a single plugin
- * @param string $name Plugin name.
- * @return self
*/
- public function updatePlugin($name)
+ public function updatePlugin(string $name): static
{
/*
* Update the plugin database and version
*/
if (!($plugin = $this->pluginManager->findByIdentifier($name))) {
$this->write(Error::class, sprintf('Unable to find plugin %s', $name));
- return;
+ return $this;
}
$this->out(sprintf('Migrating %s (%s) plugin...', Lang::get($plugin->pluginDetails()['name']), $name));
@@ -599,11 +554,9 @@ public function updatePlugin($name)
/**
* Rollback an existing plugin
*
- * @param string $name Plugin name.
- * @param string $stopOnVersion If this parameter is specified, the process stops once the provided version number is reached
- * @return self
+ * @param $stopOnVersion If this parameter is specified, the process stops once the provided version number is reached
*/
- public function rollbackPlugin(string $name, string $stopOnVersion = null)
+ public function rollbackPlugin(string $name, string $stopOnVersion = null): static
{
/*
* Remove the plugin database and version
@@ -640,33 +593,27 @@ public function rollbackPlugin(string $name, string $stopOnVersion = null)
/**
* Downloads a plugin from the update server.
- * @param string $name Plugin name.
- * @param string $hash Expected file hash.
- * @param boolean $installation Indicates whether this is a plugin installation request.
- * @return self
+ * @param $installation Indicates whether this is a plugin installation request.
*/
- public function downloadPlugin($name, $hash, $installation = false)
+ public function downloadPlugin(string $name, string $hash, bool $installation = false): static
{
$fileCode = $name . $hash;
$this->requestServerFile('plugin/get', $fileCode, $hash, [
'name' => $name,
'installation' => $installation ? 1 : 0
]);
+ return $this;
}
/**
* Extracts a plugin after it has been downloaded.
*/
- public function extractPlugin($name, $hash)
+ public function extractPlugin(string $name, string $hash): void
{
$fileCode = $name . $hash;
$filePath = $this->getFilePath($fileCode);
- if (!Zip::extract($filePath, plugins_path())) {
- throw new ApplicationException(Lang::get('system::lang.zip.extract_failed', ['file' => $filePath]));
- }
-
- @unlink($filePath);
+ $this->extractArchive($filePath, plugins_path());
}
//
@@ -675,51 +622,42 @@ public function extractPlugin($name, $hash)
/**
* Looks up a theme from the update server.
- * @param string $name Theme name.
- * @return array Details about the theme.
*/
- public function requestThemeDetails($name)
+ public function requestThemeDetails(string $name): array
{
return $this->requestServerData('theme/detail', ['name' => $name]);
}
/**
* Downloads a theme from the update server.
- * @param string $name Theme name.
- * @param string $hash Expected file hash.
- * @return self
*/
- public function downloadTheme($name, $hash)
+ public function downloadTheme(string $name, string $hash): static
{
$fileCode = $name . $hash;
-
$this->requestServerFile('theme/get', $fileCode, $hash, ['name' => $name]);
+ return $this;
}
/**
* Extracts a theme after it has been downloaded.
*/
- public function extractTheme($name, $hash)
+ public function extractTheme(string $name, string $hash): void
{
$fileCode = $name . $hash;
$filePath = $this->getFilePath($fileCode);
- if (!Zip::extract($filePath, themes_path())) {
- throw new ApplicationException(Lang::get('system::lang.zip.extract_failed', ['file' => $filePath]));
- }
+ $this->extractArchive($filePath, themes_path());
if ($this->themeManager) {
$this->themeManager->setInstalled($name);
}
-
- @unlink($filePath);
}
//
// Products
//
- public function requestProductDetails($codes, $type = null)
+ public function requestProductDetails($codes, $type = null): array
{
if ($type != 'plugin' && $type != 'theme') {
$type = 'plugin';
@@ -771,7 +709,7 @@ public function requestProductDetails($codes, $type = null)
/**
* Returns popular themes found on the marketplace.
*/
- public function requestPopularProducts($type = null)
+ public function requestPopularProducts(string $type = null): array
{
if ($type != 'plugin' && $type != 'theme') {
$type = 'plugin';
@@ -797,7 +735,7 @@ public function requestPopularProducts($type = null)
return $data;
}
- protected function loadProductDetailCache()
+ protected function loadProductDetailCache(): void
{
$defaultCache = ['theme' => [], 'plugin' => []];
$cacheKey = 'system-updates-product-details';
@@ -809,7 +747,7 @@ protected function loadProductDetailCache()
}
}
- protected function saveProductDetailCache()
+ protected function saveProductDetailCache(): void
{
if ($this->productCache === null) {
$this->loadProductDetailCache();
@@ -820,7 +758,7 @@ protected function saveProductDetailCache()
Cache::put($cacheKey, base64_encode(serialize($this->productCache)), $expiresAt);
}
- protected function cacheProductDetail($type, $code, $data)
+ protected function cacheProductDetail(string $type, string $code, array|int $data): void
{
if ($this->productCache === null) {
$this->loadProductDetailCache();
@@ -836,7 +774,7 @@ protected function cacheProductDetail($type, $code, $data)
/**
* Returns the latest changelog information.
*/
- public function requestChangelog()
+ public function requestChangelog(): array
{
$build = Parameter::get('system::core.build');
@@ -876,12 +814,9 @@ public function requestChangelog()
/**
* Writes output to the console using a Laravel CLI View component.
- *
- * @param \Illuminate\Console\View\Components\Component $component
- * @param array $arguments
- * @return static
+ * @param $component Class extending \Illuminate\Console\View\Components\Component to be used to render the message
*/
- protected function write($component, ...$arguments)
+ protected function write(string $component, ...$arguments): static
{
if ($this->notesOutput !== null) {
with(new $component($this->notesOutput))->render(...$arguments);
@@ -892,12 +827,8 @@ protected function write($component, ...$arguments)
/**
* Writes output to the console.
- *
- * @param string $message
- * @param bool $newline
- * @return static
*/
- protected function out($message, $newline = false)
+ protected function out(string $message, bool $newline = false): static
{
if ($this->notesOutput !== null) {
$this->notesOutput->write($message, $newline);
@@ -908,10 +839,8 @@ protected function out($message, $newline = false)
/**
* Sets an output stream for writing notes.
- * @param Illuminate\Console\Command $output
- * @return self
*/
- public function setNotesOutput($output)
+ public function setNotesOutput(OutputStyle $output): static
{
$this->notesOutput = $output;
@@ -924,11 +853,8 @@ public function setNotesOutput($output)
/**
* Contacts the update server for a response.
- * @param string $uri Gateway API URI
- * @param array $postData Extra post data
- * @return array
*/
- public function requestServerData($uri, $postData = [])
+ public function requestServerData(string $uri, array $postData = []): array
{
$result = Http::post($this->createServerUrl($uri), function ($http) use ($postData) {
$this->applyHttpAttributes($http, $postData);
@@ -963,18 +889,21 @@ public function requestServerData($uri, $postData = [])
throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
}
+ if (!is_array($resultData)) {
+ throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
+ }
+
return $resultData;
}
/**
* Downloads a file from the update server.
- * @param string $uri Gateway API URI
- * @param string $fileCode A unique code for saving the file.
- * @param string $expectedHash The expected file hash of the file.
- * @param array $postData Extra post data
- * @return void
+ * @param $uri Gateway API URI
+ * @param $fileCode A unique code for saving the file.
+ * @param $expectedHash The expected file hash of the file.
+ * @param $postData Extra post data
*/
- public function requestServerFile($uri, $fileCode, $expectedHash, $postData = [])
+ public function requestServerFile(string $uri, string $fileCode, string $expectedHash, array $postData = []): void
{
$filePath = $this->getFilePath($fileCode);
@@ -995,10 +924,8 @@ public function requestServerFile($uri, $fileCode, $expectedHash, $postData = []
/**
* Calculates a file path for a file code
- * @param string $fileCode A unique file code
- * @return string Full path on the disk
*/
- protected function getFilePath($fileCode)
+ protected function getFilePath(string $fileCode): string
{
$name = md5($fileCode) . '.arc';
return $this->tempDirectory . '/' . $name;
@@ -1006,10 +933,8 @@ protected function getFilePath($fileCode)
/**
* Set the API security for all transmissions.
- * @param string $key API Key
- * @param string $secret API Secret
*/
- public function setSecurity($key, $secret)
+ public function setSecurity(string $key, string $secret): void
{
$this->key = $key;
$this->secret = $secret;
@@ -1017,10 +942,8 @@ public function setSecurity($key, $secret)
/**
* Create a complete gateway server URL from supplied URI
- * @param string $uri URI
- * @return string URL
*/
- protected function createServerUrl($uri)
+ protected function createServerUrl(string $uri): string
{
$gateway = Config::get('cms.updateServer', 'https://api.wintercms.com/marketplace');
if (substr($gateway, -1) != '/') {
@@ -1032,11 +955,8 @@ protected function createServerUrl($uri)
/**
* Modifies the Network HTTP object with common attributes.
- * @param Http $http Network object
- * @param array $postData Post data
- * @return void
*/
- protected function applyHttpAttributes($http, $postData)
+ protected function applyHttpAttributes(NetworkHttp $http, array $postData): void
{
$postData['protocol_version'] = '1.1';
$postData['client'] = 'october';
@@ -1071,9 +991,8 @@ protected function applyHttpAttributes($http, $postData)
/**
* Create a nonce based on millisecond time
- * @return int
*/
- protected function createNonce()
+ protected function createNonce(): int
{
$mt = explode(' ', microtime());
return $mt[1] . substr($mt[0], 2, 6);
@@ -1081,29 +1000,21 @@ protected function createNonce()
/**
* Create a unique signature for transmission.
- * @return string
*/
- protected function createSignature($data, $secret)
+ protected function createSignature(array $data, string $secret): string
{
return base64_encode(hash_hmac('sha512', http_build_query($data, '', '&'), base64_decode($secret), true));
}
- /**
- * @return string
- */
- public function getMigrationTableName()
+ public function getMigrationTableName(): string
{
return Config::get('database.migrations', 'migrations');
}
/**
* Adds a message from a specific migration or seeder.
- *
- * @param string|object $class
- * @param string|array $message
- * @return void
*/
- protected function addMessage($class, $message)
+ protected function addMessage(string|object $class, string|array $message): void
{
if (empty($message)) {
return;
@@ -1125,10 +1036,8 @@ protected function addMessage($class, $message)
/**
* Prints collated messages from the migrations and seeders
- *
- * @return void
*/
- protected function printMessages()
+ protected function printMessages(): void
{
if (!count($this->messages)) {
return;
@@ -1144,4 +1053,48 @@ protected function printMessages()
$this->out('', true);
}
}
+
+ /**
+ * Extract the provided archive
+ *
+ * @throws ApplicationException if the archive failed to extract
+ */
+ public function extractArchive(string $archive, string $destination): void
+ {
+ if (!Zip::extract($archive, $destination)) {
+ throw new ApplicationException(Lang::get('system::lang.zip.extract_failed', ['file' => $archive]));
+ }
+
+ @unlink($archive);
+ }
+
+ /**
+ * Finds all plugins in a given path by looking for valid Plugin.php files
+ */
+ public function findPluginsInPath(string $path): array
+ {
+ $pluginFiles = [];
+
+ $iterator = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
+ RecursiveIteratorIterator::SELF_FIRST
+ );
+
+ foreach ($iterator as $file) {
+ if ($file->isFile() && $file->getFilename() === 'Plugin.php') {
+ // Attempt to extract the plugin's code
+ if (!preg_match('/namespace (.+?);/', file_get_contents($file->getRealPath()), $match)) {
+ continue;
+ }
+
+ $code = str_replace('\\', '.', $match[1]);
+
+ if (str_contains($code, '.')) {
+ $pluginFiles[$code] = $file->getPathname();
+ }
+ }
+ }
+
+ return $pluginFiles;
+ }
}
diff --git a/modules/system/console/PluginInstall.php b/modules/system/console/PluginInstall.php
index 113fe40268..9a6bbfc44f 100644
--- a/modules/system/console/PluginInstall.php
+++ b/modules/system/console/PluginInstall.php
@@ -3,6 +3,9 @@
use Winter\Storm\Console\Command;
use System\Classes\UpdateManager;
use System\Classes\PluginManager;
+use Throwable;
+use Winter\Storm\Support\Facades\File;
+use Winter\Storm\Support\Str;
/**
* Console command to install a new plugin.
@@ -32,13 +35,99 @@ class PluginInstall extends Command
/**
* Execute the console command.
- * @return void
+ *
+ *
+ * Tests:
+ * plugin.zip - doesn't exist
+ * plugin.zip - does exist
*/
- public function handle()
+ public function handle(): int
{
$pluginName = $this->argument('plugin');
$manager = UpdateManager::instance()->setNotesOutput($this->output);
+ if (Str::endsWith($pluginName, '.zip')) {
+ $packageZip = base_path($pluginName);
+ $tempPath = temp_path('packages/' . md5($packageZip));
+
+ // @TODO: Testing only
+ File::deleteDirectory($tempPath);
+ File::copy(base_path('plugin-test.zip'), $packageZip);
+
+ // Check if the file exists
+ if (!File::exists($packageZip)) {
+ $this->output->writeln(sprintf('File not found: %s', $pluginName));
+ return 1;
+ }
+
+ // Attempt to extract the plugin
+ $manager->extractArchive($packageZip, $tempPath);
+
+ // Look for plugins to install
+ $plugins = $manager->findPluginsInPath($tempPath);
+
+ if (empty($plugins)) {
+ $this->output->writeln(sprintf('No plugins found in: %s', $pluginName));
+ return 1;
+ }
+
+ if (!$this->confirm(sprintf('Detected the following plugins in %s: %s. Would you like to install them all?', $pluginName, Str::join(array_keys($plugins))))) {
+ return 0;
+ };
+
+ $pluginManager = PluginManager::instance();
+
+ foreach ($plugins as $code => $pluginClassFile) {
+ // @TODO: Check if plugin is already installed
+ if ($pluginManager->findByIdentifier($code)) {
+ if (!$this->confirm(sprintf(
+ 'Plugin %s is already installed. Would you like to replace the currently installed version with the one found in %s?',
+ $code,
+ $pluginName
+ ))) {
+ $this->output->writeln(sprintf('Skipping plugin: %s', $code));
+ unset($plugins[$code]);
+ continue;
+ }
+ }
+
+ $this->output->writeln(sprintf('Installing plugin: %s', $code));
+ $pluginDir = plugins_path(strtolower(str_replace('.', DIRECTORY_SEPARATOR, $code)));
+
+ try {
+ // Copy the files
+ File::copyDirectory(pathinfo($pluginClassFile, PATHINFO_DIRNAME), $pluginDir);
+
+ // Load the plugin
+ $pluginManager->loadPlugins();
+ $plugin = $pluginManager->findByIdentifier($code);
+ $pluginManager->registerPlugin($plugin, $code);
+
+ $pluginManager->clearFlagCache();
+ $pluginManager->loadPluginFlags();
+
+ $pluginsToMigrate[] = $code;
+
+ // Run any pending migrations for the plugin
+ // @TODO: May have to add some logic here to just run all pending migrations after completing installing all of the plugins
+ // to account for dependencies. Something else to consider is how we abort a plugin that fails to migrate.
+ $manager->updatePlugin($code);
+
+ $this->output->writeln('');
+ } catch (Throwable $e) {
+ $this->output->writeln(sprintf('Error installing plugin: %s', $code));
+ $this->output->writeln(sprintf('%s', $e->getMessage()));
+ File::deleteDirectory($pluginDir);
+
+ return 1;
+ }
+ }
+
+ $this->output->success(sprintf('Successfully installed %d plugin(s) from %s', count($plugins), $pluginName));
+
+ return 0;
+ }
+
$pluginDetails = $manager->requestPluginDetails($pluginName);
$code = array_get($pluginDetails, 'code');
@@ -50,18 +139,16 @@ public function handle()
$this->output->writeln(sprintf('Unpacking plugin: %s', $code));
$manager->extractPlugin($code, $hash);
- /*
- * Make sure plugin is registered
- */
+ // Make sure plugin is registered
$pluginManager = PluginManager::instance();
$pluginManager->loadPlugins();
$plugin = $pluginManager->findByIdentifier($code);
$pluginManager->registerPlugin($plugin, $code);
- /*
- * Migrate plugin
- */
+ // Migrate plugin
$this->output->writeln(sprintf('Migrating plugin...', $code));
$manager->updatePlugin($code);
+
+ return 0;
}
}
diff --git a/modules/system/controllers/Updates.php b/modules/system/controllers/Updates.php
index f27e1e4f5d..dd7a68e466 100644
--- a/modules/system/controllers/Updates.php
+++ b/modules/system/controllers/Updates.php
@@ -1,30 +1,37 @@
-vars['coreBuild'] = Parameter::get('system::core.build');
$this->vars['coreBuildModified'] = Parameter::get('system::core.modified', false);
@@ -80,23 +87,23 @@ public function index()
$this->vars['projectOwner'] = Parameter::get('system::project.owner');
$this->vars['pluginsActiveCount'] = PluginVersion::applyEnabled()->count();
$this->vars['pluginsCount'] = PluginVersion::count();
- return $this->asExtension('ListController')->index();
+ $this->asExtension('ListController')->index();
}
/**
* Plugin manage controller
*/
- public function manage()
+ public function manage(): void
{
$this->pageTitle = 'system::lang.plugins.manage';
PluginManager::instance()->clearFlagCache();
- return $this->asExtension('ListController')->index();
+ $this->asExtension('ListController')->index();
}
/**
* Install new plugins / themes
*/
- public function install($tab = null)
+ public function install($tab = null): ?HttpResponse
{
if (get('search')) {
return Response::make($this->onSearchProducts());
@@ -112,13 +119,16 @@ public function install($tab = null)
$this->vars['activeTab'] = $tab ?: 'plugins';
$this->vars['installedPlugins'] = $this->getInstalledPlugins();
$this->vars['installedThemes'] = $this->getInstalledThemes();
+ $this->vars['packageUploadWidget'] = $this->getPackageUploadWidget($tab === 'themes' ? 'theme' : 'plugin');
}
catch (Exception $ex) {
$this->handleError($ex);
}
+
+ return null;
}
- public function details($urlCode = null, $tab = null)
+ public function details($urlCode = null, $tab = null): void
{
try {
$this->pageTitle = 'system::lang.updates.details_title';
@@ -178,15 +188,15 @@ public function details($urlCode = null, $tab = null)
}
}
- protected function getPluginMarkdownFile($path, $filenames)
+ protected function getPluginMarkdownFile(string $path, array $filenames): ?string
{
$contents = null;
foreach ($filenames as $file) {
- if (!File::exists($path . '/'.$file)) {
+ if (!File::exists($path . '/' . $file)) {
continue;
}
- $contents = File::get($path . '/'.$file);
+ $contents = File::get($path . '/' . $file);
/*
* Parse markdown, clean HTML, remove first H1 tag
@@ -199,7 +209,7 @@ protected function getPluginMarkdownFile($path, $filenames)
return $contents;
}
- protected function getWarnings()
+ protected function getWarnings(): array
{
$warnings = [];
$missingDependencies = PluginManager::instance()->findMissingDependencies();
@@ -242,9 +252,8 @@ protected function getWarnings()
* - positive - Default CSS class
*
* @see Backend\Behaviors\ListController
- * @return string
*/
- public function listInjectRowClass($record, $definition = null)
+ public function listInjectRowClass($record, $definition = null): string
{
if ($record->disabledByConfig) {
return 'hidden';
@@ -255,7 +264,7 @@ public function listInjectRowClass($record, $definition = null)
}
if ($definition != 'manage') {
- return;
+ return '';
}
if ($record->disabledBySystem) {
@@ -272,7 +281,7 @@ public function listInjectRowClass($record, $definition = null)
/**
* Runs a specific update step.
*/
- public function onExecuteStep()
+ public function onExecuteStep(): ?RedirectResponse
{
/*
* Address timeout limits
@@ -321,6 +330,8 @@ public function onExecuteStep()
Flash::success(Lang::get('system::lang.install.install_success'));
return Redirect::refresh();
}
+
+ return null;
}
//
@@ -330,7 +341,7 @@ public function onExecuteStep()
/**
* Spawns the update checker popup.
*/
- public function onLoadUpdates()
+ public function onLoadUpdates(): string
{
return $this->makePartial('update_form');
}
@@ -338,7 +349,7 @@ public function onLoadUpdates()
/**
* Contacts the update server for a list of necessary updates.
*/
- public function onCheckForUpdates()
+ public function onCheckForUpdates(): array
{
try {
$manager = UpdateManager::instance();
@@ -362,10 +373,8 @@ public function onCheckForUpdates()
/**
* Loops the update list and checks for actionable updates.
- * @param array $result
- * @return array
*/
- protected function processImportantUpdates($result)
+ protected function processImportantUpdates(array $result): array
{
$hasImportantUpdates = false;
@@ -416,10 +425,8 @@ protected function processImportantUpdates($result)
/**
* Reverses the update lists for the core and all plugins.
- * @param array $result
- * @return array
*/
- protected function processUpdateLists($result)
+ protected function processUpdateLists(array $result): array
{
if ($core = array_get($result, 'core')) {
$result['core']['updates'] = array_reverse(array_get($core, 'updates', []), true);
@@ -435,10 +442,9 @@ protected function processUpdateLists($result)
/**
* Contacts the update server for a list of necessary updates.
*
- * @param bool $force Whether or not to force the redownload of existing tools
- * @return string The rendered "execute" partial
+ * @param $force Whether or not to force the redownload of existing tools
*/
- public function onForceUpdate($force = true)
+ public function onForceUpdate(bool $force = true): string
{
try {
$manager = UpdateManager::instance();
@@ -485,7 +491,7 @@ public function onForceUpdate($force = true)
/**
* Converts the update data to an actionable array of steps.
*/
- public function onApplyUpdates()
+ public function onApplyUpdates(): string
{
try {
/*
@@ -577,7 +583,7 @@ public function onApplyUpdates()
return $this->makePartial('execute');
}
- protected function buildUpdateSteps($core, $plugins, $themes, $isInstallationRequest)
+ protected function buildUpdateSteps($core, $plugins, $themes, $isInstallationRequest): array
{
if (!is_array($core)) {
$core = [null, null];
@@ -669,7 +675,7 @@ protected function buildUpdateSteps($core, $plugins, $themes, $isInstallationReq
/**
* Displays the form for entering a Project ID
*/
- public function onLoadProjectForm()
+ public function onLoadProjectForm(): string
{
return $this->makePartial('project_form');
}
@@ -677,7 +683,7 @@ public function onLoadProjectForm()
/**
* Validate the project ID and execute the project installation
*/
- public function onAttachProject()
+ public function onAttachProject(): string
{
try {
if (!$projectId = trim(post('project_id'))) {
@@ -701,7 +707,7 @@ public function onAttachProject()
}
}
- public function onDetachProject()
+ public function onDetachProject(): RedirectResponse
{
Parameter::set([
'system::project.id' => null,
@@ -719,8 +725,10 @@ public function onDetachProject()
/**
* Displays changelog information
+ *
+ * @throws ApplicationException if the changelog could not be fetched from the server
*/
- public function onLoadChangelog()
+ public function onLoadChangelog(): string
{
try {
$fetchedContent = UpdateManager::instance()->requestChangelog();
@@ -744,10 +752,117 @@ public function onLoadChangelog()
// Plugin management
//
+ protected ?Form $packageUploadWidget = null;
+
+ /**
+ * Get the form widget for the import popup.
+ */
+ protected function getPackageUploadWidget(string $type = 'plugin'): Form
+ {
+ $type = post('type', $type);
+
+ if (!in_array($type, ['plugin', 'theme'])) {
+ throw new ApplicationException('Invalid package type');
+ }
+
+ if ($this->packageUploadWidget !== null) {
+ return $this->packageUploadWidget;
+ }
+
+ $config = $this->makeConfig("form.{$type}_upload.yaml");
+ $config->model = new class extends Model {
+ public $attachOne = [
+ 'uploaded_package' => [\System\Models\File::class, 'public' => false],
+ ];
+ };
+ $widget = $this->makeWidget(Form::class, $config);
+ $widget->bindToController();
+
+ return $this->packageUploadWidget = $widget;
+ }
+
+ /**
+ * Displays the plugin uploader form
+ */
+ public function onLoadPluginUploader(): string
+ {
+ $this->vars['packageUploadWidget'] = $this->getPackageUploadWidget('plugin');
+ return $this->makePartial('popup_upload_plugin');
+ }
+
+ /**
+ * Installs an uploaded plugin
+ */
+ public function onInstallUploadedPlugin(): string
+ {
+ try {
+ // Get the deferred binding record for the uploaded file
+ $widget = $this->getPackageUploadWidget();
+ $class = Str::before(get_class($widget->model), chr(0));
+ $deferred = DeferredBinding::query()
+ ->where('master_type', 'LIKE', str_replace('\\', '\\\\', $class) . '%')
+ ->where('master_field', 'uploaded_package')
+ ->where('session_key', $widget->getSessionKey())
+ ->first();
+
+ // Attempt to get the file from the deferred binding
+ if (!$deferred || !$deferred->slave) {
+ throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
+ }
+ $file = $deferred->slave;
+
+ /**
+ * @TODO:
+ * - Process the uploaded file to identify the plugins to install
+ * - (optional) require confirmation to install each detected plugin
+ * - Install the identified plugins
+ * - Ensure that deferred binding records and uploaded files are removed post processing or on failure
+ */
+
+ $manager = UpdateManager::instance();
+
+ $result = $manager->installUploadedPlugin();
+
+ if (!isset($result['code']) || !isset($result['hash'])) {
+ throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
+ }
+
+ $name = $result['code'];
+ $hash = $result['hash'];
+ $plugins = [$name => $hash];
+ $plugins = $this->appendRequiredPlugins($plugins, $result);
+
+ /*
+ * Update steps
+ */
+ $updateSteps = $this->buildUpdateSteps(null, $plugins, [], true);
+
+ /*
+ * Finish up
+ */
+ $updateSteps[] = [
+ 'code' => 'completeInstall',
+ 'label' => Lang::get('system::lang.install.install_completing'),
+ ];
+
+ $this->vars['updateSteps'] = $updateSteps;
+
+ return $this->makePartial('execute');
+ }
+ catch (Exception $ex) {
+ // @TODO: Remove this, temporary debugging
+ throw $ex;
+ $this->handleError($ex);
+ return $this->makePartial('plugin_uploader');
+ }
+ }
+
/**
* Validate the plugin code and execute the plugin installation
+ *
+ * @throws ApplicationException If validation fails or the plugin cannot be installed
*/
- public function onInstallPlugin()
+ public function onInstallPlugin(): string
{
try {
if (!$code = trim(post('code'))) {
@@ -791,9 +906,8 @@ public function onInstallPlugin()
/**
* Rollback and remove a single plugin from the system.
- * @return void
*/
- public function onRemovePlugin()
+ public function onRemovePlugin(): RedirectResponse
{
if ($pluginCode = post('code')) {
PluginManager::instance()->deletePlugin($pluginCode);
@@ -805,9 +919,8 @@ public function onRemovePlugin()
/**
* Perform a bulk action on the provided plugins
- * @return void
*/
- public function onBulkAction()
+ public function onBulkAction(): RedirectResponse
{
if (($bulkAction = post('action')) &&
($checkedIds = post('checked')) &&
@@ -912,9 +1025,8 @@ public function onInstallTheme()
/**
* Deletes a single theme from the system.
- * @return void
*/
- public function onRemoveTheme()
+ public function onRemoveTheme(): RedirectResponse
{
if ($themeCode = post('code')) {
ThemeManager::instance()->deleteTheme($themeCode);
@@ -929,7 +1041,7 @@ public function onRemoveTheme()
// Product install
//
- public function onSearchProducts()
+ public function onSearchProducts(): array
{
$searchType = get('search', 'plugins');
$serverUri = $searchType == 'plugins' ? 'plugin/search' : 'theme/search';
@@ -938,7 +1050,7 @@ public function onSearchProducts()
return $manager->requestServerData($serverUri, ['query' => get('query')]);
}
- public function onGetPopularPlugins()
+ public function onGetPopularPlugins(): array
{
$installed = $this->getInstalledPlugins();
$popular = UpdateManager::instance()->requestPopularProducts('plugin');
@@ -947,7 +1059,7 @@ public function onGetPopularPlugins()
return ['result' => $popular];
}
- public function onGetPopularThemes()
+ public function onGetPopularThemes(): array
{
$installed = $this->getInstalledThemes();
$popular = UpdateManager::instance()->requestPopularProducts('theme');
@@ -956,14 +1068,14 @@ public function onGetPopularThemes()
return ['result' => $popular];
}
- protected function getInstalledPlugins()
+ protected function getInstalledPlugins(): array
{
$installed = PluginVersion::lists('code');
$manager = UpdateManager::instance();
return $manager->requestProductDetails($installed, 'plugin');
}
- protected function getInstalledThemes()
+ protected function getInstalledThemes(): array
{
$history = Parameter::get('system::theme.history', []);
$manager = UpdateManager::instance();
@@ -983,7 +1095,7 @@ protected function getInstalledThemes()
/*
* Remove installed products from the collection
*/
- protected function filterPopularProducts($popular, $installed)
+ protected function filterPopularProducts($popular, $installed): array
{
$installedArray = [];
foreach ($installed as $product) {
@@ -1007,7 +1119,7 @@ protected function filterPopularProducts($popular, $installed)
/**
* Encode HTML safe product code, this is to prevent issues with array_get().
*/
- protected function encodeCode($code)
+ protected function encodeCode(string $code): string
{
return str_replace('.', ':', $code);
}
@@ -1015,18 +1127,15 @@ protected function encodeCode($code)
/**
* Decode HTML safe product code.
*/
- protected function decodeCode($code)
+ protected function decodeCode(string $code): string
{
return str_replace(':', '.', $code);
}
/**
* Adds require plugin codes to the collection based on a result.
- * @param array $plugins
- * @param array $result
- * @return array
*/
- protected function appendRequiredPlugins(array $plugins, array $result)
+ protected function appendRequiredPlugins(array $plugins, array $result): array
{
foreach ((array) array_get($result, 'require') as $plugin) {
if (
diff --git a/modules/system/controllers/updates/_install_plugins.php b/modules/system/controllers/updates/_install_plugins.php
index 461ff78d3f..427f1ff002 100644
--- a/modules/system/controllers/updates/_install_plugins.php
+++ b/modules/system/controllers/updates/_install_plugins.php
@@ -1,23 +1,38 @@