Implement improved "set build" replacement (#5087)
This change implements an improved "set build" utility through the "october:version" Artisan command that, instead of checking the October CMS server for the latest build, checks the module files against a source manifest kept on GitHub. This check allows us to accurately determine the build based on the module files in the October CMS installation, and can even detect versions if the module files are modified (except in the cases of extreme modification). An additional utility has been implemented, "october:manifest", which will build the manifest JSON file in order to provide the maintainers with a way of generating this manifest file as required. Replaces #4615.
This commit is contained in:
parent
50bdfa0c39
commit
4141646105
|
|
@ -61,7 +61,7 @@
|
|||
"php artisan package:discover"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"php artisan october:util set build",
|
||||
"php artisan october:version",
|
||||
"php artisan package:discover"
|
||||
],
|
||||
"test": [
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ class ServiceProvider extends ModuleServiceProvider
|
|||
protected function registerPrivilegedActions()
|
||||
{
|
||||
$requests = ['/combine/', '@/system/updates', '@/system/install', '@/backend/auth'];
|
||||
$commands = ['october:up', 'october:update'];
|
||||
$commands = ['october:up', 'october:update', 'october:version'];
|
||||
|
||||
/*
|
||||
* Requests
|
||||
|
|
@ -248,6 +248,8 @@ class ServiceProvider extends ModuleServiceProvider
|
|||
$this->registerConsoleCommand('october.env', 'System\Console\OctoberEnv');
|
||||
$this->registerConsoleCommand('october.install', 'System\Console\OctoberInstall');
|
||||
$this->registerConsoleCommand('october.passwd', 'System\Console\OctoberPasswd');
|
||||
$this->registerConsoleCommand('october.version', 'System\Console\OctoberVersion');
|
||||
$this->registerConsoleCommand('october.manifest', 'System\Console\OctoberManifest');
|
||||
|
||||
$this->registerConsoleCommand('plugin.install', 'System\Console\PluginInstall');
|
||||
$this->registerConsoleCommand('plugin.remove', 'System\Console\PluginRemove');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,193 @@
|
|||
<?php namespace System\Classes;
|
||||
|
||||
use ApplicationException;
|
||||
use Config;
|
||||
use October\Rain\Filesystem\Filesystem;
|
||||
use October\Rain\Halcyon\Datasource\FileDatasource;
|
||||
|
||||
/**
|
||||
* Stores the file manifest for this October CMS installation.
|
||||
*
|
||||
* This manifest is a file checksum of all files within this October CMS installation. When compared to the source
|
||||
* manifest, this allows us to determine the current installation's build number.
|
||||
*
|
||||
* @package october\system
|
||||
* @author Ben Thomson
|
||||
*/
|
||||
class FileManifest
|
||||
{
|
||||
/**
|
||||
* @var string Root folder of this installation.
|
||||
*/
|
||||
protected $root;
|
||||
|
||||
/**
|
||||
* @var array Modules to store in manifest.
|
||||
*/
|
||||
protected $modules = ['system', 'backend', 'cms'];
|
||||
|
||||
/**
|
||||
* @var array Files cache.
|
||||
*/
|
||||
protected $files = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $root The root folder to get the file list from.
|
||||
* @param array $modules An array of modules to include in the file manifest.
|
||||
*/
|
||||
public function __construct($root = null, array $modules = null)
|
||||
{
|
||||
if (isset($root)) {
|
||||
$this->setRoot($root);
|
||||
} else {
|
||||
$this->setRoot(base_path());
|
||||
}
|
||||
|
||||
if (isset($modules)) {
|
||||
$this->setModules($modules);
|
||||
} else {
|
||||
$this->setModules(Config::get('cms.loadModules', ['System', 'Backend', 'Cms']));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the root folder.
|
||||
*
|
||||
* @param string $root
|
||||
* @throws ApplicationException If the specified root does not exist.
|
||||
*/
|
||||
public function setRoot($root)
|
||||
{
|
||||
if (is_string($root)) {
|
||||
$this->root = realpath($root);
|
||||
|
||||
if ($this->root === false || !is_dir($this->root)) {
|
||||
throw new ApplicationException(
|
||||
'Invalid root specified for the file manifest.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the modules.
|
||||
*
|
||||
* @param array $modules
|
||||
*/
|
||||
public function setModules(array $modules)
|
||||
{
|
||||
$this->modules = array_map(function ($module) {
|
||||
return strtolower($module);
|
||||
}, $modules);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of files and their corresponding hashsums.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFiles()
|
||||
{
|
||||
if (count($this->files)) {
|
||||
return $this->files;
|
||||
}
|
||||
|
||||
$files = [];
|
||||
|
||||
foreach ($this->modules as $module) {
|
||||
$path = $this->root . '/modules/' . $module;
|
||||
|
||||
if (!is_dir($path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($this->findFiles($path) as $file) {
|
||||
$files[$this->getFilename($file)] = hash('sha3-256', $this->normalizeFileContents($file));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->files = $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the checksum of a specific install.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getModuleChecksums()
|
||||
{
|
||||
if (!count($this->files)) {
|
||||
$this->getFiles();
|
||||
}
|
||||
|
||||
$modules = [];
|
||||
foreach ($this->modules as $module) {
|
||||
$modules[$module] = '';
|
||||
}
|
||||
|
||||
foreach ($this->files as $path => $hash) {
|
||||
// Determine module
|
||||
$module = explode('/', $path)[2];
|
||||
|
||||
$modules[$module] .= $hash;
|
||||
}
|
||||
|
||||
return array_map(function ($moduleSum) {
|
||||
return hash('sha3-256', $moduleSum);
|
||||
}, $modules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all files within the path.
|
||||
*
|
||||
* @param string $basePath The base path to look for files within.
|
||||
* @return array
|
||||
*/
|
||||
protected function findFiles(string $basePath)
|
||||
{
|
||||
$datasource = new FileDatasource($basePath, new Filesystem);
|
||||
|
||||
$files = array_map(function ($path) use ($basePath) {
|
||||
return $basePath . '/' . $path;
|
||||
}, array_keys($datasource->getAvailablePaths()));
|
||||
|
||||
// Ensure files are sorted so they are in a consistent order, no matter the way the OS returns the file list.
|
||||
sort($files, SORT_NATURAL);
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filename without the root.
|
||||
*
|
||||
* @param string $file
|
||||
* @return string
|
||||
*/
|
||||
protected function getFilename(string $file): string
|
||||
{
|
||||
return str_replace($this->root, '', $file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalises the file contents, irrespective of OS.
|
||||
*
|
||||
* @param string $file
|
||||
* @return string
|
||||
*/
|
||||
protected function normalizeFileContents(string $file): string
|
||||
{
|
||||
if (!is_file($file)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$contents = file_get_contents($file);
|
||||
|
||||
return str_replace(PHP_EOL, "\n", $contents);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,356 @@
|
|||
<?php namespace System\Classes;
|
||||
|
||||
use ApplicationException;
|
||||
use Config;
|
||||
|
||||
/**
|
||||
* Reads and stores the October CMS source manifest information.
|
||||
*
|
||||
* The source manifest is a meta JSON file, stored on GitHub, that contains the hashsums of all module files across all
|
||||
* buils of October CMS. This allows us to compare the October CMS installation against the expected file checksums and
|
||||
* determine the installed build and whether it has been modified.
|
||||
*
|
||||
* @package october\system
|
||||
* @author Ben Thomson
|
||||
*/
|
||||
class SourceManifest
|
||||
{
|
||||
/**
|
||||
* @var string The URL to the source manifest
|
||||
*/
|
||||
protected $source;
|
||||
|
||||
/**
|
||||
* @var array Array of builds, keyed by build number, with files for keys and hashes for values.
|
||||
*/
|
||||
protected $builds = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $manifest Manifest file to load
|
||||
* @param bool $autoload Loads the manifest on construct
|
||||
*/
|
||||
public function __construct($source = null, $autoload = true)
|
||||
{
|
||||
if (isset($source)) {
|
||||
$this->setSource($source);
|
||||
} else {
|
||||
$this->setSource(
|
||||
Config::get(
|
||||
'cms.sourceManifestUrl',
|
||||
'https://raw.githubusercontent.com/octoberrain/meta/master/manifest/builds.json'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ($autoload) {
|
||||
$this->load();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the source manifest URL.
|
||||
*
|
||||
* @param string $source
|
||||
* @return void
|
||||
*/
|
||||
public function setSource($source)
|
||||
{
|
||||
if (is_string($source)) {
|
||||
$this->source = $source;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the manifest file.
|
||||
*
|
||||
* @throws ApplicationException If the manifest is invalid, or cannot be parsed.
|
||||
*/
|
||||
public function load()
|
||||
{
|
||||
$source = file_get_contents($this->source);
|
||||
if (empty($source)) {
|
||||
throw new ApplicationException(
|
||||
'Source manifest not found'
|
||||
);
|
||||
}
|
||||
|
||||
$data = json_decode($source, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new ApplicationException(
|
||||
'Unable to decode source manifest JSON data. JSON Error: ' . json_last_error_msg()
|
||||
);
|
||||
}
|
||||
if (!isset($data['manifest']) || !is_array($data['manifest'])) {
|
||||
throw new ApplicationException(
|
||||
'The source manifest at "' . $this->source . '" does not appear to be a valid source manifest file.'
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($data['manifest'] as $build) {
|
||||
$this->builds[$build['build']] = [
|
||||
'modules' => $build['modules'],
|
||||
'files' => $build['files'],
|
||||
];
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a FileManifest instance as a build to this source manifest.
|
||||
*
|
||||
* Changes between builds are calculated and stored with the build. Builds are stored numerically, in ascending
|
||||
* order.
|
||||
*
|
||||
* @param integer $build Build number.
|
||||
* @param FileManifest $manifest The file manifest to add as a build.
|
||||
* @param integer $previous The previous build number, used to determine changes with this build.
|
||||
* @return void
|
||||
*/
|
||||
public function addBuild($build, FileManifest $manifest, $previous = null)
|
||||
{
|
||||
$this->builds[(int) $build] = [
|
||||
'modules' => $manifest->getModuleChecksums(),
|
||||
'files' => $this->processChanges($manifest, $previous),
|
||||
];
|
||||
|
||||
// Sort builds numerically in ascending order.
|
||||
ksort($this->builds[$build], SORT_NUMERIC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all builds.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getBuilds()
|
||||
{
|
||||
return $this->builds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum build number in the manifest.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getMaxBuild()
|
||||
{
|
||||
if (!count($this->builds)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return max(array_keys($this->builds));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the JSON data to be stored with the source manifest.
|
||||
*
|
||||
* @throws ApplicationException If no builds have been added to this source manifest.
|
||||
* @return string
|
||||
*/
|
||||
public function generate()
|
||||
{
|
||||
if (!count($this->builds)) {
|
||||
throw new ApplicationException(
|
||||
'No builds have been added to the manifest.'
|
||||
);
|
||||
}
|
||||
|
||||
$json = [
|
||||
'manifest' => [],
|
||||
];
|
||||
|
||||
foreach ($this->builds as $build => $details) {
|
||||
$json['manifest'][] = [
|
||||
'build' => $build,
|
||||
'modules' => $details['modules'],
|
||||
'files' => $details['files'],
|
||||
];
|
||||
}
|
||||
|
||||
return json_encode($json, JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the filelist state at a selected build.
|
||||
*
|
||||
* This method will list all expected files and hashsums at the specified build number.
|
||||
*
|
||||
* @param integer $build Build number to get the filelist state for.
|
||||
* @throws ApplicationException If the specified build has not been added to the source manifest.
|
||||
* @return array
|
||||
*/
|
||||
public function getState($build)
|
||||
{
|
||||
if (!isset($this->builds[$build])) {
|
||||
throw new \Exception('The specified build has not been added.');
|
||||
}
|
||||
|
||||
$state = [];
|
||||
|
||||
foreach ($this->builds as $number => $details) {
|
||||
if (isset($details['files']['added'])) {
|
||||
foreach ($details['files']['added'] as $filename => $sum) {
|
||||
$state[$filename] = $sum;
|
||||
}
|
||||
}
|
||||
if (isset($details['files']['modified'])) {
|
||||
foreach ($details['files']['modified'] as $filename => $sum) {
|
||||
$state[$filename] = $sum;
|
||||
}
|
||||
}
|
||||
if (isset($details['files']['removed'])) {
|
||||
foreach ($details['files']['removed'] as $filename) {
|
||||
unset($state[$filename]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($number === $build) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares a file manifest with the source manifest.
|
||||
*
|
||||
* This will determine the build of the October CMS installation.
|
||||
*
|
||||
* @param FileManifest $manifest The file manifest to compare against the source.
|
||||
* @return array|null Will return an array with the build, modified state and the probability that it is the
|
||||
* version specified. If the detected version does not look like a likely candidate, this will return null.
|
||||
*/
|
||||
public function compare(FileManifest $manifest)
|
||||
{
|
||||
$modules = $manifest->getModuleChecksums();
|
||||
|
||||
// Look for an unmodified version
|
||||
foreach ($this->getBuilds() as $build => $details) {
|
||||
$matched = array_intersect_assoc($details['modules'], $modules);
|
||||
|
||||
if (count($matched) === count($modules)) {
|
||||
return [
|
||||
'build' => $build,
|
||||
'modified' => false,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// If we could not find an unmodified version, try to find the closest version and assume this is a modified
|
||||
// install.
|
||||
$buildMatch = [];
|
||||
|
||||
foreach ($this->getBuilds() as $build => $details) {
|
||||
$state = $this->getState($build);
|
||||
|
||||
// Include only the files that match the modules being loaded in this file manifest
|
||||
$availableModules = array_keys($modules);
|
||||
|
||||
foreach ($state as $file => $sum) {
|
||||
// Determine module
|
||||
$module = explode('/', $file)[2];
|
||||
|
||||
if (!in_array($module, $availableModules)) {
|
||||
unset($state[$file]);
|
||||
}
|
||||
}
|
||||
|
||||
$filesExpected = count($state);
|
||||
$filesFound = [];
|
||||
$filesChanged = [];
|
||||
|
||||
foreach ($manifest->getFiles() as $file => $sum) {
|
||||
// Unknown new file
|
||||
if (!isset($state[$file])) {
|
||||
$filesChanged[] = $file;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Modified file
|
||||
if ($state[$file] !== $sum) {
|
||||
$filesFound[] = $file;
|
||||
$filesChanged[] = $file;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Pristine file
|
||||
$filesFound[] = $file;
|
||||
}
|
||||
|
||||
$foundPercent = count($filesFound) / $filesExpected;
|
||||
$changedPercent = count($filesChanged) / $filesExpected;
|
||||
|
||||
$score = ((1 * $foundPercent) - $changedPercent);
|
||||
$buildMatch[$build] = round($score * 100, 2);
|
||||
}
|
||||
|
||||
|
||||
// Find likely version (we have to be at least 60% sure)
|
||||
$likelyBuild = array_search(max($buildMatch), $buildMatch);
|
||||
if ($buildMatch[$likelyBuild] < 60) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'build' => $likelyBuild,
|
||||
'modified' => true,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines file changes between the specified build and the previous build.
|
||||
*
|
||||
* Will return an array of added, modified and removed files.
|
||||
*
|
||||
* @param FileManifest $manifest The current build's file manifest.
|
||||
* @param integer $previous The previous build number, used to determine changes with this build.
|
||||
* @return array
|
||||
*/
|
||||
protected function processChanges(FileManifest $manifest, $previous = null)
|
||||
{
|
||||
// If no previous build has been provided, all files are added
|
||||
if (is_null($previous)) {
|
||||
return [
|
||||
'added' => $manifest->getFiles(),
|
||||
];
|
||||
}
|
||||
|
||||
// Only save files if they are changing the "state" of the manifest (ie. the file is modified, added or removed)
|
||||
$state = $this->getState($previous);
|
||||
$added = [];
|
||||
$modified = [];
|
||||
|
||||
foreach ($manifest->getFiles() as $file => $sum) {
|
||||
if (!isset($state[$file])) {
|
||||
$added[$file] = $sum;
|
||||
continue;
|
||||
} else {
|
||||
if ($state[$file] !== $sum) {
|
||||
$modified[$file] = $sum;
|
||||
}
|
||||
unset($state[$file]);
|
||||
}
|
||||
}
|
||||
|
||||
// Any files still left in state have been removed
|
||||
$removed = array_keys($state);
|
||||
|
||||
$changes = [];
|
||||
if (count($added)) {
|
||||
$changes['added'] = $added;
|
||||
}
|
||||
if (count($modified)) {
|
||||
$changes['modified'] = $modified;
|
||||
}
|
||||
if (count($removed)) {
|
||||
$changes['removed'] = $removed;
|
||||
}
|
||||
|
||||
return $changes;
|
||||
}
|
||||
}
|
||||
|
|
@ -358,23 +358,38 @@ class UpdateManager
|
|||
}
|
||||
|
||||
/**
|
||||
* Asks the gateway for the lastest build number and stores it.
|
||||
* Determines build number from source manifest.
|
||||
*
|
||||
* An array will be returned with the following, if a build can be determined:
|
||||
* - `build` - The detected build number installed.
|
||||
* - `modified` - Whether the installation appears to have been modified.
|
||||
*
|
||||
* Otherwise, `null` will be returned.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getBuildNumberManually()
|
||||
{
|
||||
$source = new SourceManifest();
|
||||
$manifest = new FileManifest(null, null, true);
|
||||
|
||||
// Find build by comparing with source manifest
|
||||
return $source->compare($manifest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the build number in the database.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setBuildNumberManually()
|
||||
{
|
||||
$postData = [];
|
||||
$build = $this->getBuildNumberManually();
|
||||
|
||||
if (Config::get('cms.edgeUpdates', false)) {
|
||||
$postData['edge'] = 1;
|
||||
if (!is_null($build)) {
|
||||
$this->setBuild($build['build'], null, $build['modified']);
|
||||
}
|
||||
|
||||
$result = $this->requestServerData('ping', $postData);
|
||||
|
||||
$build = (int) array_get($result, 'pong', 420);
|
||||
|
||||
$this->setBuild($build);
|
||||
|
||||
return $build;
|
||||
}
|
||||
|
||||
|
|
@ -457,12 +472,14 @@ class UpdateManager
|
|||
* Sets the build number and hash
|
||||
* @param string $hash
|
||||
* @param string $build
|
||||
* @param bool $modified
|
||||
* @return void
|
||||
*/
|
||||
public function setBuild($build, $hash = null)
|
||||
public function setBuild($build, $hash = null, $modified = false)
|
||||
{
|
||||
$params = [
|
||||
'system::core.build' => $build
|
||||
'system::core.build' => $build,
|
||||
'system::core.modified' => $modified,
|
||||
];
|
||||
|
||||
if ($hash) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,170 @@
|
|||
<?php namespace System\Console;
|
||||
|
||||
use ApplicationException;
|
||||
use ZipArchive;
|
||||
use System\Classes\FileManifest;
|
||||
use System\Classes\SourceManifest;
|
||||
|
||||
class OctoberManifest extends \Illuminate\Console\Command
|
||||
{
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Generates a build manifest of October CMS builds.';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of the console command.
|
||||
*/
|
||||
protected $signature = 'october:manifest
|
||||
{target : Specifies the target file for the build manifest.}
|
||||
{--minBuild= : Specifies the minimum build number to retrieve from the source.}
|
||||
{--maxBuild= : Specifies the maximum build number to retreive from the source.}';
|
||||
|
||||
/**
|
||||
* @var bool Indicates whether the command should be shown in the Artisan command list.
|
||||
*/
|
||||
protected $hidden = true;
|
||||
|
||||
/**
|
||||
* @var string Source repository download file.
|
||||
*/
|
||||
protected $sourceBuildFile = 'https://github.com/octobercms/october/archive/v1.0.%d.zip';
|
||||
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$minBuild = $this->option('minBuild') ?? 420;
|
||||
$maxBuild = $this->option('maxBuild') ?? 9999;
|
||||
|
||||
$targetFile = (substr($this->argument('target'), 0, 1) === '/')
|
||||
? $this->argument('target')
|
||||
: getcwd() . '/' . $this->argument('target');
|
||||
|
||||
if (empty($targetFile)) {
|
||||
throw new ApplicationException(
|
||||
'A target argument must be specified for the generated manifest file.'
|
||||
);
|
||||
}
|
||||
|
||||
if ($minBuild > $maxBuild) {
|
||||
throw new ApplicationException(
|
||||
'Minimum build specified is larger than the maximum build specified.'
|
||||
);
|
||||
}
|
||||
|
||||
if (file_exists($targetFile)) {
|
||||
$manifest = new SourceManifest($targetFile);
|
||||
$manifestMaxBuild = $manifest->getMaxBuild();
|
||||
|
||||
if ($manifestMaxBuild > $minBuild) {
|
||||
$minBuild = $manifestMaxBuild + 1;
|
||||
|
||||
if ($minBuild > $maxBuild) {
|
||||
throw new ApplicationException(
|
||||
'This manifest already contains all requested builds.'
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$manifest = new SourceManifest('', false);
|
||||
}
|
||||
|
||||
// Create temporary directory to hold builds
|
||||
$buildDir = storage_path('temp/builds/');
|
||||
if (!is_dir($buildDir)) {
|
||||
mkdir($buildDir, 0775, true);
|
||||
}
|
||||
|
||||
for ($build = $minBuild; $build <= $maxBuild; ++$build) {
|
||||
// Download version from GitHub
|
||||
$this->comment('Processing build ' . $build);
|
||||
$this->line(' - Downloading...');
|
||||
|
||||
if (file_exists($buildDir . 'build-' . $build . '.zip') || is_dir($buildDir . $build . '/')) {
|
||||
$this->info(' - Already downloaded.');
|
||||
} else {
|
||||
$zipUrl = sprintf($this->sourceBuildFile, $build);
|
||||
$zipFile = @file_get_contents($zipUrl);
|
||||
|
||||
if (empty($zipFile)) {
|
||||
$this->error(' - Not found.');
|
||||
break;
|
||||
}
|
||||
|
||||
file_put_contents($buildDir . 'build-' . $build . '.zip', $zipFile);
|
||||
|
||||
$this->info(' - Downloaded.');
|
||||
}
|
||||
|
||||
// Extract version
|
||||
$this->line(' - Extracting...');
|
||||
if (is_dir($buildDir . $build . '/')) {
|
||||
$this->info(' - Already extracted.');
|
||||
} else {
|
||||
$zip = new ZipArchive;
|
||||
if ($zip->open($buildDir . 'build-' . $build . '.zip')) {
|
||||
$toExtract = [];
|
||||
$paths = [
|
||||
'october-1.0.' . $build . '/modules/backend/',
|
||||
'october-1.0.' . $build . '/modules/cms/',
|
||||
'october-1.0.' . $build . '/modules/system/',
|
||||
];
|
||||
|
||||
// Only get necessary files from the modules directory
|
||||
for ($i = 0; $i < $zip->numFiles; ++$i) {
|
||||
$filename = $zip->statIndex($i)['name'];
|
||||
|
||||
foreach ($paths as $path) {
|
||||
if (strpos($filename, $path) === 0) {
|
||||
$toExtract[] = $filename;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!count($toExtract)) {
|
||||
$this->error(' - Unable to get valid files for extraction. Cancelled.');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$zip->extractTo($buildDir . $build . '/', $toExtract);
|
||||
$zip->close();
|
||||
|
||||
// Remove ZIP file
|
||||
unlink($buildDir . 'build-' . $build . '.zip');
|
||||
} else {
|
||||
$this->error(' - Unable to extract zip file. Cancelled.');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$this->info(' - Extracted.');
|
||||
}
|
||||
|
||||
// Determine previous build
|
||||
$manifestBuilds = $manifest->getBuilds();
|
||||
$previous = null;
|
||||
if (count($manifestBuilds)) {
|
||||
if (isset($manifestBuilds[$build - 1])) {
|
||||
$previous = $build - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Add build to manifest
|
||||
$this->line(' - Adding to manifest...');
|
||||
$buildManifest = new FileManifest($buildDir . $build . '/october-1.0.' . $build);
|
||||
$manifest->addBuild($build, $buildManifest, $previous);
|
||||
$this->info(' - Added.');
|
||||
}
|
||||
|
||||
// Generate manifest
|
||||
$this->comment('Generating manifest...');
|
||||
file_put_contents($targetFile, $manifest->generate());
|
||||
|
||||
$this->comment('Completed.');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
<?php namespace System\Console;
|
||||
|
||||
use App;
|
||||
use Lang;
|
||||
use File;
|
||||
use Config;
|
||||
|
|
@ -9,7 +8,6 @@ use Symfony\Component\Console\Input\InputOption;
|
|||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use System\Classes\UpdateManager;
|
||||
use System\Classes\CombineAssets;
|
||||
use Exception;
|
||||
use System\Models\Parameter;
|
||||
use System\Models\File as FileModel;
|
||||
|
||||
|
|
@ -28,7 +26,6 @@ use System\Models\File as FileModel;
|
|||
* - compile less: Compile registered LESS files only.
|
||||
* - compile scss: Compile registered SCSS files only.
|
||||
* - compile lang: Compile registered Language files only.
|
||||
* - set build: Pull the latest stable build number from the update gateway and set it as the current build number.
|
||||
* - set project --projectId=<id>: Set the projectId for this october instance.
|
||||
*
|
||||
* @package october\system
|
||||
|
|
@ -111,31 +108,10 @@ class OctoberUtil extends Command
|
|||
|
||||
protected function utilSetBuild()
|
||||
{
|
||||
$this->comment('-');
|
||||
$this->comment('NOTE: This command is now deprecated. Please use "php artisan october:version" instead.');
|
||||
$this->comment('');
|
||||
|
||||
/*
|
||||
* Skip setting the build number if no database is detected to set it within
|
||||
*/
|
||||
if (!App::hasDatabase()) {
|
||||
$this->comment('No database detected - skipping setting the build number.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$build = UpdateManager::instance()->setBuildNumberManually();
|
||||
$this->comment('*** October sets build: '.$build);
|
||||
}
|
||||
catch (Exception $ex) {
|
||||
$this->comment('*** You were kicked from #october by Ex: ('.$ex->getMessage().')');
|
||||
}
|
||||
|
||||
$this->comment('-');
|
||||
sleep(1);
|
||||
$this->comment('Ping? Pong!');
|
||||
$this->comment('-');
|
||||
sleep(1);
|
||||
$this->comment('Ping? Pong!');
|
||||
$this->comment('-');
|
||||
return $this->call('october:version');
|
||||
}
|
||||
|
||||
protected function utilCompileJs()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
<?php namespace System\Console;
|
||||
|
||||
use App;
|
||||
use System\Classes\UpdateManager;
|
||||
|
||||
class OctoberVersion extends \Illuminate\Console\Command
|
||||
{
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Detects the build number (version) of this October CMS instance.';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of the console command.
|
||||
*/
|
||||
protected $signature = 'october:version';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->comment('*** Detecting October CMS build...');
|
||||
|
||||
if (!App::hasDatabase()) {
|
||||
$build = UpdateManager::instance()->getBuildNumberManually();
|
||||
|
||||
// Skip setting the build number if no database is detected to set it within
|
||||
$this->comment('*** No database detected - skipping setting the build number.');
|
||||
} else {
|
||||
$build = UpdateManager::instance()->setBuildNumberManually();
|
||||
}
|
||||
|
||||
if (is_null($build)) {
|
||||
$this->error('Unable to detect your build of October CMS.');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($build['modified']) {
|
||||
$this->info('*** Detected a modified version of October CMS build ' . $build['build'] . '.');
|
||||
} else {
|
||||
$this->info('*** Detected October CMS build ' . $build['build'] . '.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -75,6 +75,7 @@ class Updates extends Controller
|
|||
public function index()
|
||||
{
|
||||
$this->vars['coreBuild'] = Parameter::get('system::core.build');
|
||||
$this->vars['coreBuildModified'] = Parameter::get('system::core.modified', false);
|
||||
$this->vars['projectId'] = Parameter::get('system::project.id');
|
||||
$this->vars['projectName'] = Parameter::get('system::project.name');
|
||||
$this->vars['projectOwner'] = Parameter::get('system::project.owner');
|
||||
|
|
|
|||
|
|
@ -34,7 +34,19 @@
|
|||
<?php if ($coreBuild): ?>
|
||||
<div class="scoreboard-item title-value">
|
||||
<h4><?= e(trans('system::lang.updates.core_current_build')) ?></h4>
|
||||
<p><?= $coreBuild ?></p>
|
||||
<?php if ($coreBuildModified): ?>
|
||||
<p
|
||||
class="oc-icon-exclamation-circle"
|
||||
data-toggle="tooltip"
|
||||
data-placement="bottom"
|
||||
title="This build has been modified"
|
||||
>
|
||||
<?= $coreBuild ?>
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<p><?= $coreBuild ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<p class="description">
|
||||
<a
|
||||
href="javascript:;"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
<?php
|
||||
// file1.php - version 1.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?php
|
||||
// file1.php - version 1.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?php
|
||||
// file2.php - version 1.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?php
|
||||
// file1.php - version 1 / test2.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?php
|
||||
// file2.php - version 2.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?php
|
||||
// file3.php - version 1.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?php
|
||||
// file1.php - version 1 / test2.
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
use October\Rain\Exception\ApplicationException;
|
||||
use System\Classes\FileManifest;
|
||||
|
||||
class FileManifestTest extends TestCase
|
||||
{
|
||||
/** @var FileManifest instance */
|
||||
protected $fileManifest;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->fileManifest = new FileManifest(base_path('tests/fixtures/manifest/2'), ['test', 'test2']);
|
||||
}
|
||||
|
||||
public function testGetFiles()
|
||||
{
|
||||
$this->assertEquals([
|
||||
'/modules/test/file1.php' => '6f9b0b94528a85b2a6bb67b5621e074aef1b4c9fc9ee3ea1bd69100ea14cb3db',
|
||||
'/modules/test/file2.php' => '96ae9f6b6377ad29226ea169f952de49fc29ae895f18a2caed76aeabdf050f1b',
|
||||
'/modules/test2/file1.php' => '94bd47b1ac7b2837b31883ebcd38c8101687321f497c3c4b9744f68ae846721d',
|
||||
], $this->fileManifest->getFiles());
|
||||
}
|
||||
|
||||
public function testGetModuleChecksums()
|
||||
{
|
||||
$this->assertEquals([
|
||||
'test' => 'c0b794ff210862a4ce16223802efe6e28969f5a4fb42480ec8c2fef2da23d181',
|
||||
'test2' => '32c9f2fb6e0a22dde288a0fe1e4834798360b25e5a91d2597409d9302221381d',
|
||||
], $this->fileManifest->getModuleChecksums());
|
||||
}
|
||||
|
||||
public function testGetFilesInvalidRoot()
|
||||
{
|
||||
$this->expectException(ApplicationException::class);
|
||||
$this->expectExceptionMessage('Invalid root specified for the file manifest.');
|
||||
|
||||
$this->fileManifest->setRoot(base_path('tests/fixtures/manifest/invalid'));
|
||||
|
||||
$this->fileManifest->getFiles();
|
||||
}
|
||||
|
||||
public function testSingleModule()
|
||||
{
|
||||
$this->fileManifest->setModules(['test']);
|
||||
|
||||
$this->assertEquals([
|
||||
'/modules/test/file1.php' => '6f9b0b94528a85b2a6bb67b5621e074aef1b4c9fc9ee3ea1bd69100ea14cb3db',
|
||||
'/modules/test/file2.php' => '96ae9f6b6377ad29226ea169f952de49fc29ae895f18a2caed76aeabdf050f1b',
|
||||
], $this->fileManifest->getFiles());
|
||||
|
||||
$this->assertEquals([
|
||||
'test' => 'c0b794ff210862a4ce16223802efe6e28969f5a4fb42480ec8c2fef2da23d181',
|
||||
], $this->fileManifest->getModuleChecksums());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
<?php
|
||||
|
||||
use October\Rain\Exception\ApplicationException;
|
||||
use System\Classes\SourceManifest;
|
||||
use System\Classes\FileManifest;
|
||||
|
||||
class SourceManifestTest extends TestCase
|
||||
{
|
||||
/** @var SourceManifest instance */
|
||||
protected $sourceManifest;
|
||||
|
||||
/** @var array Emulated builds from the manifest fixture */
|
||||
protected $builds;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->builds = [
|
||||
1 => new FileManifest(base_path('tests/fixtures/manifest/1'), ['test', 'test2']),
|
||||
2 => new FileManifest(base_path('tests/fixtures/manifest/2'), ['test', 'test2']),
|
||||
3 => new FileManifest(base_path('tests/fixtures/manifest/3'), ['test', 'test2']),
|
||||
];
|
||||
|
||||
$this->sourceManifest = new SourceManifest($this->manifestPath(), false);
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
$this->deleteManifest();
|
||||
}
|
||||
|
||||
public function testCreateManifest()
|
||||
{
|
||||
$this->createManifest(true);
|
||||
|
||||
$this->assertEquals(
|
||||
'{' . "\n" .
|
||||
' "manifest": [' . "\n" .
|
||||
' {' . "\n" .
|
||||
' "build": 1,' . "\n" .
|
||||
' "modules": {' . "\n" .
|
||||
' "test": "e1d6c6e4c482688e231ee37d89668268426512013695de47bfcb424f9a645c7b",' . "\n" .
|
||||
' "test2": "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a"' . "\n" .
|
||||
' },' . "\n" .
|
||||
' "files": {' . "\n" .
|
||||
' "added": {' . "\n" .
|
||||
' "\/modules\/test\/file1.php": "6f9b0b94528a85b2a6bb67b5621e074aef1b4c9fc9ee3ea1bd69100ea14cb3db"' . "\n" .
|
||||
' }' . "\n" .
|
||||
' }' . "\n" .
|
||||
' },' . "\n" .
|
||||
' {' . "\n" .
|
||||
' "build": 2,' . "\n" .
|
||||
' "modules": {' . "\n" .
|
||||
' "test": "c0b794ff210862a4ce16223802efe6e28969f5a4fb42480ec8c2fef2da23d181",' . "\n" .
|
||||
' "test2": "32c9f2fb6e0a22dde288a0fe1e4834798360b25e5a91d2597409d9302221381d"' . "\n" .
|
||||
' },' . "\n" .
|
||||
' "files": {' . "\n" .
|
||||
' "added": {' . "\n" .
|
||||
' "\/modules\/test\/file2.php": "96ae9f6b6377ad29226ea169f952de49fc29ae895f18a2caed76aeabdf050f1b",' . "\n" .
|
||||
' "\/modules\/test2\/file1.php": "94bd47b1ac7b2837b31883ebcd38c8101687321f497c3c4b9744f68ae846721d"' . "\n" .
|
||||
' }' . "\n" .
|
||||
' }' . "\n" .
|
||||
' },' . "\n" .
|
||||
' {' . "\n" .
|
||||
' "build": 3,' . "\n" .
|
||||
' "modules": {' . "\n" .
|
||||
' "test": "419a3c073a4296213cdc9319cfc488383753e2e81cefa1c73db38749b82a3c51",' . "\n" .
|
||||
' "test2": "32c9f2fb6e0a22dde288a0fe1e4834798360b25e5a91d2597409d9302221381d"' . "\n" .
|
||||
' },' . "\n" .
|
||||
' "files": {' . "\n" .
|
||||
' "added": {' . "\n" .
|
||||
' "\/modules\/test\/file3.php": "7f4132b05911a6b0df4d41bf5dc3d007786b63a5a22daf3060ed222816d57b54"' . "\n" .
|
||||
' },' . "\n" .
|
||||
' "modified": {' . "\n" .
|
||||
' "\/modules\/test\/file2.php": "2c61b2f5688275574251a19a57e06a4eb9e537b3916ebf6f71768e184a4ae538"' . "\n" .
|
||||
' },' . "\n" .
|
||||
' "removed": [' . "\n" .
|
||||
' "\/modules\/test\/file1.php"' . "\n" .
|
||||
' ]' . "\n" .
|
||||
' }' . "\n" .
|
||||
' }' . "\n" .
|
||||
' ]' . "\n" .
|
||||
'}',
|
||||
file_get_contents($this->manifestPath())
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetBuilds()
|
||||
{
|
||||
$this->createManifest();
|
||||
|
||||
$buildKeys = array_keys($this->sourceManifest->getBuilds());
|
||||
|
||||
$this->assertCount(3, $buildKeys);
|
||||
$this->assertEquals([1, 2, 3], $buildKeys);
|
||||
}
|
||||
|
||||
public function testGetMaxBuild()
|
||||
{
|
||||
$this->createManifest();
|
||||
|
||||
$this->assertEquals(3, $this->sourceManifest->getMaxBuild());
|
||||
}
|
||||
|
||||
public function testGetState()
|
||||
{
|
||||
$this->createManifest();
|
||||
|
||||
$this->assertEquals([
|
||||
'/modules/test/file1.php' => '6f9b0b94528a85b2a6bb67b5621e074aef1b4c9fc9ee3ea1bd69100ea14cb3db',
|
||||
], $this->sourceManifest->getState(1));
|
||||
|
||||
$this->assertEquals([
|
||||
'/modules/test/file1.php' => '6f9b0b94528a85b2a6bb67b5621e074aef1b4c9fc9ee3ea1bd69100ea14cb3db',
|
||||
'/modules/test/file2.php' => '96ae9f6b6377ad29226ea169f952de49fc29ae895f18a2caed76aeabdf050f1b',
|
||||
'/modules/test2/file1.php' => '94bd47b1ac7b2837b31883ebcd38c8101687321f497c3c4b9744f68ae846721d',
|
||||
], $this->sourceManifest->getState(2));
|
||||
|
||||
$this->assertEquals([
|
||||
'/modules/test/file2.php' => '2c61b2f5688275574251a19a57e06a4eb9e537b3916ebf6f71768e184a4ae538',
|
||||
'/modules/test/file3.php' => '7f4132b05911a6b0df4d41bf5dc3d007786b63a5a22daf3060ed222816d57b54',
|
||||
'/modules/test2/file1.php' => '94bd47b1ac7b2837b31883ebcd38c8101687321f497c3c4b9744f68ae846721d',
|
||||
], $this->sourceManifest->getState(3));
|
||||
}
|
||||
|
||||
public function testCompare()
|
||||
{
|
||||
$this->createManifest();
|
||||
|
||||
$this->assertEquals([
|
||||
'build' => 1,
|
||||
'modified' => false,
|
||||
], $this->sourceManifest->compare($this->builds[1]));
|
||||
|
||||
$this->assertEquals([
|
||||
'build' => 2,
|
||||
'modified' => false,
|
||||
], $this->sourceManifest->compare($this->builds[2]));
|
||||
|
||||
$this->assertEquals([
|
||||
'build' => 3,
|
||||
'modified' => false,
|
||||
], $this->sourceManifest->compare($this->builds[3]));
|
||||
}
|
||||
|
||||
public function testCompareModified()
|
||||
{
|
||||
$this->createManifest();
|
||||
|
||||
// Hot-swap "tests/fixtures/manifest/3/modules/test/file3.php"
|
||||
$old = file_get_contents(base_path('tests/fixtures/manifest/3/modules/test/file3.php'));
|
||||
file_put_contents(base_path('tests/fixtures/manifest/3/modules/test/file3.php'), '<?php // Changed');
|
||||
|
||||
$modifiedManifest = new FileManifest(base_path('tests/fixtures/manifest/3'), ['test', 'test2']);
|
||||
$modifiedManifest->getFiles();
|
||||
|
||||
file_put_contents(base_path('tests/fixtures/manifest/3/modules/test/file3.php'), $old);
|
||||
|
||||
$this->assertEquals([
|
||||
'build' => 3,
|
||||
'modified' => true,
|
||||
], $this->sourceManifest->compare($modifiedManifest));
|
||||
}
|
||||
|
||||
protected function createManifest(bool $write = false)
|
||||
{
|
||||
$this->deleteManifest();
|
||||
|
||||
$last = null;
|
||||
|
||||
foreach ($this->builds as $build => $fileManifest) {
|
||||
$this->sourceManifest->addBuild($build, $fileManifest, $last);
|
||||
|
||||
$last = $build;
|
||||
}
|
||||
|
||||
if ($write) {
|
||||
file_put_contents($this->manifestPath(), $this->sourceManifest->generate());
|
||||
}
|
||||
}
|
||||
|
||||
protected function deleteManifest()
|
||||
{
|
||||
if (file_exists($this->manifestPath())) {
|
||||
unlink($this->manifestPath());
|
||||
}
|
||||
}
|
||||
|
||||
protected function manifestPath()
|
||||
{
|
||||
return base_path('tests/fixtures/manifest/builds.json');
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue