ORIENT/modules/system/classes/UpdateManager.php

1110 lines
30 KiB
PHP
Raw Normal View History

2014-05-14 13:24:20 +00:00
<?php namespace System\Classes;
use App;
use Url;
use File;
2014-05-14 13:24:20 +00:00
use Lang;
use Http;
use Cache;
2014-05-14 13:24:20 +00:00
use Schema;
use Config;
2022-12-14 15:55:13 +00:00
use Request;
use ApplicationException;
use Cms\Classes\ThemeManager;
use System\Models\Parameter;
2014-05-14 13:24:20 +00:00
use System\Models\PluginVersion;
use System\Helpers\Cache as CacheHelper;
2014-05-14 13:24:20 +00:00
use October\Rain\Filesystem\Zip;
use Carbon\Carbon;
2014-05-14 13:24:20 +00:00
use Exception;
/**
* Update manager
*
* Handles the CMS install and update process.
*
* @package october\system
* @author Alexey Bobkov, Samuel Georges
*/
class UpdateManager
{
use \October\Rain\Support\Traits\Singleton;
2017-05-19 23:03:58 +00:00
/**
* @var \Illuminate\Console\OutputStyle
*/
protected $notesOutput;
2014-05-14 13:24:20 +00:00
/**
* @var string Application base path.
*/
protected $baseDirectory;
/**
* @var string A temporary working directory.
*/
protected $tempDirectory;
/**
2020-04-11 15:46:22 +00:00
* @var \System\Classes\PluginManager
2014-05-14 13:24:20 +00:00
*/
protected $pluginManager;
/**
2020-04-11 15:46:22 +00:00
* @var \Cms\Classes\ThemeManager
*/
protected $themeManager;
2014-05-14 13:24:20 +00:00
/**
2020-04-11 15:46:22 +00:00
* @var \System\Classes\VersionManager
2014-05-14 13:24:20 +00:00
*/
protected $versionManager;
/**
* @var string Secure API Key
*/
protected $key;
/**
* @var string Secure API Secret
*/
protected $secret;
/**
* @var boolean If set to true, core updates will not be downloaded or extracted.
*/
protected $disableCoreUpdates = false;
/**
* @var array Cache of gateway products
*/
protected $productCache;
2017-05-13 10:43:44 +00:00
/**
2020-04-11 15:46:22 +00:00
* @var \Illuminate\Database\Migrations\Migrator
2017-05-13 10:43:44 +00:00
*/
protected $migrator;
/**
2020-04-11 15:46:22 +00:00
* @var \Illuminate\Database\Migrations\DatabaseMigrationRepository
2017-05-13 10:43:44 +00:00
*/
protected $repository;
2022-12-14 15:55:13 +00:00
/**
* @var array An array of messages returned by migrations / seeders. Returned at the end of the update process.
*/
protected $messages = [];
2014-05-14 13:24:20 +00:00
/**
* Initialize this singleton.
*/
protected function init()
{
$this->pluginManager = PluginManager::instance();
$this->themeManager = class_exists(ThemeManager::class) ? ThemeManager::instance() : null;
2014-05-14 13:24:20 +00:00
$this->versionManager = VersionManager::instance();
2015-02-07 04:42:20 +00:00
$this->tempDirectory = temp_path();
$this->baseDirectory = base_path();
$this->disableCoreUpdates = Config::get('cms.disableCoreUpdates', false);
$this->bindContainerObjects();
/*
* Ensure temp directory exists
*/
2014-10-18 09:58:50 +00:00
if (!File::isDirectory($this->tempDirectory)) {
File::makeDirectory($this->tempDirectory, 0777, true);
2014-10-18 09:58:50 +00:00
}
2014-05-14 13:24:20 +00:00
}
2015-02-21 00:43:17 +00:00
/**
* 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()
2015-02-21 00:41:43 +00:00
{
$this->migrator = App::make('migrator');
$this->repository = App::make('migration.repository');
}
2014-05-14 13:24:20 +00:00
/**
* Creates the migration table and updates
* @return self
*/
public function update()
{
2017-01-12 19:15:59 +00:00
$firstUp = !Schema::hasTable($this->getMigrationTableName());
2014-05-14 13:24:20 +00:00
if ($firstUp) {
$this->repository->createRepository();
$this->note('Migration table created');
2014-05-14 13:24:20 +00:00
}
/*
* Update modules
*/
$modules = Config::get('cms.loadModules', []);
2014-10-18 09:58:50 +00:00
foreach ($modules as $module) {
2014-05-14 13:24:20 +00:00
$this->migrateModule($module);
2014-10-18 09:58:50 +00:00
}
2014-05-14 13:24:20 +00:00
/*
* Update plugins
*/
$plugins = $this->pluginManager->getPlugins();
foreach ($plugins as $code => $plugin) {
$this->updatePlugin($code);
2014-05-14 13:24:20 +00:00
}
Parameter::set('system::update.count', 0);
CacheHelper::clear();
2014-05-14 13:24:20 +00:00
/*
* Seed modules
*/
if ($firstUp) {
$modules = Config::get('cms.loadModules', []);
2014-10-18 09:58:50 +00:00
foreach ($modules as $module) {
2014-05-14 13:24:20 +00:00
$this->seedModule($module);
2014-10-18 09:58:50 +00:00
}
2014-05-14 13:24:20 +00:00
}
2022-12-14 15:55:13 +00:00
// Print messages returned by migrations / seeders
$this->printMessages();
2014-05-14 13:24:20 +00:00
return $this;
}
/**
* Checks for new updates and returns the amount of unapplied updates.
* Only requests from the server at a set interval (retry timer).
2020-04-11 15:46:22 +00:00
* @param boolean $force Ignore the retry timer.
2014-05-14 13:24:20 +00:00
* @return int Number of unapplied updates.
*/
public function check($force = false)
{
/*
* Already know about updates, never retry.
*/
$oldCount = Parameter::get('system::update.count');
2014-10-18 09:58:50 +00:00
if ($oldCount > 0) {
2014-05-14 13:24:20 +00:00
return $oldCount;
2014-10-18 09:58:50 +00:00
}
2014-05-14 13:24:20 +00:00
/*
* Retry period not passed, skipping.
*/
2018-08-15 16:45:37 +00:00
if (!$force
&& ($retryTimestamp = Parameter::get('system::update.retry'))
&& Carbon::createFromTimeStamp($retryTimestamp)->isFuture()
) {
return $oldCount;
2014-05-14 13:24:20 +00:00
}
try {
$result = $this->requestUpdateList();
$newCount = array_get($result, 'update', 0);
2020-04-11 15:46:22 +00:00
} catch (Exception $ex) {
2014-05-14 13:24:20 +00:00
$newCount = 0;
}
/*
* Remember update count, set retry date
*/
Parameter::set('system::update.count', $newCount);
Parameter::set('system::update.retry', Carbon::now()->addHours(24)->timestamp);
2014-05-14 13:24:20 +00:00
return $newCount;
}
/**
* Requests an update list used for checking for new updates.
2020-04-11 15:46:22 +00:00
* @param boolean $force Request application and plugins hash list regardless of version.
2014-05-14 13:24:20 +00:00
* @return array
*/
public function requestUpdateList($force = false)
{
$installed = PluginVersion::all();
$versions = $installed->lists('version', 'code');
$names = $installed->lists('name', 'code');
2015-06-30 09:06:28 +00:00
$icons = $installed->lists('icon', 'code');
$frozen = $installed->lists('is_frozen', 'code');
$updatable = $installed->lists('is_updatable', 'code');
$build = Parameter::get('system::core.build');
$themes = [];
if ($this->themeManager) {
$themes = array_keys($this->themeManager->getInstalled());
}
2014-05-14 13:24:20 +00:00
$params = [
2020-04-11 15:46:22 +00:00
'core' => $this->getHash(),
2021-04-16 09:15:27 +00:00
'plugins' => base64_encode(json_encode($versions)),
'themes' => base64_encode(json_encode($themes)),
2020-04-11 15:46:22 +00:00
'build' => $build,
'force' => $force
2014-05-14 13:24:20 +00:00
];
$result = $this->requestServerData('core/update', $params);
$updateCount = (int) array_get($result, 'update', 0);
2014-05-14 13:24:20 +00:00
/*
* Inject known core build
*/
if ($core = array_get($result, 'core')) {
$core['old_build'] = Parameter::get('system::core.build');
2014-05-14 13:24:20 +00:00
$result['core'] = $core;
}
/*
* Inject the application's known plugin name and version
*/
$plugins = [];
foreach (array_get($result, 'plugins', []) as $code => $info) {
$info['name'] = $names[$code] ?? $code;
$info['old_version'] = $versions[$code] ?? false;
$info['icon'] = $icons[$code] ?? false;
/*
* If a plugin has updates frozen, or cannot be updated,
* do not add to the list and discount an update unit.
*/
if (
(isset($frozen[$code]) && $frozen[$code]) ||
(isset($updatable[$code]) && !$updatable[$code])
) {
$updateCount = max(0, --$updateCount);
2020-04-11 15:46:22 +00:00
} else {
$plugins[$code] = $info;
}
2014-05-14 13:24:20 +00:00
}
$result['plugins'] = $plugins;
/*
* Strip out themes that have been installed before
*/
if ($this->themeManager) {
$themes = [];
foreach (array_get($result, 'themes', []) as $code => $info) {
if (!$this->themeManager->isInstalled($code)) {
$themes[$code] = $info;
}
2014-10-18 09:58:50 +00:00
}
$result['themes'] = $themes;
}
/*
* If there is a core update and core updates are disabled,
* remove the entry and discount an update unit.
*/
if (array_get($result, 'core') && $this->disableCoreUpdates) {
$updateCount = max(0, --$updateCount);
unset($result['core']);
}
/*
* Recalculate the update counter
*/
$updateCount += count($themes);
$result['hasUpdates'] = $updateCount > 0;
$result['update'] = $updateCount;
Parameter::set('system::update.count', $updateCount);
2014-05-14 13:24:20 +00:00
return $result;
}
/**
* Requests details about a project based on its identifier.
2020-04-11 15:46:22 +00:00
* @param string $projectId
2014-05-14 13:24:20 +00:00
* @return array
*/
public function requestProjectDetails($projectId)
{
2017-07-29 05:33:51 +00:00
return $this->requestServerData('project/detail', ['id' => $projectId]);
2014-05-14 13:24:20 +00:00
}
/**
* Roll back all modules and plugins.
* @return self
*/
public function uninstall()
{
/*
* Rollback plugins
*/
2022-12-14 15:55:13 +00:00
$plugins = array_reverse($this->pluginManager->getPlugins());
2014-05-14 13:24:20 +00:00
foreach ($plugins as $name => $plugin) {
$this->rollbackPlugin($name);
}
/*
* Register module migration files
*/
2017-05-13 10:43:44 +00:00
$paths = [];
2014-05-14 13:24:20 +00:00
$modules = Config::get('cms.loadModules', []);
2017-05-13 10:43:44 +00:00
2014-05-14 13:24:20 +00:00
foreach ($modules as $module) {
2020-04-11 15:46:22 +00:00
$paths[] = $path = base_path() . '/modules/' . strtolower($module) . '/database/migrations';
2014-05-14 13:24:20 +00:00
}
/*
* Rollback modules
*/
2022-12-14 15:55:13 +00:00
if (isset($this->notesOutput)) {
$this->migrator->setOutput($this->notesOutput);
}
2020-02-27 08:58:11 +00:00
while (true) {
$rolledBack = $this->migrator->rollback($paths, ['pretend' => false]);
2014-05-14 13:24:20 +00:00
2017-05-13 10:43:44 +00:00
if (count($rolledBack) == 0) {
2014-10-18 09:58:50 +00:00
break;
}
2014-05-14 13:24:20 +00:00
}
2017-01-12 19:15:59 +00:00
Schema::dropIfExists($this->getMigrationTableName());
2014-05-14 13:24:20 +00:00
return $this;
}
/**
2022-12-14 15:55:13 +00:00
* Determines build number from source manifest.
*
* This will return an array with the following information:
* - `build`: The build number we determined was most likely the build installed.
* - `modified`: Whether we detected any modifications between the installed build and the manifest.
* - `confident`: Whether we are at least 60% sure that this is the installed build. More modifications to
* 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
*/
2022-12-14 15:55:13 +00:00
public function getBuildNumberManually($detailed = false)
{
2022-12-14 15:55:13 +00:00
$source = new SourceManifest();
$manifest = new FileManifest(null, null, true);
2022-12-14 15:55:13 +00:00
// Find build by comparing with source manifest
return $source->compare($manifest, $detailed);
}
2021-03-11 10:16:57 +00:00
2022-12-14 15:55:13 +00:00
/**
* 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
*/
public function setBuildNumberManually($detailed = false)
{
$build = $this->getBuildNumberManually($detailed);
2021-03-11 10:16:57 +00:00
2022-12-14 15:55:13 +00:00
if ($build['confident']) {
$this->setBuild($build['build'], null, $build['modified']);
}
2021-03-11 10:16:57 +00:00
return $build;
}
2014-05-14 13:24:20 +00:00
//
// Modules
//
/**
* Returns the currently installed system hash.
* @return string
*/
public function getHash()
{
return Parameter::get('system::core.hash', md5('NULL'));
2014-05-14 13:24:20 +00:00
}
/**
* Run migrations on a single module
* @param string $module Module name
* @return self
*/
public function migrateModule($module)
{
2022-12-14 15:55:13 +00:00
if (isset($this->notesOutput)) {
$this->migrator->setOutput($this->notesOutput);
}
2020-02-27 08:58:11 +00:00
$this->note($module);
2022-12-14 15:55:13 +00:00
$this->migrator->run(base_path() . '/modules/'.strtolower($module).'/database/migrations');
2017-05-19 23:03:58 +00:00
2014-05-14 13:24:20 +00:00
return $this;
}
/**
* Run seeds on a module
* @param string $module Module name
* @return self
*/
public function seedModule($module)
{
2020-04-11 15:46:22 +00:00
$className = '\\' . $module . '\Database\Seeds\DatabaseSeeder';
2014-10-18 09:58:50 +00:00
if (!class_exists($className)) {
2014-05-14 13:24:20 +00:00
return;
2014-10-18 09:58:50 +00:00
}
2014-05-14 13:24:20 +00:00
$seeder = App::make($className);
2022-12-14 15:55:13 +00:00
$return = $seeder->run();
if (isset($return) && (is_string($return) || is_array($return))) {
$this->addMessage($className, $return);
}
2014-05-14 13:24:20 +00:00
$this->note(sprintf('<info>Seeded %s</info> ', $module));
return $this;
}
/**
* Downloads the core from the update server.
* @param string $hash Expected file hash.
* @return void
*/
public function downloadCore($hash)
{
$this->requestServerFile('core/get', 'core', $hash, ['type' => 'update']);
}
/**
* Extracts the core after it has been downloaded.
* @return void
*/
2017-07-29 06:21:34 +00:00
public function extractCore()
2014-05-14 13:24:20 +00:00
{
$filePath = $this->getFilePath('core');
2014-10-18 09:58:50 +00:00
if (!Zip::extract($filePath, $this->baseDirectory)) {
2014-05-14 13:24:20 +00:00
throw new ApplicationException(Lang::get('system::lang.zip.extract_failed', ['file' => $filePath]));
2014-10-18 09:58:50 +00:00
}
2014-05-14 13:24:20 +00:00
@unlink($filePath);
}
/**
* Sets the build number and hash
* @param string $hash
* @param string $build
2022-12-14 15:55:13 +00:00
* @param bool $modified
* @return void
*/
2022-12-14 15:55:13 +00:00
public function setBuild($build, $hash = null, $modified = false)
{
$params = [
2022-12-14 15:55:13 +00:00
'system::core.build' => $build,
'system::core.modified' => $modified,
];
if ($hash) {
$params['system::core.hash'] = $hash;
}
Parameter::set($params);
2014-05-14 13:24:20 +00:00
}
//
// Plugins
//
/**
* Looks up a plugin from the update server.
* @param string $name Plugin name.
* @return array Details about the plugin.
*/
public function requestPluginDetails($name)
{
2017-07-29 05:33:51 +00:00
return $this->requestServerData('plugin/detail', ['name' => $name]);
2014-05-14 13:24:20 +00:00
}
/**
* 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)
{
2017-07-29 05:33:51 +00:00
return $this->requestServerData('plugin/content', ['name' => $name]);
}
2014-05-14 13:24:20 +00:00
/**
* Runs update on a single plugin
* @param string $name Plugin name.
* @return self
*/
public function updatePlugin($name)
{
/*
* Update the plugin database and version
*/
if (!($plugin = $this->pluginManager->findByIdentifier($name))) {
$this->note('<error>Unable to find:</error> ' . $name);
return;
}
2017-05-19 23:03:58 +00:00
$this->note($name);
2022-12-14 15:55:13 +00:00
$this->versionManager->setNotesOutput($this->notesOutput);
2017-05-19 23:03:58 +00:00
2022-12-14 15:55:13 +00:00
$this->versionManager->updatePlugin($plugin);
2017-05-19 23:03:58 +00:00
2014-05-14 13:24:20 +00:00
return $this;
}
/**
2020-04-11 15:46:22 +00:00
* Rollback an existing plugin
*
2014-05-14 13:24:20 +00:00
* @param string $name Plugin name.
2020-04-11 15:46:22 +00:00
* @param string $stopOnVersion If this parameter is specified, the process stops once the provided version number is reached
2014-05-14 13:24:20 +00:00
* @return self
*/
2020-04-11 15:46:22 +00:00
public function rollbackPlugin(string $name, string $stopOnVersion = null)
2014-05-14 13:24:20 +00:00
{
/*
* Remove the plugin database and version
*/
2018-08-15 16:45:37 +00:00
if (!($plugin = $this->pluginManager->findByIdentifier($name))
&& $this->versionManager->purgePlugin($name)
) {
$this->note('<info>Purged from database:</info> ' . $name);
return $this;
2014-05-14 13:24:20 +00:00
}
2020-04-11 15:46:22 +00:00
if ($stopOnVersion && !$this->versionManager->hasDatabaseVersion($plugin, $stopOnVersion)) {
throw new ApplicationException(Lang::get('system::lang.updates.plugin_version_not_found'));
}
if ($this->versionManager->removePlugin($plugin, $stopOnVersion, true)) {
2014-05-14 13:24:20 +00:00
$this->note('<info>Rolled back:</info> ' . $name);
2020-04-11 15:46:22 +00:00
if ($currentVersion = $this->versionManager->getCurrentVersion($plugin)) {
$this->note('<info>Current Version:</info> ' . $currentVersion . ' (' . $this->versionManager->getCurrentVersionNote($plugin) . ')');
}
return $this;
}
2014-05-14 13:24:20 +00:00
$this->note('<error>Unable to find:</error> ' . $name);
2017-07-29 05:33:51 +00:00
2014-05-14 13:24:20 +00:00
return $this;
}
/**
* 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.
2014-05-14 13:24:20 +00:00
* @return self
*/
public function downloadPlugin($name, $hash, $installation = false)
2014-05-14 13:24:20 +00:00
{
$fileCode = $name . $hash;
$this->requestServerFile('plugin/get', $fileCode, $hash, [
2020-04-11 15:46:22 +00:00
'name' => $name,
'installation' => $installation ? 1 : 0
]);
2014-05-14 13:24:20 +00:00
}
/**
* Extracts a plugin after it has been downloaded.
*/
public function extractPlugin($name, $hash)
{
$fileCode = $name . $hash;
$filePath = $this->getFilePath($fileCode);
2021-04-16 09:15:27 +00:00
$innerPath = str_replace('.', '/', strtolower($name));
2014-05-14 13:24:20 +00:00
2021-04-16 09:15:27 +00:00
if (!Zip::extract($filePath, plugins_path($innerPath))) {
2014-05-14 13:24:20 +00:00
throw new ApplicationException(Lang::get('system::lang.zip.extract_failed', ['file' => $filePath]));
2014-10-18 09:58:50 +00:00
}
2014-05-14 13:24:20 +00:00
@unlink($filePath);
}
//
// Themes
//
/**
* Looks up a theme from the update server.
* @param string $name Theme name.
* @return array Details about the theme.
*/
public function requestThemeDetails($name)
{
2017-07-29 05:33:51 +00:00
return $this->requestServerData('theme/detail', ['name' => $name]);
}
2014-07-24 10:07:52 +00:00
/**
* 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)
{
$fileCode = $name . $hash;
2017-07-29 05:33:51 +00:00
2014-07-24 10:07:52 +00:00
$this->requestServerFile('theme/get', $fileCode, $hash, ['name' => $name]);
}
/**
* Extracts a theme after it has been downloaded.
*/
public function extractTheme($name, $hash)
{
$fileCode = $name . $hash;
$filePath = $this->getFilePath($fileCode);
2021-04-16 09:15:27 +00:00
$innerPath = str_replace('.', '-', strtolower($name));
2014-07-24 10:07:52 +00:00
2021-04-16 09:15:27 +00:00
if (!Zip::extract($filePath, themes_path($innerPath))) {
2014-07-24 10:07:52 +00:00
throw new ApplicationException(Lang::get('system::lang.zip.extract_failed', ['file' => $filePath]));
2014-10-18 09:58:50 +00:00
}
2014-07-24 10:07:52 +00:00
if ($this->themeManager) {
$this->themeManager->setInstalled($name);
2017-07-14 11:32:48 +00:00
}
2014-07-24 10:07:52 +00:00
@unlink($filePath);
}
//
// Products
//
public function requestProductDetails($codes, $type = null)
{
2017-07-29 05:33:51 +00:00
if ($type != 'plugin' && $type != 'theme') {
$type = 'plugin';
2017-07-29 05:33:51 +00:00
}
$codes = (array) $codes;
$this->loadProductDetailCache();
/*
* New products requested
*/
$newCodes = array_diff($codes, array_keys($this->productCache[$type]));
if (count($newCodes)) {
$dataCodes = [];
2020-04-11 15:46:22 +00:00
$data = $this->requestServerData($type . '/details', ['names' => $newCodes]);
foreach ($data as $product) {
$code = array_get($product, 'code', -1);
$this->cacheProductDetail($type, $code, $product);
$dataCodes[] = $code;
}
/*
* Cache unknown products
*/
$unknownCodes = array_diff($newCodes, $dataCodes);
foreach ($unknownCodes as $code) {
$this->cacheProductDetail($type, $code, -1);
}
$this->saveProductDetailCache();
}
/*
* Build details from cache
*/
$result = [];
$requestedDetails = array_intersect_key($this->productCache[$type], array_flip($codes));
foreach ($requestedDetails as $detail) {
if ($detail === -1) {
continue;
}
$result[] = $detail;
}
return $result;
}
/**
* Returns popular themes found on the marketplace.
*/
public function requestPopularProducts($type = null)
{
2017-07-29 05:33:51 +00:00
if ($type != 'plugin' && $type != 'theme') {
$type = 'plugin';
2017-07-29 05:33:51 +00:00
}
2020-04-11 15:46:22 +00:00
$cacheKey = 'system-updates-popular-' . $type;
if (Cache::has($cacheKey)) {
return @unserialize(@base64_decode(Cache::get($cacheKey))) ?: [];
}
2020-04-11 15:46:22 +00:00
$data = $this->requestServerData($type . '/popular');
2022-12-14 15:55:13 +00:00
$expiresAt = now()->addMinutes(60);
Cache::put($cacheKey, base64_encode(serialize($data)), $expiresAt);
foreach ($data as $product) {
$code = array_get($product, 'code', -1);
$this->cacheProductDetail($type, $code, $product);
}
$this->saveProductDetailCache();
return $data;
}
protected function loadProductDetailCache()
{
$defaultCache = ['theme' => [], 'plugin' => []];
$cacheKey = 'system-updates-product-details';
if (Cache::has($cacheKey)) {
$this->productCache = @unserialize(@base64_decode(Cache::get($cacheKey))) ?: $defaultCache;
2020-04-11 15:46:22 +00:00
} else {
$this->productCache = $defaultCache;
}
}
protected function saveProductDetailCache()
{
if ($this->productCache === null) {
$this->loadProductDetailCache();
}
$cacheKey = 'system-updates-product-details';
$expiresAt = Carbon::now()->addDays(2);
Cache::put($cacheKey, base64_encode(serialize($this->productCache)), $expiresAt);
}
protected function cacheProductDetail($type, $code, $data)
{
if ($this->productCache === null) {
$this->loadProductDetailCache();
}
$this->productCache[$type][$code] = $data;
}
//
// Changelog
//
/**
* Returns the latest changelog information.
*/
public function requestChangelog()
{
$result = Http::get('https://octobercms.com/changelog?json');
if ($result->code == 404) {
throw new ApplicationException(Lang::get('system::lang.server.response_empty'));
}
if ($result->code != 200) {
throw new ApplicationException(
strlen($result->body)
? $result->body
: Lang::get('system::lang.server.response_empty')
);
}
try {
$resultData = json_decode($result->body, true);
2020-04-11 15:46:22 +00:00
} catch (Exception $ex) {
throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
}
return $resultData;
}
2014-05-14 13:24:20 +00:00
//
// Notes
//
/**
* Raise a note event for the migrator.
2020-04-11 15:46:22 +00:00
* @param string $message
2017-05-19 23:03:58 +00:00
* @return self
2014-05-14 13:24:20 +00:00
*/
protected function note($message)
{
2017-05-19 23:03:58 +00:00
if ($this->notesOutput !== null) {
$this->notesOutput->writeln($message);
}
return $this;
}
/**
* Sets an output stream for writing notes.
2020-04-11 15:46:22 +00:00
* @param Illuminate\Console\Command $output
2017-05-19 23:03:58 +00:00
* @return self
*/
public function setNotesOutput($output)
{
$this->notesOutput = $output;
2014-05-14 13:24:20 +00:00
return $this;
}
//
// Gateway access
//
/**
* Contacts the update server for a response.
2020-04-11 15:46:22 +00:00
* @param string $uri Gateway API URI
* @param array $postData Extra post data
2014-05-14 13:24:20 +00:00
* @return array
*/
public function requestServerData($uri, $postData = [])
{
2014-10-18 09:58:50 +00:00
$result = Http::post($this->createServerUrl($uri), function ($http) use ($postData) {
$this->applyHttpAttributes($http, $postData);
});
2014-05-14 13:24:20 +00:00
2014-10-18 09:58:50 +00:00
if ($result->code == 404) {
2014-05-14 13:24:20 +00:00
throw new ApplicationException(Lang::get('system::lang.server.response_not_found'));
2014-10-18 09:58:50 +00:00
}
2014-05-14 13:24:20 +00:00
if ($result->code != 200) {
throw new ApplicationException(
strlen($result->body)
? $result->body
: Lang::get('system::lang.server.response_empty')
);
}
$resultData = false;
try {
$resultData = @json_decode($result->body, true);
2020-04-11 15:46:22 +00:00
} catch (Exception $ex) {
2014-05-14 13:24:20 +00:00
throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
}
2014-10-18 09:58:50 +00:00
if ($resultData === false || (is_string($resultData) && !strlen($resultData))) {
2014-05-14 13:24:20 +00:00
throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
2014-10-18 09:58:50 +00:00
}
2014-05-14 13:24:20 +00:00
2022-12-14 15:55:13 +00:00
if (!$this->validateServerSignature($resultData, $result->headers['Rest-Sign'] ?? '')) {
throw new ApplicationException(Lang::get('system::lang.server.response_invalid') . ' (Bad signature)');
}
2014-05-14 13:24:20 +00:00
return $resultData;
}
/**
* Downloads a file from the update server.
2020-04-11 15:46:22 +00:00
* @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
2014-05-14 13:24:20 +00:00
* @return void
*/
public function requestServerFile($uri, $fileCode, $expectedHash, $postData = [])
{
$filePath = $this->getFilePath($fileCode);
2014-10-18 09:58:50 +00:00
$result = Http::post($this->createServerUrl($uri), function ($http) use ($postData, $filePath) {
$this->applyHttpAttributes($http, $postData);
$http->toFile($filePath);
});
2014-05-14 13:24:20 +00:00
2021-04-16 09:15:27 +00:00
if (in_array($result->code, [301, 302])) {
if ($redirectUrl = array_get($result->info, 'redirect_url')) {
$result = Http::get($redirectUrl, function ($http) use ($postData, $filePath) {
$http->toFile($filePath);
});
}
2014-10-18 09:58:50 +00:00
}
2014-05-14 13:24:20 +00:00
2021-04-16 09:15:27 +00:00
if ($result->code != 200) {
throw new ApplicationException(File::get($filePath));
2014-05-14 13:24:20 +00:00
}
}
/**
* Calculates a file path for a file code
2020-04-11 15:46:22 +00:00
* @param string $fileCode A unique file code
2014-05-14 13:24:20 +00:00
* @return string Full path on the disk
*/
2014-08-01 08:18:09 +00:00
protected function getFilePath($fileCode)
2014-05-14 13:24:20 +00:00
{
$name = md5($fileCode) . '.arc';
return $this->tempDirectory . '/' . $name;
}
/**
* Set the API security for all transmissions.
2020-04-11 15:46:22 +00:00
* @param string $key API Key
2014-05-14 13:24:20 +00:00
* @param string $secret API Secret
*/
public function setSecurity($key, $secret)
{
$this->key = $key;
$this->secret = $secret;
}
/**
* Create a complete gateway server URL from supplied URI
2020-04-11 15:46:22 +00:00
* @param string $uri URI
2014-05-14 13:24:20 +00:00
* @return string URL
*/
2014-08-01 08:18:09 +00:00
protected function createServerUrl($uri)
2014-05-14 13:24:20 +00:00
{
2021-04-16 09:15:27 +00:00
$gateway = Config::get('cms.updateServer', 'https://gateway.octobercms.com/api');
2014-10-18 09:58:50 +00:00
if (substr($gateway, -1) != '/') {
2014-05-14 13:24:20 +00:00
$gateway .= '/';
2014-10-18 09:58:50 +00:00
}
2014-05-14 13:24:20 +00:00
return $gateway . $uri;
}
/**
* Modifies the Network HTTP object with common attributes.
2020-04-11 15:46:22 +00:00
* @param Http $http Network object
* @param array $postData Post data
2014-05-14 13:24:20 +00:00
* @return void
*/
2014-08-01 08:18:09 +00:00
protected function applyHttpAttributes($http, $postData)
2014-05-14 13:24:20 +00:00
{
2022-12-14 15:55:13 +00:00
$postData['protocol_version'] = '1.3';
$postData['client'] = 'October CMS';
2021-04-16 09:15:27 +00:00
$postData['server'] = base64_encode(json_encode([
'php' => PHP_VERSION,
'url' => Url::to('/'),
2022-12-14 15:55:13 +00:00
'ip' => Request::ip(),
'since' => PluginVersion::orderBy('created_at')->value('created_at')
]));
if ($projectId = Parameter::get('system::project.id')) {
$postData['project'] = $projectId;
}
if (Config::get('cms.edgeUpdates', false)) {
$postData['edge'] = 1;
}
2014-05-14 13:24:20 +00:00
if ($this->key && $this->secret) {
$postData['nonce'] = $this->createNonce();
$http->header('Rest-Key', $this->key);
$http->header('Rest-Sign', $this->createSignature($postData, $this->secret));
}
2014-10-18 09:58:50 +00:00
if ($credentials = Config::get('cms.updateAuth')) {
2014-05-14 13:24:20 +00:00
$http->auth($credentials);
2014-10-18 09:58:50 +00:00
}
2014-05-14 13:24:20 +00:00
$http->noRedirect();
$http->data($postData);
}
/**
2014-05-17 16:08:01 +00:00
* Create a nonce based on millisecond time
2014-05-14 13:24:20 +00:00
* @return int
*/
2014-08-01 08:18:09 +00:00
protected function createNonce()
2014-05-14 13:24:20 +00:00
{
$mt = explode(' ', microtime());
return $mt[1] . substr($mt[0], 2, 6);
}
/**
* Create a unique signature for transmission.
* @return string
*/
2014-08-01 08:18:09 +00:00
protected function createSignature($data, $secret)
2014-05-14 13:24:20 +00:00
{
return base64_encode(hash_hmac('sha512', http_build_query($data, '', '&'), base64_decode($secret), true));
}
2017-01-12 19:15:59 +00:00
/**
* @return string
*/
public function getMigrationTableName()
2017-01-12 19:15:59 +00:00
{
return Config::get('database.migrations', 'migrations');
}
2022-12-14 15:55:13 +00:00
/**
* Adds a message from a specific migration or seeder.
*
* @param string|object $class
* @param string|array $message
* @return void
*/
protected function addMessage($class, $message)
{
if (empty($message)) {
return;
}
if (is_object($class)) {
$class = get_class($class);
}
if (!isset($this->messages[$class])) {
$this->messages[$class] = [];
}
if (is_string($message)) {
$this->messages[$class][] = $message;
} elseif (is_array($message)) {
array_merge($this->messages[$class], $message);
}
}
/**
* Prints collated messages from the migrations and seeders
*
* @return void
*/
protected function printMessages()
{
if (!count($this->messages)) {
return;
}
// Add a line break
$this->note('');
foreach ($this->messages as $class => $messages) {
$this->note(sprintf('<info>%s reported:</info>', $class));
foreach ($messages as $message) {
$this->note(' - ' . (string) $message);
}
}
}
/**
* validateServerSignature checks the server has provided a valid signature
*
* @return bool
*/
protected function validateServerSignature($data, $signature)
{
if (!$signature) {
return false;
}
$signature = base64_decode($signature);
$pubKey = '-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt+KwvTXqC8Mz9vV4KIvX
3y+aZusrlg26jdbNVUuhXNFbt1VisjJydHW2+WGsiEHSy2s61ZAV2dICR6f3huSw
jY/MH9j23Oo/u61CBpvIS3Q8uC+TLtJl4/F9eqlnzocfMoKe8NmcBbUR3TKQoIok
xbSMl6jiE2k5TJdzhHUxjZRIeeLDLMKYX6xt37LdhuM8zO6sXQmCGg4J6LmHTJph
96H11gBvcFSFJSmIiDykJOELZl/aVcY1g3YgpL0mw5Bw1VTmKaRdz1eBi9DmKrKX
UijG4gD8eLRV/FS/sZCFNR/evbQXvTBxO0TOIVi85PlQEcMl4SBj0CoTyNbcAGtz
4wIDAQAB
-----END PUBLIC KEY-----';
$pubKey = Config::get('system.update_gateway_key', $pubKey);
$data = base64_encode(json_encode($data));
return openssl_verify($data, $signature, $pubKey) === 1;
}
2014-05-14 13:24:20 +00:00
}