From 5dbfa133e76ce8ec6c4113dec4aa0a2a998a6735 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Thu, 1 Nov 2018 21:53:16 -0600 Subject: [PATCH 01/29] Config file change and cms_themes_contents table migration --- config/cms.php | 26 +++++++++++++++++++ ...018_11_01_000001_Db_Cms_Theme_Contents.php | 25 ++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 modules/cms/database/migrations/2018_11_01_000001_Db_Cms_Theme_Contents.php diff --git a/config/cms.php b/config/cms.php index e0f58e236..c89b3d114 100644 --- a/config/cms.php +++ b/config/cms.php @@ -381,4 +381,30 @@ return [ 'restrictBaseDir' => true, + /* + |-------------------------------------------------------------------------- + | CMS Database Layer + |-------------------------------------------------------------------------- + | + | Enables the database layer for the CMS content files. + | + | Allowed values: + | - false: Database layer is disabled, the FileDatasource is used + | - true: Database layer is enabled, the AutoDatasource is used + | - null: Setting equal to the inverse of app.debug: debug enabled, this disabled + | + | The database layer stores all modified CMS files in the database. + | Files that are not modified continue to be loaded from the filesystem. + | The `theme:sync $themeDir` console command is available to populate the + | database from the filesystem with the `--toFile` flag to sync in the + | other direction (database to filesystem) and the `--path="/path/to/file.md" + | flag to sync only a specific file. + | + | Files available in the database are cached to indicate that they should + | be loaded from the database. + | + */ + + 'enableDatabaseLayer' => false, + ]; diff --git a/modules/cms/database/migrations/2018_11_01_000001_Db_Cms_Theme_Contents.php b/modules/cms/database/migrations/2018_11_01_000001_Db_Cms_Theme_Contents.php new file mode 100644 index 000000000..1c46b0177 --- /dev/null +++ b/modules/cms/database/migrations/2018_11_01_000001_Db_Cms_Theme_Contents.php @@ -0,0 +1,25 @@ +engine = 'InnoDB'; + $table->increments('id'); + $table->string('source')->index(); + $table->string('path')->index(); + $table->longText('content'); + $table->integer('file_size')->unsigned(); + $table->dateTime('updated_at')->nullable(); + }); + } + + public function down() + { + Schema::dropIfExists('cms_theme_contents'); + } +} From e14ded4be61690b668f22c8645c5188eb67ba7dd Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Fri, 2 Nov 2018 16:04:35 -0600 Subject: [PATCH 02/29] Initial work on the AutoDatasource --- modules/cms/classes/AutoDatasource.php | 262 +++++++++++++++++++++++++ modules/cms/classes/Theme.php | 26 ++- 2 files changed, 282 insertions(+), 6 deletions(-) create mode 100644 modules/cms/classes/AutoDatasource.php diff --git a/modules/cms/classes/AutoDatasource.php b/modules/cms/classes/AutoDatasource.php new file mode 100644 index 000000000..5a06b1e88 --- /dev/null +++ b/modules/cms/classes/AutoDatasource.php @@ -0,0 +1,262 @@ +datasources = $datasources; + + $this->populateCache(); + + $this->postProcessor = new Processor; + } + + /** + * Populate the local cache of paths available in each datasource + * + * @param boolean $refresh Default false, set to true to force the cache to be rebuilt + * @return void + */ + protected function populateCache($refresh = false) + { + $pathCache = []; + foreach ($this->datasources as $datasource) { + // Remove any existing cache data + if ($refresh) { + Cache::forget($datasource->getPathsCacheKey); + } + + // Load the cache + $pathCache[] = Cache::rememberForever($datasource->getPathsCacheKey(), function () use ($datasource) { + return $datasource->getAvailablePaths(); + }); + } + $this->pathCache = $pathCache; + } + + /** + * Returns the datasource instances being used internally + * + * @return array + */ + public function getDatasources() + { + return $this->datasources; + } + + /** + * Get the appropriate datasource for the provided path + * + * @param string $path + * @return Datasource + */ + protected function getDatasourceForPath($path) + { + // Default to the last datasource provided + $datasourceIndex = count($this->datasources) - 1; + + foreach ($this->pathCache as $i => $paths) { + if (in_array($path, $paths)) { + $datasourceIndex = $i; + } + } + + return $this->datasources[$datasourceIndex]; + } + + /** + * Helper to make file path. + * + * @param string $dirName + * @param string $fileName + * @param string $extension + * @return string + */ + protected function makeFilePath($dirName, $fileName, $extension) + { + return $dirName . '/' . $fileName . '.' . $extension; + } + + /** + * Returns a single template. + * + * @param string $dirName + * @param string $fileName + * @param string $extension + * @return mixed + */ + public function selectOne($dirName, $fileName, $extension) + { + return $this->getDatasourceForPath($this->makeFilePath($dirName, $fileName, $extension))->selectOne($dirName, $fileName, $extension); + } + + /** + * Returns all templates. + * + * @param string $dirName + * @param array $options Array of options, [ + * 'columns' => ['fileName', 'mtime', 'content'], // Only return specific columns + * 'extensions' => ['htm', 'md', 'twig'], // Extensions to search for + * 'fileMatch' => '*gr[ae]y', // Shell matching pattern to match the filename against using the fnmatch function + * 'orders' => false // Not implemented + * 'limit' => false // Not implemented + * 'offset' => false // Not implemented + * ]; + * @return array + */ + public function select($dirName, array $options = []) + { + // Initialize result set + $sourceResults = []; + + foreach ($this->datasources as $datasource) { + $sourceResults = array_merge($sourceResults, $datasource->select($dirName, $options)); + } + + // Remove duplicate results prioritizing results from earlier datasources + // Reverse the order of the source results so that keyBy prioritizes + // earlier results rather than later results + $results = array_values(collect(array_reverse($sourceResults))->keyBy('fileName')->all()); + + return $results; + } + + /** + * Creates a new template. + * + * @param string $dirName + * @param string $fileName + * @param string $extension + * @param string $content + * @return bool + */ + public function insert($dirName, $fileName, $extension, $content) + { + // @TODO: Implement this + } + + /** + * Updates an existing template. + * + * @param string $dirName + * @param string $fileName + * @param string $extension + * @param string $content + * @param string $oldFileName Defaults to null + * @param string $oldExtension Defaults to null + * @return int + */ + public function update($dirName, $fileName, $extension, $content, $oldFileName = null, $oldExtension = null) + { + // @TODO: Implement this + } + + /** + * Run a delete statement against the datasource. + * + * @param string $dirName + * @param string $fileName + * @param string $extension + * @return int + */ + public function delete($dirName, $fileName, $extension) + { + // Initial implementation, forces delete on every datasource + $exceptionCount = 0; + try { + foreach ($this->datasources as $datasource) { + $datasource->delete($dirName, $fileName, $extension); + } + + // Refresh the cache + $this->populateCache(true); + } + catch (Exception $ex) { + // Only throw exception if content couldn't be removed from any datasource + $exceptionCount++; + if ($exceptionCount >= count($this->datasources)) { + throw (new DeleteFileException)->setInvalidPath($this->makeFilePath($dirName, $fileName, $extension)); + } + } + } + + /** + * Return the last modified date of an object + * + * @param string $dirName + * @param string $fileName + * @param string $extension + * @return int + */ + public function lastModified($dirName, $fileName, $extension) + { + return $this->getDatasourceForPath($this->makeFilePath($dirName, $fileName, $extension))->lastModified($dirName, $fileName, $extension); + } + + /** + * Generate a cache key unique to this datasource. + * + * @param string $name + * @return string + */ + public function makeCacheKey($name = '') + { + $key = ''; + foreach ($this->datasources as $datasource) { + $key .= $datasource->makeCacheKey($name) . '-'; + } + $key .= $name; + + return crc32($key); + } + + /** + * Generate a paths cache key unique to this datasource + * + * @return string + */ + public function getPathsCacheKey() + { + return 'halcyon-datastore-auto'; + } + + /** + * Get all available paths within this datastore + * + * @return array $paths ['path/to/file1.md', 'path/to/file2.md'] + */ + public function getAvailablePaths() + { + $paths = []; + foreach ($this->datasources as $datasource) { + $paths = array_merge($paths, $datasource->getAvailablePaths()); + } + return array_unique($paths); + } +} \ No newline at end of file diff --git a/modules/cms/classes/Theme.php b/modules/cms/classes/Theme.php index 10fce24dd..3fc0c467d 100644 --- a/modules/cms/classes/Theme.php +++ b/modules/cms/classes/Theme.php @@ -8,13 +8,14 @@ use Lang; use Cache; use Event; use Config; -use Cms\Models\ThemeData; -use System\Models\Parameter; -use October\Rain\Halcyon\Datasource\FileDatasource; -use ApplicationException; +use Exception; use SystemException; use DirectoryIterator; -use Exception; +use ApplicationException; +use Cms\Models\ThemeData; +use System\Models\Parameter; +use October\Rain\Halcyon\Datasource\DbDatasource; +use October\Rain\Halcyon\Datasource\FileDatasource; /** * This class represents the CMS theme. @@ -513,7 +514,20 @@ class Theme $resolver = App::make('halcyon'); if (!$resolver->hasDatasource($this->dirName)) { - $datasource = new FileDatasource($this->getPath(), App::make('files')); + $enableDbLayer = Config::get('cms.enableDatabaseLayer', false); + if (is_null($enableDbLayer)) { + $enableDbLayer = !Config::get('app.debug'); + } + + if ($enableDbLayer) { + $datasource = new AutoDatasource([ + new DbDatasource($this->dirName, 'cms_theme_contents'), + new FileDatasource($this->getPath(), App::make('files')), + ]); + } else { + $datasource = new FileDatasource($this->getPath(), App::make('files')); + } + $resolver->addDatasource($this->dirName, $datasource); } } From a4802d503687d9827b640d604974e6712319734e Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Mon, 5 Nov 2018 13:39:55 -0600 Subject: [PATCH 03/29] Minor updates to config cms.enableDatabaseLayer docs --- config/cms.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/cms.php b/config/cms.php index c89b3d114..49e649c27 100644 --- a/config/cms.php +++ b/config/cms.php @@ -397,10 +397,10 @@ return [ | Files that are not modified continue to be loaded from the filesystem. | The `theme:sync $themeDir` console command is available to populate the | database from the filesystem with the `--toFile` flag to sync in the - | other direction (database to filesystem) and the `--path="/path/to/file.md" - | flag to sync only a specific file. + | other direction (database to filesystem) and the `--paths="/path/to/file.md,/path/to/file2.md" + | flag to sync only specific files. | - | Files available in the database are cached to indicate that they should + | Files modified in the database are cached to indicate that they should | be loaded from the database. | */ From bba42c36e98309259ce1af92c52a457f1fadc8fd Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Mon, 5 Nov 2018 14:35:41 -0600 Subject: [PATCH 04/29] Added outline of theme:sync command --- modules/system/ServiceProvider.php | 1 + modules/system/console/ThemeSync.php | 128 +++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 modules/system/console/ThemeSync.php diff --git a/modules/system/ServiceProvider.php b/modules/system/ServiceProvider.php index fd7ec13c1..9792defd0 100644 --- a/modules/system/ServiceProvider.php +++ b/modules/system/ServiceProvider.php @@ -241,6 +241,7 @@ class ServiceProvider extends ModuleServiceProvider $this->registerConsoleCommand('theme.remove', 'System\Console\ThemeRemove'); $this->registerConsoleCommand('theme.list', 'System\Console\ThemeList'); $this->registerConsoleCommand('theme.use', 'System\Console\ThemeUse'); + $this->registerConsoleCommand('theme.sync', 'System\Console\ThemeSync'); } /* diff --git a/modules/system/console/ThemeSync.php b/modules/system/console/ThemeSync.php new file mode 100644 index 000000000..9c009ad02 --- /dev/null +++ b/modules/system/console/ThemeSync.php @@ -0,0 +1,128 @@ +error("The application is not using a database."); + } + + // Check to see if the DB layer is enabled + $enableDbLayer = Config::get('cms.enableDatabaseLayer', false); + if (is_null($enableDbLayer)) { + $enableDbLayer = !Config::get('app.debug'); + } + if (!$enableDbLayer) { + return $this->error("cms.enableDatabaseLayer is not enabled, enable it first and try again."); + } + + // Check to see if the provided theme exists + $themeManager = ThemeManager::instance(); + $themeName = $this->argument('name') ?: Theme::getActiveThemeCode(); + $themeExists = Theme::exists($themeName); + if (!$themeExists) { + $themeName = strtolower(str_replace('.', '-', $themeName)); + $themeExists = Theme::exists($themeName); + } + if (!$themeExists) { + return $this->error(sprintf('The theme %s does not exist.', $themeName)); + } + + // Get the target and source datasources + $availableSources = ['filesystem', 'database']; + $target = $this->option('target') ?: 'database'; + $source = 'filesystem'; + if ($target === 'filesystem') { + $source = 'database'; + } + if (!in_array($target, $availableSources)) { + $this->error(sprintf("Provided --target of %s is invalid. Allowed: database, filesystem"), $target); + } + + // Get the paths + $paths = $this->option('paths') ?: null; + if ($paths) { + $paths = array_map('trim', explode(',', $paths)); + } + + // Confirm with the user + if (!$this->confirmToProceed(sprintf('This will REPLACE the provided paths from "themes/%s" on the %s with content from the %s', $themeName, $target, $source), function () { return true; })) { + return; + } + + try { + // TODO: Actually implement the functionality + + $this->info(sprintf('The theme %s has been synced from the %s to the %s.', $themeName, $source, $target)); + } + catch (Exception $ex) { + $this->error($ex->getMessage()); + } + } + + /** + * Get the console command arguments. + * @return array + */ + protected function getArguments() + { + return [ + ['name', InputArgument::OPTIONAL, 'The name of the theme (directory name). Defaults to currently active theme.'], + ]; + } + + /** + * Get the console command options. + * @return array + */ + protected function getOptions() + { + return [ + ['paths', null, InputOption::VALUE_REQUIRED, 'Comma-separated specific paths (relative to provided theme directory) to specificaly sync. Default is all paths'], + ['target', null, InputOption::VALUE_REQUIRED, 'The target of the sync, the other will be used as the source. Defaults to "database", can be "filesystem"'], + ['force', null, InputOption::VALUE_NONE, 'Force the operation to run.'], + ]; + } +} From 32c7891942359507be230e50b2e09409b42b68b3 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Mon, 5 Nov 2018 14:35:58 -0600 Subject: [PATCH 05/29] Minor changes --- modules/cms/classes/AutoDatasource.php | 2 ++ modules/cms/classes/Theme.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/cms/classes/AutoDatasource.php b/modules/cms/classes/AutoDatasource.php index 5a06b1e88..58cde30a0 100644 --- a/modules/cms/classes/AutoDatasource.php +++ b/modules/cms/classes/AutoDatasource.php @@ -9,6 +9,8 @@ use October\Rain\Halcyon\Datasource\DatasourceInterface; /** * Datasource that loads from other data sources automatically + * + * @Todo: Need to prevent softdeleted DB records from appearing, even if they exist in the filesystem */ class AutoDatasource extends Datasource implements DatasourceInterface { diff --git a/modules/cms/classes/Theme.php b/modules/cms/classes/Theme.php index 3fc0c467d..f688c2657 100644 --- a/modules/cms/classes/Theme.php +++ b/modules/cms/classes/Theme.php @@ -519,7 +519,7 @@ class Theme $enableDbLayer = !Config::get('app.debug'); } - if ($enableDbLayer) { + if ($enableDbLayer && App::hasDatabase()) { $datasource = new AutoDatasource([ new DbDatasource($this->dirName, 'cms_theme_contents'), new FileDatasource($this->getPath(), App::make('files')), From 65e0c9d7b69349b76636624574beecdd63a16aa4 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Mon, 5 Nov 2018 16:02:12 -0600 Subject: [PATCH 06/29] Add initial support for deleted paths --- modules/cms/classes/AutoDatasource.php | 21 ++++++++++++++++--- ...018_11_01_000001_Db_Cms_Theme_Contents.php | 1 + 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/modules/cms/classes/AutoDatasource.php b/modules/cms/classes/AutoDatasource.php index 58cde30a0..9c6da6a5d 100644 --- a/modules/cms/classes/AutoDatasource.php +++ b/modules/cms/classes/AutoDatasource.php @@ -83,10 +83,19 @@ class AutoDatasource extends Datasource implements DatasourceInterface // Default to the last datasource provided $datasourceIndex = count($this->datasources) - 1; + $isDeleted = false; + foreach ($this->pathCache as $i => $paths) { - if (in_array($path, $paths)) { + if (isset($paths[$path])) { $datasourceIndex = $i; - } + + // Set isDeleted to the inverse of the the path's existance flag + $isDeleted = !$paths[$path]; + } + } + + if ($isDeleted) { + throw new Exception("$path is deleted"); } return $this->datasources[$datasourceIndex]; @@ -115,7 +124,13 @@ class AutoDatasource extends Datasource implements DatasourceInterface */ public function selectOne($dirName, $fileName, $extension) { - return $this->getDatasourceForPath($this->makeFilePath($dirName, $fileName, $extension))->selectOne($dirName, $fileName, $extension); + try { + $result = $this->getDatasourceForPath($this->makeFilePath($dirName, $fileName, $extension))->selectOne($dirName, $fileName, $extension); + } catch (Exception $ex) { + $result = null; + } + + return $result; } /** diff --git a/modules/cms/database/migrations/2018_11_01_000001_Db_Cms_Theme_Contents.php b/modules/cms/database/migrations/2018_11_01_000001_Db_Cms_Theme_Contents.php index 1c46b0177..0ddb5631f 100644 --- a/modules/cms/database/migrations/2018_11_01_000001_Db_Cms_Theme_Contents.php +++ b/modules/cms/database/migrations/2018_11_01_000001_Db_Cms_Theme_Contents.php @@ -15,6 +15,7 @@ class DbCmsThemeContents extends Migration $table->longText('content'); $table->integer('file_size')->unsigned(); $table->dateTime('updated_at')->nullable(); + $table->dateTime('deleted_at')->nullable(); }); } From 4887519e27024353314df39400e426c7a616ff3a Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Tue, 6 Nov 2018 14:22:05 -0600 Subject: [PATCH 07/29] AutoDatasource bug fixes and performance improvements Properly remove deleted paths from results returned by AutoDatasource->select() and utilize the cache exclusively when only selecting the fileName column preventing calls to the source datasources. --- modules/cms/classes/AutoDatasource.php | 87 ++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 11 deletions(-) diff --git a/modules/cms/classes/AutoDatasource.php b/modules/cms/classes/AutoDatasource.php index 9c6da6a5d..078af4a5d 100644 --- a/modules/cms/classes/AutoDatasource.php +++ b/modules/cms/classes/AutoDatasource.php @@ -51,7 +51,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface foreach ($this->datasources as $datasource) { // Remove any existing cache data if ($refresh) { - Cache::forget($datasource->getPathsCacheKey); + Cache::forget($datasource->getPathsCacheKey()); } // Load the cache @@ -101,6 +101,49 @@ class AutoDatasource extends Datasource implements DatasourceInterface return $this->datasources[$datasourceIndex]; } + /** + * Get all valid paths for the provided directory, removing any paths marked as deleted + * + * @param string $dirName + * @param array $options Array of options, [ + * 'extensions' => ['htm', 'md', 'twig'], // Extensions to search for + * 'fileMatch' => '*gr[ae]y', // Shell matching pattern to match the filename against using the fnmatch function + * ]; + * @return array $paths ["$dirName/path/1.md", "$dirName/path/2.md"] + */ + protected function getValidPaths($dirName, $options) + { + // Initialize result set + $paths = []; + + // Reverse the order of the sources so that earlier + // sources are prioritized over later sources + $pathsCache = array_reverse($this->pathCache); + + // Get paths available in the provided dirName, allowing proper prioritization of earlier datasources + foreach ($pathsCache as $sourcePaths) { + $paths = array_merge($paths, array_filter($sourcePaths, function ($path) use ($dirName, $options) { + $basePath = $dirName . '/'; + + $inPath = starts_with($path, $basePath); + + // Check the fileMatch if provided as an option + $fnMatch = !empty($options['fileMatch']) ? fnmatch($options['fileMatch'], str_after($path, $basePath)) : true; + + // Check the extension if provided as an option + $validExt = is_array($options['extensions']) && !empty($options['extensions']) ? in_array(pathinfo($path, PATHINFO_EXTENSION), $options['extensions']) : true; + + return $inPath && $fnMatch && $validExt; + }, ARRAY_FILTER_USE_KEY)); + } + + // Filter out 'deleted' paths: + $paths = array_filter($paths, function ($value) { return (bool) $value; }); + + // Return just an array of paths + return array_keys($paths); + } + /** * Helper to make file path. * @@ -129,7 +172,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface } catch (Exception $ex) { $result = null; } - + return $result; } @@ -149,18 +192,40 @@ class AutoDatasource extends Datasource implements DatasourceInterface */ public function select($dirName, array $options = []) { - // Initialize result set - $sourceResults = []; + // Handle fileName listings through just the cache + if (@$options['columns'] === ['fileName']) { + // Return just filenames of the valid paths for this directory + $results = array_values(array_map(function ($path) use ($dirName) { + return ['fileName' => str_after($path, $dirName . '/')]; + }, $this->getValidPaths($dirName, $options))); - foreach ($this->datasources as $datasource) { - $sourceResults = array_merge($sourceResults, $datasource->select($dirName, $options)); + // Retrieve full listings from datasources directly + } else { + // Initialize result set + $sourceResults = []; + + // Reverse the order of the sources so that earlier + // sources are prioritized over later sources + $datasources = array_reverse($this->datasources); + + foreach ($datasources as $datasource) { + $sourceResults = array_merge($sourceResults, $datasource->select($dirName, $options)); + } + + // Remove duplicate results prioritizing results from earlier datasources + $sourceResults = collect($sourceResults)->keyBy('fileName'); + + // Get a list of valid filenames from the list of valid paths for this directory + $validFiles = array_map(function ($path) use ($dirName) { + return str_after($path, $dirName . '/'); + }, $this->getValidPaths($dirName, $options)); + + // Filter out deleted paths + $results = array_values($sourceResults->filter(function ($value, $key) use ($validFiles) { + return in_array($key, $validFiles); + })->all()); } - // Remove duplicate results prioritizing results from earlier datasources - // Reverse the order of the source results so that keyBy prioritizes - // earlier results rather than later results - $results = array_values(collect(array_reverse($sourceResults))->keyBy('fileName')->all()); - return $results; } From 0efae6dc336c40210286f79d35fd42ad7e5fad03 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Tue, 6 Nov 2018 14:38:19 -0600 Subject: [PATCH 08/29] AutoDatasource deletions now occur only on first datasource --- modules/cms/classes/AutoDatasource.php | 61 ++++++++++++++++---------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/modules/cms/classes/AutoDatasource.php b/modules/cms/classes/AutoDatasource.php index 078af4a5d..8126ddced 100644 --- a/modules/cms/classes/AutoDatasource.php +++ b/modules/cms/classes/AutoDatasource.php @@ -9,7 +9,7 @@ use October\Rain\Halcyon\Datasource\DatasourceInterface; /** * Datasource that loads from other data sources automatically - * + * * @Todo: Need to prevent softdeleted DB records from appearing, even if they exist in the filesystem */ class AutoDatasource extends Datasource implements DatasourceInterface @@ -26,7 +26,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface /** * Create a new datasource instance. - * + * * @param array $datasources Array of datasources to utilize. Lower indexes = higher priority * @return void */ @@ -35,7 +35,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface $this->datasources = $datasources; $this->populateCache(); - + $this->postProcessor = new Processor; } @@ -74,7 +74,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface /** * Get the appropriate datasource for the provided path - * + * * @param string $path * @return Datasource */ @@ -88,10 +88,10 @@ class AutoDatasource extends Datasource implements DatasourceInterface foreach ($this->pathCache as $i => $paths) { if (isset($paths[$path])) { $datasourceIndex = $i; - - // Set isDeleted to the inverse of the the path's existance flag + + // Set isDeleted to the inverse of the the path's existance flag $isDeleted = !$paths[$path]; - } + } } if ($isDeleted) { @@ -111,12 +111,12 @@ class AutoDatasource extends Datasource implements DatasourceInterface * ]; * @return array $paths ["$dirName/path/1.md", "$dirName/path/2.md"] */ - protected function getValidPaths($dirName, $options) + protected function getValidPaths($dirName, $options = []) { // Initialize result set $paths = []; - // Reverse the order of the sources so that earlier + // Reverse the order of the sources so that earlier // sources are prioritized over later sources $pathsCache = array_reverse($this->pathCache); @@ -146,7 +146,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface /** * Helper to make file path. - * + * * @param string $dirName * @param string $fileName * @param string $extension @@ -204,7 +204,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface // Initialize result set $sourceResults = []; - // Reverse the order of the sources so that earlier + // Reverse the order of the sources so that earlier // sources are prioritized over later sources $datasources = array_reverse($this->datasources); @@ -220,7 +220,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface return str_after($path, $dirName . '/'); }, $this->getValidPaths($dirName, $options)); - // Filter out deleted paths + // Filter out deleted paths $results = array_values($sourceResults->filter(function ($value, $key) use ($validFiles) { return in_array($key, $validFiles); })->all()); @@ -241,6 +241,9 @@ class AutoDatasource extends Datasource implements DatasourceInterface public function insert($dirName, $fileName, $extension, $content) { // @TODO: Implement this + + // Refresh the cache + $this->populateCache(true); } /** @@ -257,10 +260,13 @@ class AutoDatasource extends Datasource implements DatasourceInterface public function update($dirName, $fileName, $extension, $content, $oldFileName = null, $oldExtension = null) { // @TODO: Implement this + + // Refresh the cache + $this->populateCache(true); } /** - * Run a delete statement against the datasource. + * Run a delete statement against the datasource, only runs delete on first datasource * * @param string $dirName * @param string $fileName @@ -269,21 +275,28 @@ class AutoDatasource extends Datasource implements DatasourceInterface */ public function delete($dirName, $fileName, $extension) { - // Initial implementation, forces delete on every datasource - $exceptionCount = 0; try { - foreach ($this->datasources as $datasource) { - $datasource->delete($dirName, $fileName, $extension); - } + // Delete from only the first datasource + $this->datasources[0]->delete($dirName, $fileName, $extension); // Refresh the cache $this->populateCache(true); } catch (Exception $ex) { - // Only throw exception if content couldn't be removed from any datasource - $exceptionCount++; - if ($exceptionCount >= count($this->datasources)) { - throw (new DeleteFileException)->setInvalidPath($this->makeFilePath($dirName, $fileName, $extension)); + // Check to see if this is a valid path to delete + $path = $this->makeFilePath($dirName, $fileName, $extension); + + if (in_array($path, $this->getValidPaths($dirName))) { + // Retrieve the current record + $record = $this->selectOne($dirName, $fileName, $extension); + + // Insert the current record into the first datasource so we can mark it as deleted + $this->datasource[0]->insert($dirName, $fileName, $extension, $record['content']); + + // Perform the deletion on the newly inserted record + $this->delete($dirName, $fileName, $extension); + } else { + throw (new DeleteFileException)->setInvalidPath($path); } } } @@ -320,7 +333,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface /** * Generate a paths cache key unique to this datasource - * + * * @return string */ public function getPathsCacheKey() @@ -330,7 +343,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface /** * Get all available paths within this datastore - * + * * @return array $paths ['path/to/file1.md', 'path/to/file2.md'] */ public function getAvailablePaths() From e5518e097694c1b244eae97cd0e0754e2156d2b9 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Tue, 6 Nov 2018 14:48:47 -0600 Subject: [PATCH 09/29] Update AutoDatasource->getAvailablePaths implementation to match updated interface --- modules/cms/classes/AutoDatasource.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/cms/classes/AutoDatasource.php b/modules/cms/classes/AutoDatasource.php index 8126ddced..ab80be836 100644 --- a/modules/cms/classes/AutoDatasource.php +++ b/modules/cms/classes/AutoDatasource.php @@ -344,14 +344,15 @@ class AutoDatasource extends Datasource implements DatasourceInterface /** * Get all available paths within this datastore * - * @return array $paths ['path/to/file1.md', 'path/to/file2.md'] + * @return array $paths ['path/to/file1.md' => true (path can be handled and exists), 'path/to/file2.md' => false (path can be handled but doesn't exist)] */ public function getAvailablePaths() { $paths = []; - foreach ($this->datasources as $datasource) { + $datasources = array_reverse($this->datasources); + foreach ($datasources as $datasource) { $paths = array_merge($paths, $datasource->getAvailablePaths()); } - return array_unique($paths); + return $paths; } } \ No newline at end of file From e622defed1c93d7c54b6c47790bb0cfaa085df6d Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Tue, 6 Nov 2018 16:00:27 -0600 Subject: [PATCH 10/29] Initial implementation of insert() and update() on the AutoDatasource --- modules/cms/classes/AutoDatasource.php | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/modules/cms/classes/AutoDatasource.php b/modules/cms/classes/AutoDatasource.php index ab80be836..1ebf8f8a9 100644 --- a/modules/cms/classes/AutoDatasource.php +++ b/modules/cms/classes/AutoDatasource.php @@ -91,6 +91,9 @@ class AutoDatasource extends Datasource implements DatasourceInterface // Set isDeleted to the inverse of the the path's existance flag $isDeleted = !$paths[$path]; + + // Break on first datasource that can handle the path + break; } } @@ -230,7 +233,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface } /** - * Creates a new template. + * Creates a new template, only inserts to the first datasource * * @param string $dirName * @param string $fileName @@ -240,10 +243,12 @@ class AutoDatasource extends Datasource implements DatasourceInterface */ public function insert($dirName, $fileName, $extension, $content) { - // @TODO: Implement this + $result = $this->datasources[0]->insert($dirName, $fileName, $extension, $content); // Refresh the cache $this->populateCache(true); + + return $result; } /** @@ -259,10 +264,19 @@ class AutoDatasource extends Datasource implements DatasourceInterface */ public function update($dirName, $fileName, $extension, $content, $oldFileName = null, $oldExtension = null) { - // @TODO: Implement this + $searchFileName = $oldFileName ?: $fileName; + $searchExt = $oldExtension ?: $oldExtension; + + if (!empty($this->datasources[0]->selectOne($dirName, $searchFileName, $searchExt))) { + $result = $this->datasources[0]->update($dirName, $fileName, $extension, $content, $oldFileName, $oldExtension); + } else { + $result = $this->datasources[0]->insert($dirName, $fileName, $extension, $content); + } // Refresh the cache $this->populateCache(true); + + return $result; } /** @@ -291,7 +305,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface $record = $this->selectOne($dirName, $fileName, $extension); // Insert the current record into the first datasource so we can mark it as deleted - $this->datasource[0]->insert($dirName, $fileName, $extension, $record['content']); + $this->insert($dirName, $fileName, $extension, $record['content']); // Perform the deletion on the newly inserted record $this->delete($dirName, $fileName, $extension); From 45bd2654fc93ab2777b6776846954754b6ee4e0d Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Wed, 14 Nov 2018 16:52:46 -0600 Subject: [PATCH 11/29] Added Meta CmsObject, fixed bug in AutoDatasource --- modules/cms/classes/AutoDatasource.php | 2 +- modules/cms/classes/Meta.php | 85 ++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 modules/cms/classes/Meta.php diff --git a/modules/cms/classes/AutoDatasource.php b/modules/cms/classes/AutoDatasource.php index 1ebf8f8a9..b90a43836 100644 --- a/modules/cms/classes/AutoDatasource.php +++ b/modules/cms/classes/AutoDatasource.php @@ -265,7 +265,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface public function update($dirName, $fileName, $extension, $content, $oldFileName = null, $oldExtension = null) { $searchFileName = $oldFileName ?: $fileName; - $searchExt = $oldExtension ?: $oldExtension; + $searchExt = $oldExtension ?: $extension; if (!empty($this->datasources[0]->selectOne($dirName, $searchFileName, $searchExt))) { $result = $this->datasources[0]->update($dirName, $fileName, $extension, $content, $oldFileName, $oldExtension); diff --git a/modules/cms/classes/Meta.php b/modules/cms/classes/Meta.php new file mode 100644 index 000000000..450d5b58f --- /dev/null +++ b/modules/cms/classes/Meta.php @@ -0,0 +1,85 @@ +bindEvent('model.beforeSave', function () { + $this->content = $this->renderContent(); + }); + $this->bindEvent('model.afterFetch', function () { + $this->attributes = array_merge($this->parseContent(), $this->attributes); + }); + } + + /** + * Processes the content attribute to an array of menu data. + * @return array|null + */ + protected function parseContent() + { + if ($this->contentDataCache !== null) { + return $this->contentDataCache; + } + + $parsedData = Yaml::parse($this->content); + + if (!is_array($parsedData)) { + return null; + } + + return $this->contentDataCache = $parsedData; + } + + /** + * Renders the meta data as a content string in YAML format. + * @return string + */ + protected function renderContent() + { + return Yaml::render($this->settings); + } + + /** + * Compile the content for this CMS object, used by the theme logger. + * @return string + */ + public function toCompiled() + { + return $this->renderContent(); + } +} \ No newline at end of file From 38fe4af1e6cd4a91d098e4ff786be168129d6c78 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Thu, 15 Nov 2018 15:14:05 -0600 Subject: [PATCH 12/29] Fixed minor oversight in delete() method on AutoDatasource --- modules/cms/classes/AutoDatasource.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/cms/classes/AutoDatasource.php b/modules/cms/classes/AutoDatasource.php index b90a43836..173ad4833 100644 --- a/modules/cms/classes/AutoDatasource.php +++ b/modules/cms/classes/AutoDatasource.php @@ -292,9 +292,6 @@ class AutoDatasource extends Datasource implements DatasourceInterface try { // Delete from only the first datasource $this->datasources[0]->delete($dirName, $fileName, $extension); - - // Refresh the cache - $this->populateCache(true); } catch (Exception $ex) { // Check to see if this is a valid path to delete @@ -313,6 +310,9 @@ class AutoDatasource extends Datasource implements DatasourceInterface throw (new DeleteFileException)->setInvalidPath($path); } } + + // Refresh the cache + $this->populateCache(true); } /** From 1920d5b4b6edb25fdf6b316e74bff3ee1926c049 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Thu, 22 Nov 2018 12:04:41 -0600 Subject: [PATCH 13/29] Fix bug where updating records that didn't exist in the DB yet would cause both the original and new records to display --- modules/cms/classes/AutoDatasource.php | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/modules/cms/classes/AutoDatasource.php b/modules/cms/classes/AutoDatasource.php index 173ad4833..42d3b6594 100644 --- a/modules/cms/classes/AutoDatasource.php +++ b/modules/cms/classes/AutoDatasource.php @@ -10,7 +10,8 @@ use October\Rain\Halcyon\Datasource\DatasourceInterface; /** * Datasource that loads from other data sources automatically * - * @Todo: Need to prevent softdeleted DB records from appearing, even if they exist in the filesystem + * @package october\cms + * @author Luke Towers */ class AutoDatasource extends Datasource implements DatasourceInterface { @@ -24,6 +25,11 @@ class AutoDatasource extends Datasource implements DatasourceInterface */ protected $pathCache = []; + /** + * @var boolean Flag on whether the cache should respect refresh requests + */ + protected $allowCacheRefreshes = true; + /** * Create a new datasource instance. * @@ -50,7 +56,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface $pathCache = []; foreach ($this->datasources as $datasource) { // Remove any existing cache data - if ($refresh) { + if ($refresh && $this->allowCacheRefreshes) { Cache::forget($datasource->getPathsCacheKey()); } @@ -134,7 +140,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface $fnMatch = !empty($options['fileMatch']) ? fnmatch($options['fileMatch'], str_after($path, $basePath)) : true; // Check the extension if provided as an option - $validExt = is_array($options['extensions']) && !empty($options['extensions']) ? in_array(pathinfo($path, PATHINFO_EXTENSION), $options['extensions']) : true; + $validExt = !empty($options['extensions']) && is_array($options['extensions']) ? in_array(pathinfo($path, PATHINFO_EXTENSION), $options['extensions']) : true; return $inPath && $fnMatch && $validExt; }, ARRAY_FILTER_USE_KEY)); @@ -267,6 +273,14 @@ class AutoDatasource extends Datasource implements DatasourceInterface $searchFileName = $oldFileName ?: $fileName; $searchExt = $oldExtension ?: $extension; + // Ensure that files that are being renamed have their old names marked as deleted prior to inserting the renamed file + // Also ensure that the cache only gets updated at the end of this operation instead of twice, once here and again at the end + if ($searchFileName !== $fileName || $searchExt !== $extension) { + $this->allowCacheRefreshes = false; + $this->delete($dirName, $searchFileName, $searchExt); + $this->allowCacheRefreshes = true; + } + if (!empty($this->datasources[0]->selectOne($dirName, $searchFileName, $searchExt))) { $result = $this->datasources[0]->update($dirName, $fileName, $extension, $content, $oldFileName, $oldExtension); } else { From 0a8450b21cb279cd267f5254b431376612c241ff Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Thu, 22 Nov 2018 13:34:19 -0600 Subject: [PATCH 14/29] Added base structure of the Commit & Reset buttons, NOTE: Have not actually implemented the logic for them yet, just added the initial shells / UI for them. --- modules/cms/assets/js/october.cmspage.js | 14 ++- modules/cms/controllers/Index.php | 111 +++++++++++++++--- .../index/_common_toolbar_actions.htm | 34 ++++++ .../controllers/index/_content_toolbar.htm | 17 +-- .../cms/controllers/index/_layout_toolbar.htm | 17 +-- .../cms/controllers/index/_page_toolbar.htm | 17 +-- .../controllers/index/_partial_toolbar.htm | 17 +-- modules/cms/lang/en/lang.php | 6 +- 8 files changed, 149 insertions(+), 84 deletions(-) create mode 100644 modules/cms/controllers/index/_common_toolbar_actions.htm diff --git a/modules/cms/assets/js/october.cmspage.js b/modules/cms/assets/js/october.cmspage.js index 6692e2715..9361581b7 100644 --- a/modules/cms/assets/js/october.cmspage.js +++ b/modules/cms/assets/js/october.cmspage.js @@ -173,7 +173,7 @@ var dataId = $target.closest('li').attr('data-tab-id'), title = $target.attr('title'), $sidePanel = $('#cms-side-panel') - + if (title) this.setPageTitle(title) @@ -250,6 +250,8 @@ $form.on('changed.oc.changeMonitor', function() { $panel.trigger('modified.oc.tab') + $panel.find('[data-control=commit-button]').addClass('hide'); + $panel.find('[data-control=reset-button]').addClass('hide'); self.updateModifiedCounter() }) @@ -279,6 +281,10 @@ CmsPage.prototype.onAjaxSuccess = function(ev, context, data) { var element = ev.target + // Update the visibilities of the commit & reset buttons + $('[data-control=commit-button]', element).toggleClass('hide', !data.canCommit) + $('[data-control=reset-button]', element).toggleClass('hide', !data.canReset) + if (data.templatePath !== undefined) { $('input[name=templatePath]', element).val(data.templatePath) $('input[name=templateMtime]', element).val(data.templateMtime) @@ -359,7 +365,7 @@ }).done(function(data) { var tabs = $('#cms-master-tabs').data('oc.tab'); $.each(data.deleted, function(index, path){ - var + var tabId = templateType + '-' + data.theme + '-' + path, tab = tabs.findByIdentifier(tabId) @@ -640,7 +646,7 @@ } CmsPage.prototype.reloadForm = function(form) { - var + var $form = $(form), data = { type: $('[name=templateType]', $form).val(), @@ -682,7 +688,7 @@ $(form).request('onGetTemplateList', { success: function(data) { $('#cms-master-tabs > .tab-content select[name="settings[layout]"]').each(function(){ - var + var $select = $(this), value = $select.val() diff --git a/modules/cms/controllers/Index.php b/modules/cms/controllers/Index.php index 7751ede8f..cdef7f6d7 100644 --- a/modules/cms/controllers/Index.php +++ b/modules/cms/controllers/Index.php @@ -134,6 +134,8 @@ class Index extends Controller $this->vars['templatePath'] = Request::input('path'); $this->vars['lastModified'] = DateTime::makeCarbon($template->mtime); + $this->vars['canCommit'] = $this->canCommitTemplate($template); + $this->vars['canReset'] = $this->canResetTemplate($template); if ($type === 'page') { $router = new RainRouter; @@ -225,20 +227,7 @@ class Index extends Controller Flash::success(Lang::get('cms::lang.template.saved')); - $result = [ - 'templatePath' => $template->fileName, - 'templateMtime' => $template->mtime, - 'tabTitle' => $this->getTabTitle($type, $template) - ]; - - if ($type === 'page') { - $result['pageUrl'] = Url::to($template->url); - $router = new Router($this->theme); - $router->clearCache(); - CmsCompoundObject::clearCache($this->theme); - } - - return $result; + return $this->getUpdateResponse($template, $type); } /** @@ -266,6 +255,8 @@ class Index extends Controller $widget = $this->makeTemplateFormWidget($type, $template); $this->vars['templatePath'] = ''; + $this->vars['canCommit'] = $this->canCommitTemplate($template); + $this->vars['canReset'] = $this->canResetTemplate($template); return [ 'tabTitle' => $this->getTabTitle($type, $template), @@ -397,10 +388,100 @@ class Index extends Controller return $content; } + /** + * Commits the DB changes of a template to the filesystem + * + * @return void + */ + public function onCommit() + { + $this->validateRequestTheme(); + $type = Request::input('templateType'); + $template = $this->loadTemplate($type, trim(Request::input('templatePath'))); + + if ($this->canCommitTemplate($template)) { + + } + + return $this->getUpdateResponse($template, $type); + } + + /** + * Resets a template to the version on the filesystem + * + * @return void + */ + public function onReset() + { + $this->validateRequestTheme(); + $type = Request::input('templateType'); + $template = $this->loadTemplate($type, trim(Request::input('templatePath'))); + + if ($this->canResetTemplate($template)) { + + } + + return $this->getUpdateResponse($template, $type); + } + // - // Methods for the internal use + // Methods for internal use // + /** + * Get the response to return in an AJAX request that updates a template + * + * @param CmsObject $template The template that has been affected + * @param string $type The type of template being affected + * @return array $result; + */ + protected function getUpdateResponse($template, $type) + { + $result = [ + 'templatePath' => $template->fileName, + 'templateMtime' => $template->mtime, + 'tabTitle' => $this->getTabTitle($type, $template) + ]; + + if ($type === 'page') { + $result['pageUrl'] = Url::to($template->url); + $router = new Router($this->theme); + $router->clearCache(); + CmsCompoundObject::clearCache($this->theme); + } + + $result['canCommit'] = $this->canCommitTemplate($template); + $result['canReset'] = $this->canResetTemplate($template); + + return $result; + } + + /** + * Check to see if the provided template can be committed + * + * @param CmsObject $template + * @return boolean + */ + protected function canCommitTemplate($template) + { + $result = true; + + return $result; + } + + /** + * Check to see if the provided template can be reset + * + * @param CmsObject $template + * @return boolean + */ + protected function canResetTemplate($template) + { + $result = true; + + return $result; + } + /** * Validate that the current request is within the active theme * @return void diff --git a/modules/cms/controllers/index/_common_toolbar_actions.htm b/modules/cms/controllers/index/_common_toolbar_actions.htm new file mode 100644 index 000000000..42b247e8e --- /dev/null +++ b/modules/cms/controllers/index/_common_toolbar_actions.htm @@ -0,0 +1,34 @@ + + + + + + + + + + \ No newline at end of file diff --git a/modules/cms/controllers/index/_content_toolbar.htm b/modules/cms/controllers/index/_content_toolbar.htm index fb01abe95..02476a792 100644 --- a/modules/cms/controllers/index/_content_toolbar.htm +++ b/modules/cms/controllers/index/_content_toolbar.htm @@ -8,20 +8,5 @@ - - - - - - + makePartial('common_toolbar_actions', ['toolbarSource' => 'content']); ?> diff --git a/modules/cms/controllers/index/_layout_toolbar.htm b/modules/cms/controllers/index/_layout_toolbar.htm index 1510ea3ca..fe336a1b1 100644 --- a/modules/cms/controllers/index/_layout_toolbar.htm +++ b/modules/cms/controllers/index/_layout_toolbar.htm @@ -8,20 +8,5 @@ - - - - - - + makePartial('common_toolbar_actions', ['toolbarSource' => 'layout']); ?> diff --git a/modules/cms/controllers/index/_page_toolbar.htm b/modules/cms/controllers/index/_page_toolbar.htm index 790da0689..e72f76d0b 100644 --- a/modules/cms/controllers/index/_page_toolbar.htm +++ b/modules/cms/controllers/index/_page_toolbar.htm @@ -19,20 +19,5 @@ - - - - - - + makePartial('common_toolbar_actions', ['toolbarSource' => 'page']); ?> diff --git a/modules/cms/controllers/index/_partial_toolbar.htm b/modules/cms/controllers/index/_partial_toolbar.htm index 6cfc6dd49..4f4df2cbd 100644 --- a/modules/cms/controllers/index/_partial_toolbar.htm +++ b/modules/cms/controllers/index/_partial_toolbar.htm @@ -8,20 +8,5 @@ - - - - - - + makePartial('common_toolbar_actions', ['toolbarSource' => 'partial']); ?> diff --git a/modules/cms/lang/en/lang.php b/modules/cms/lang/en/lang.php index d96915e4c..7bf82ba7c 100644 --- a/modules/cms/lang/en/lang.php +++ b/modules/cms/lang/en/lang.php @@ -185,7 +185,11 @@ return [ 'open_searchbox' => 'Open Search box', 'close_searchbox' => 'Close Search box', 'open_replacebox' => 'Open Replace box', - 'close_replacebox' => 'Close Replace box' + 'close_replacebox' => 'Close Replace box', + 'reset' => 'Reset', + 'commit' => 'Commit', + 'reset_confirm' => 'Are you sure you want to reset this file to the copy that is on the filesystem? This will completely replace it with the file that is on the filesystem', + 'commit_confirm' => 'Are you sure you want to commit your changes to this file to the filesystem? This will overwrite the existing file on the filesystem', ], 'asset' => [ 'menu_label' => 'Assets', From 7809f9ada556121c9e6d457700b0a2a6b451259d Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Thu, 22 Nov 2018 13:41:11 -0600 Subject: [PATCH 15/29] Added a Theme::databaseLayerEnabled method to make checking for the db layer easier --- modules/cms/classes/Theme.php | 22 ++++++++++++++++------ modules/cms/controllers/Index.php | 12 ++++++++++-- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/modules/cms/classes/Theme.php b/modules/cms/classes/Theme.php index f688c2657..f975c0e72 100644 --- a/modules/cms/classes/Theme.php +++ b/modules/cms/classes/Theme.php @@ -505,6 +505,21 @@ class Theme return ThemeData::forTheme($this); } + /** + * Checks to see if the database layer has been enabled + * + * @return boolean + */ + public static function databaseLayerEnabled() + { + $enableDbLayer = Config::get('cms.enableDatabaseLayer', false); + if (is_null($enableDbLayer)) { + $enableDbLayer = !Config::get('app.debug'); + } + + return $enableDbLayer && App::hasDatabase(); + } + /** * Ensures this theme is registered as a Halcyon them datasource. * @return void @@ -514,12 +529,7 @@ class Theme $resolver = App::make('halcyon'); if (!$resolver->hasDatasource($this->dirName)) { - $enableDbLayer = Config::get('cms.enableDatabaseLayer', false); - if (is_null($enableDbLayer)) { - $enableDbLayer = !Config::get('app.debug'); - } - - if ($enableDbLayer && App::hasDatabase()) { + if (static::databaseLayerEnabled()) { $datasource = new AutoDatasource([ new DbDatasource($this->dirName, 'cms_theme_contents'), new FileDatasource($this->getPath(), App::make('files')), diff --git a/modules/cms/controllers/Index.php b/modules/cms/controllers/Index.php index cdef7f6d7..fc5a8bf7d 100644 --- a/modules/cms/controllers/Index.php +++ b/modules/cms/controllers/Index.php @@ -464,7 +464,11 @@ class Index extends Controller */ protected function canCommitTemplate($template) { - $result = true; + $result = true; // will set to false by default + + if (Theme::databaseLayerEnabled()) { + + } return $result; } @@ -477,7 +481,11 @@ class Index extends Controller */ protected function canResetTemplate($template) { - $result = true; + $result = true; // will set to false by default + + if (Theme::databaseLayerEnabled()) { + + } return $result; } From f730fc85e1d977f6e0bcb2bb7166830f2fe2da71 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Thu, 22 Nov 2018 16:54:35 -0600 Subject: [PATCH 16/29] Fix import statement --- modules/cms/classes/Meta.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/cms/classes/Meta.php b/modules/cms/classes/Meta.php index 450d5b58f..bfebf016d 100644 --- a/modules/cms/classes/Meta.php +++ b/modules/cms/classes/Meta.php @@ -1,6 +1,6 @@ Date: Fri, 23 Nov 2018 11:07:36 -0600 Subject: [PATCH 17/29] Implement canCommitTemplate and canResetTemplate checks, added UX for commit / reset buttons (load indicator & success flash messages) --- modules/cms/classes/AutoDatasource.php | 58 +++++++++++++++---- modules/cms/classes/Theme.php | 20 +++++-- modules/cms/controllers/Index.php | 31 ++++++---- .../index/_common_toolbar_actions.htm | 2 + modules/cms/lang/en/lang.php | 8 ++- 5 files changed, 93 insertions(+), 26 deletions(-) diff --git a/modules/cms/classes/AutoDatasource.php b/modules/cms/classes/AutoDatasource.php index 42d3b6594..9f734f40b 100644 --- a/modules/cms/classes/AutoDatasource.php +++ b/modules/cms/classes/AutoDatasource.php @@ -2,6 +2,7 @@ use Cache; use Exception; +use October\Rain\Halcyon\Model; use October\Rain\Halcyon\Processors\Processor; use October\Rain\Halcyon\Datasource\Datasource; use October\Rain\Halcyon\Exception\DeleteFileException; @@ -33,7 +34,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface /** * Create a new datasource instance. * - * @param array $datasources Array of datasources to utilize. Lower indexes = higher priority + * @param array $datasources Array of datasources to utilize. Lower indexes = higher priority ['datasourceName' => $datasource] * @return void */ public function __construct($datasources) @@ -69,13 +70,34 @@ class AutoDatasource extends Datasource implements DatasourceInterface } /** - * Returns the datasource instances being used internally + * Check to see if the specified datasource has the provided Halcyon Model * - * @return array + * @param string $source The string key of the datasource to check + * @param Model $model The Halcyon Model to check for + * @return boolean */ - public function getDatasources() + public function sourceHasModel(string $source, Model $model) { - return $this->datasources; + $result = false; + + $keys = array_keys($this->datasources); + if (in_array($source, $keys)) { + // Get the datasource's cache index key + $cacheIndex = array_search($source, $keys); + + // Generate the path + list($name, $extension) = $model->getFileNameParts(); + $path = $this->makeFilePath($model->getObjectTypeDirName(), $name, $extension); + + // Deleted paths are included as being handled by a datasource + // The functionality built on this will need to make sure they + // include deleted records when actually performing sycning actions + if (isset($this->pathCache[$cacheIndex][$path])) { + $result = true; + } + } + + return $result; } /** @@ -107,6 +129,8 @@ class AutoDatasource extends Datasource implements DatasourceInterface throw new Exception("$path is deleted"); } + $datasourceIndex = array_keys($this->datasources)[$datasourceIndex]; + return $this->datasources[$datasourceIndex]; } @@ -166,6 +190,17 @@ class AutoDatasource extends Datasource implements DatasourceInterface return $dirName . '/' . $fileName . '.' . $extension; } + /** + * Get the first datasource for use with CRUD operations + * + * @return DatasourceInterface + */ + protected function getFirstDatasource() + { + $keys = array_keys($this->datasources); + return $this->datasources[$keys[0]]; + } + /** * Returns a single template. * @@ -249,7 +284,8 @@ class AutoDatasource extends Datasource implements DatasourceInterface */ public function insert($dirName, $fileName, $extension, $content) { - $result = $this->datasources[0]->insert($dirName, $fileName, $extension, $content); + // Insert only on the first datasource + $result = $this->getFirstDatasource()->insert($dirName, $fileName, $extension, $content); // Refresh the cache $this->populateCache(true); @@ -281,10 +317,12 @@ class AutoDatasource extends Datasource implements DatasourceInterface $this->allowCacheRefreshes = true; } - if (!empty($this->datasources[0]->selectOne($dirName, $searchFileName, $searchExt))) { - $result = $this->datasources[0]->update($dirName, $fileName, $extension, $content, $oldFileName, $oldExtension); + $datasource = $this->getFirstDatasource(); + + if (!empty($datasource->selectOne($dirName, $searchFileName, $searchExt))) { + $result = $datasource->update($dirName, $fileName, $extension, $content, $oldFileName, $oldExtension); } else { - $result = $this->datasources[0]->insert($dirName, $fileName, $extension, $content); + $result = $datasource->insert($dirName, $fileName, $extension, $content); } // Refresh the cache @@ -305,7 +343,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface { try { // Delete from only the first datasource - $this->datasources[0]->delete($dirName, $fileName, $extension); + $this->getFirstDatasource()->delete($dirName, $fileName, $extension); } catch (Exception $ex) { // Check to see if this is a valid path to delete diff --git a/modules/cms/classes/Theme.php b/modules/cms/classes/Theme.php index f975c0e72..ab54a35ce 100644 --- a/modules/cms/classes/Theme.php +++ b/modules/cms/classes/Theme.php @@ -16,6 +16,7 @@ use Cms\Models\ThemeData; use System\Models\Parameter; use October\Rain\Halcyon\Datasource\DbDatasource; use October\Rain\Halcyon\Datasource\FileDatasource; +use October\Rain\Halcyon\Datasource\DatasourceInterface; /** * This class represents the CMS theme. @@ -514,14 +515,14 @@ class Theme { $enableDbLayer = Config::get('cms.enableDatabaseLayer', false); if (is_null($enableDbLayer)) { - $enableDbLayer = !Config::get('app.debug'); + $enableDbLayer = !Config::get('app.debug', false); } return $enableDbLayer && App::hasDatabase(); } /** - * Ensures this theme is registered as a Halcyon them datasource. + * Ensures this theme is registered as a Halcyon datasource. * @return void */ public function registerHalyconDatasource() @@ -531,8 +532,8 @@ class Theme if (!$resolver->hasDatasource($this->dirName)) { if (static::databaseLayerEnabled()) { $datasource = new AutoDatasource([ - new DbDatasource($this->dirName, 'cms_theme_contents'), - new FileDatasource($this->getPath(), App::make('files')), + 'database' => new DbDatasource($this->dirName, 'cms_theme_contents'), + 'filesystem' => new FileDatasource($this->getPath(), App::make('files')), ]); } else { $datasource = new FileDatasource($this->getPath(), App::make('files')); @@ -542,6 +543,17 @@ class Theme } } + /** + * Get the theme's datasource + * + * @return DatasourceInterface + */ + public function getDatasource() + { + $resolver = App::make('halcyon'); + return $resolver->datasource($this->getDirName()); + } + /** * Implements the getter functionality. * @param string $name diff --git a/modules/cms/controllers/Index.php b/modules/cms/controllers/Index.php index fc5a8bf7d..059be61aa 100644 --- a/modules/cms/controllers/Index.php +++ b/modules/cms/controllers/Index.php @@ -16,6 +16,7 @@ use Cms\Classes\Router; use Cms\Classes\Layout; use Cms\Classes\Partial; use Cms\Classes\Content; +use Cms\Classes\CmsObject; use Cms\Classes\CmsCompoundObject; use Cms\Classes\ComponentManager; use Cms\Classes\ComponentPartial; @@ -391,7 +392,7 @@ class Index extends Controller /** * Commits the DB changes of a template to the filesystem * - * @return void + * @return array $response */ public function onCommit() { @@ -400,7 +401,9 @@ class Index extends Controller $template = $this->loadTemplate($type, trim(Request::input('templatePath'))); if ($this->canCommitTemplate($template)) { + // @TODO: Implement commit logic + Flash::success(Lang::get('cms::lang.editor.commit_success', ['type' => $type])); } return $this->getUpdateResponse($template, $type); @@ -409,7 +412,7 @@ class Index extends Controller /** * Resets a template to the version on the filesystem * - * @return void + * @return array $response */ public function onReset() { @@ -418,7 +421,9 @@ class Index extends Controller $template = $this->loadTemplate($type, trim(Request::input('templatePath'))); if ($this->canResetTemplate($template)) { + // @TODO: Implement reset logic + Flash::success(Lang::get('cms::lang.editor.reset_success', ['type' => $type])); } return $this->getUpdateResponse($template, $type); @@ -435,7 +440,7 @@ class Index extends Controller * @param string $type The type of template being affected * @return array $result; */ - protected function getUpdateResponse($template, $type) + protected function getUpdateResponse(CmsObject $template, string $type) { $result = [ 'templatePath' => $template->fileName, @@ -458,16 +463,20 @@ class Index extends Controller /** * Check to see if the provided template can be committed + * Only available in debug mode, the DB layer must be enabled, and the template must exist in the database * * @param CmsObject $template * @return boolean */ - protected function canCommitTemplate($template) + protected function canCommitTemplate(CmsObject $template) { - $result = true; // will set to false by default - - if (Theme::databaseLayerEnabled()) { + $result = false; + if (Config::get('app.debug', false) && + Theme::databaseLayerEnabled() && + Theme::getActiveTheme()->getDatasource()->sourceHasModel('database', $template) + ) { + $result = true; } return $result; @@ -475,16 +484,18 @@ class Index extends Controller /** * Check to see if the provided template can be reset + * Only available when the DB layer is enabled and the template exists in both the DB & Filesystem * * @param CmsObject $template * @return boolean */ - protected function canResetTemplate($template) + protected function canResetTemplate(CmsObject $template) { - $result = true; // will set to false by default + $result = false; if (Theme::databaseLayerEnabled()) { - + $datasource = Theme::getActiveTheme()->getDatasource(); + $result = $datasource->sourceHasModel('database', $template) && $datasource->sourceHasModel('filesystem', $template); } return $result; diff --git a/modules/cms/controllers/index/_common_toolbar_actions.htm b/modules/cms/controllers/index/_common_toolbar_actions.htm index 42b247e8e..07aaa19c4 100644 --- a/modules/cms/controllers/index/_common_toolbar_actions.htm +++ b/modules/cms/controllers/index/_common_toolbar_actions.htm @@ -3,6 +3,7 @@ class="btn btn-danger oc-icon-download hide" data-request="onCommit" data-request-confirm="" + data-load-indicator="" data-control="commit-button"> @@ -12,6 +13,7 @@ class="btn btn-danger oc-icon-bomb hide" data-request="onReset" data-request-confirm="" + data-load-indicator="" data-control="reset-button"> diff --git a/modules/cms/lang/en/lang.php b/modules/cms/lang/en/lang.php index 7bf82ba7c..a6096b04d 100644 --- a/modules/cms/lang/en/lang.php +++ b/modules/cms/lang/en/lang.php @@ -186,10 +186,14 @@ return [ 'close_searchbox' => 'Close Search box', 'open_replacebox' => 'Open Replace box', 'close_replacebox' => 'Close Replace box', - 'reset' => 'Reset', 'commit' => 'Commit', - 'reset_confirm' => 'Are you sure you want to reset this file to the copy that is on the filesystem? This will completely replace it with the file that is on the filesystem', + 'reset' => 'Reset', 'commit_confirm' => 'Are you sure you want to commit your changes to this file to the filesystem? This will overwrite the existing file on the filesystem', + 'reset_confirm' => 'Are you sure you want to reset this file to the copy that is on the filesystem? This will completely replace it with the file that is on the filesystem', + 'committing' => 'Committing', + 'resetting' => 'Resetting', + 'commit_success' => 'The :type has been committed to the filesystem', + 'reset_success' => 'The :type has been reset to the filesystem version', ], 'asset' => [ 'menu_label' => 'Assets', From 878bb890b9ece55c6913c9968036fa046f74306b Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Fri, 23 Nov 2018 11:28:34 -0600 Subject: [PATCH 18/29] Passed the buck to the AutoDatasource to implement pushToSource and removeFromSource, added type hinting to AutoDatasource parameters --- modules/cms/classes/AutoDatasource.php | 46 ++++++++++++++++++++------ modules/cms/controllers/Index.php | 23 ++++++++++--- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/modules/cms/classes/AutoDatasource.php b/modules/cms/classes/AutoDatasource.php index 9f734f40b..13e8ce90a 100644 --- a/modules/cms/classes/AutoDatasource.php +++ b/modules/cms/classes/AutoDatasource.php @@ -37,7 +37,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface * @param array $datasources Array of datasources to utilize. Lower indexes = higher priority ['datasourceName' => $datasource] * @return void */ - public function __construct($datasources) + public function __construct(array $datasources) { $this->datasources = $datasources; @@ -100,13 +100,37 @@ class AutoDatasource extends Datasource implements DatasourceInterface return $result; } + /** + * Push the provided model to the specified datasource + * + * @param Model $model The Halcyon Model to push + * @param string $source The string key of the datasource to use + * @return void + */ + public function pushToSource(Model $model, string $source) + { + + } + + /** + * Remove the provided model from the specified datasource + * + * @param Model $model The Halcyon model to remove + * @param string $source The string key of the datasource to use + * @return void + */ + public function removeFromSource(Model $model, string $source) + { + + } + /** * Get the appropriate datasource for the provided path * * @param string $path * @return Datasource */ - protected function getDatasourceForPath($path) + protected function getDatasourceForPath(string $path) { // Default to the last datasource provided $datasourceIndex = count($this->datasources) - 1; @@ -144,7 +168,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface * ]; * @return array $paths ["$dirName/path/1.md", "$dirName/path/2.md"] */ - protected function getValidPaths($dirName, $options = []) + protected function getValidPaths(string $dirName, $options = []) { // Initialize result set $paths = []; @@ -185,7 +209,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface * @param string $extension * @return string */ - protected function makeFilePath($dirName, $fileName, $extension) + protected function makeFilePath(string $dirName, string $fileName, string $extension) { return $dirName . '/' . $fileName . '.' . $extension; } @@ -209,7 +233,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface * @param string $extension * @return mixed */ - public function selectOne($dirName, $fileName, $extension) + public function selectOne(string $dirName, string $fileName, string $extension) { try { $result = $this->getDatasourceForPath($this->makeFilePath($dirName, $fileName, $extension))->selectOne($dirName, $fileName, $extension); @@ -224,7 +248,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface * Returns all templates. * * @param string $dirName - * @param array $options Array of options, [ + * @param array $options Array of options, [ * 'columns' => ['fileName', 'mtime', 'content'], // Only return specific columns * 'extensions' => ['htm', 'md', 'twig'], // Extensions to search for * 'fileMatch' => '*gr[ae]y', // Shell matching pattern to match the filename against using the fnmatch function @@ -234,7 +258,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface * ]; * @return array */ - public function select($dirName, array $options = []) + public function select(string $dirName, $options = []) { // Handle fileName listings through just the cache if (@$options['columns'] === ['fileName']) { @@ -282,7 +306,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface * @param string $content * @return bool */ - public function insert($dirName, $fileName, $extension, $content) + public function insert(string $dirName, string $fileName, string $extension, string $content) { // Insert only on the first datasource $result = $this->getFirstDatasource()->insert($dirName, $fileName, $extension, $content); @@ -304,7 +328,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface * @param string $oldExtension Defaults to null * @return int */ - public function update($dirName, $fileName, $extension, $content, $oldFileName = null, $oldExtension = null) + public function update(string $dirName, string $fileName, string $extension, string $content, $oldFileName = null, $oldExtension = null) { $searchFileName = $oldFileName ?: $fileName; $searchExt = $oldExtension ?: $extension; @@ -339,7 +363,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface * @param string $extension * @return int */ - public function delete($dirName, $fileName, $extension) + public function delete(string $dirName, string $fileName, string $extension) { try { // Delete from only the first datasource @@ -375,7 +399,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface * @param string $extension * @return int */ - public function lastModified($dirName, $fileName, $extension) + public function lastModified(string $dirName, string $fileName, string $extension) { return $this->getDatasourceForPath($this->makeFilePath($dirName, $fileName, $extension))->lastModified($dirName, $fileName, $extension); } diff --git a/modules/cms/controllers/Index.php b/modules/cms/controllers/Index.php index 059be61aa..64dbf4484 100644 --- a/modules/cms/controllers/Index.php +++ b/modules/cms/controllers/Index.php @@ -401,7 +401,10 @@ class Index extends Controller $template = $this->loadTemplate($type, trim(Request::input('templatePath'))); if ($this->canCommitTemplate($template)) { - // @TODO: Implement commit logic + // Populate the filesystem with the template and then remove it from the db + $datasource = $this->getThemeDatasource(); + $datasource->pushToSource($template, 'filesystem'); + $datasource->removeFromSource($template, 'database'); Flash::success(Lang::get('cms::lang.editor.commit_success', ['type' => $type])); } @@ -421,7 +424,9 @@ class Index extends Controller $template = $this->loadTemplate($type, trim(Request::input('templatePath'))); if ($this->canResetTemplate($template)) { - // @TODO: Implement reset logic + // Remove the template from the DB + $datasource = $this->getThemeDatasource(); + $datasource->removeFromSource($template, 'database'); Flash::success(Lang::get('cms::lang.editor.reset_success', ['type' => $type])); } @@ -461,6 +466,16 @@ class Index extends Controller return $result; } + /** + * Get the active theme's datasource + * + * @return \October\Rain\Halcyon\Datasource\DatasourceInterface + */ + protected function getThemeDatasource() + { + return $this->theme->getDatasource(); + } + /** * Check to see if the provided template can be committed * Only available in debug mode, the DB layer must be enabled, and the template must exist in the database @@ -474,7 +489,7 @@ class Index extends Controller if (Config::get('app.debug', false) && Theme::databaseLayerEnabled() && - Theme::getActiveTheme()->getDatasource()->sourceHasModel('database', $template) + $this->getThemeDatasource()->sourceHasModel('database', $template) ) { $result = true; } @@ -494,7 +509,7 @@ class Index extends Controller $result = false; if (Theme::databaseLayerEnabled()) { - $datasource = Theme::getActiveTheme()->getDatasource(); + $datasource = $this->getThemeDatasource(); $result = $datasource->sourceHasModel('database', $template) && $datasource->sourceHasModel('filesystem', $template); } From a4f5e1b96e851b62874b02b04c0c9c4318f9f066 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Fri, 23 Nov 2018 13:35:51 -0600 Subject: [PATCH 19/29] Finished initial implementation of Commit / Reset buttons --- modules/cms/assets/js/october.cmspage.js | 6 ++ modules/cms/classes/AutoDatasource.php | 90 +++++++++++++++++------- modules/cms/controllers/Index.php | 5 +- 3 files changed, 75 insertions(+), 26 deletions(-) diff --git a/modules/cms/assets/js/october.cmspage.js b/modules/cms/assets/js/october.cmspage.js index 9361581b7..0c28f5aa8 100644 --- a/modules/cms/assets/js/october.cmspage.js +++ b/modules/cms/assets/js/october.cmspage.js @@ -281,6 +281,12 @@ CmsPage.prototype.onAjaxSuccess = function(ev, context, data) { var element = ev.target + // Reload the form if the server has requested it + if (data.forceReload) { + this.reloadForm(element) + return; + } + // Update the visibilities of the commit & reset buttons $('[data-control=commit-button]', element).toggleClass('hide', !data.canCommit) $('[data-control=reset-button]', element).toggleClass('hide', !data.canReset) diff --git a/modules/cms/classes/AutoDatasource.php b/modules/cms/classes/AutoDatasource.php index 13e8ce90a..59cb73139 100644 --- a/modules/cms/classes/AutoDatasource.php +++ b/modules/cms/classes/AutoDatasource.php @@ -31,6 +31,11 @@ class AutoDatasource extends Datasource implements DatasourceInterface */ protected $allowCacheRefreshes = true; + /** + * @var string The key for the datasource to perform CRUD operations on + */ + protected $activeDatasourceKey = ''; + /** * Create a new datasource instance. * @@ -41,6 +46,8 @@ class AutoDatasource extends Datasource implements DatasourceInterface { $this->datasources = $datasources; + $this->activeDatasourceKey = array_keys($datasources)[0]; + $this->populateCache(); $this->postProcessor = new Processor; @@ -109,7 +116,23 @@ class AutoDatasource extends Datasource implements DatasourceInterface */ public function pushToSource(Model $model, string $source) { + // Set the active datasource to the provided source and retrieve it + $originalActiveKey = $this->activeDatasourceKey; + $this->activeDatasourceKey = $source; + $datasource = $this->getActiveDatasource(); + // Get the path parts + $dirName = $model->getObjectTypeDirName(); + list($fileName, $extension) = $model->getFileNameParts(); + + // Get the file content + $content = $datasource->getPostProcessor()->processUpdate($model->newQuery(), []); + + // Perform an update on the selected datasource (will insert if it doesn't exist) + $this->update($dirName, $fileName, $extension, $content); + + // Restore the original active datasource + $this->activeDatasourceKey = $originalActiveKey; } /** @@ -121,7 +144,20 @@ class AutoDatasource extends Datasource implements DatasourceInterface */ public function removeFromSource(Model $model, string $source) { + // Set the active datasource to the provided source and retrieve it + $originalActiveKey = $this->activeDatasourceKey; + $this->activeDatasourceKey = $source; + $datasource = $this->getActiveDatasource(); + // Get the path parts + $dirName = $model->getObjectTypeDirName(); + list($fileName, $extension) = $model->getFileNameParts(); + + // Perform a forced delete on the selected datasource to ensure it's removed + $this->forceDelete($dirName, $fileName, $extension); + + // Restore the original active datasource + $this->activeDatasourceKey = $originalActiveKey; } /** @@ -168,7 +204,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface * ]; * @return array $paths ["$dirName/path/1.md", "$dirName/path/2.md"] */ - protected function getValidPaths(string $dirName, $options = []) + protected function getValidPaths(string $dirName, array $options = []) { // Initialize result set $paths = []; @@ -215,14 +251,13 @@ class AutoDatasource extends Datasource implements DatasourceInterface } /** - * Get the first datasource for use with CRUD operations + * Get the datasource for use with CRUD operations * * @return DatasourceInterface */ - protected function getFirstDatasource() + protected function getActiveDatasource() { - $keys = array_keys($this->datasources); - return $this->datasources[$keys[0]]; + return $this->datasources[$this->activeDatasourceKey]; } /** @@ -258,7 +293,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface * ]; * @return array */ - public function select(string $dirName, $options = []) + public function select(string $dirName, array $options = []) { // Handle fileName listings through just the cache if (@$options['columns'] === ['fileName']) { @@ -298,7 +333,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface } /** - * Creates a new template, only inserts to the first datasource + * Creates a new template, only inserts to the active datasource * * @param string $dirName * @param string $fileName @@ -308,8 +343,8 @@ class AutoDatasource extends Datasource implements DatasourceInterface */ public function insert(string $dirName, string $fileName, string $extension, string $content) { - // Insert only on the first datasource - $result = $this->getFirstDatasource()->insert($dirName, $fileName, $extension, $content); + // Insert only on the active datasource + $result = $this->getActiveDatasource()->insert($dirName, $fileName, $extension, $content); // Refresh the cache $this->populateCache(true); @@ -341,7 +376,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface $this->allowCacheRefreshes = true; } - $datasource = $this->getFirstDatasource(); + $datasource = $this->getActiveDatasource(); if (!empty($datasource->selectOne($dirName, $searchFileName, $searchExt))) { $result = $datasource->update($dirName, $fileName, $extension, $content, $oldFileName, $oldExtension); @@ -356,7 +391,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface } /** - * Run a delete statement against the datasource, only runs delete on first datasource + * Run a delete statement against the datasource, only runs delete on active datasource * * @param string $dirName * @param string $fileName @@ -366,24 +401,31 @@ class AutoDatasource extends Datasource implements DatasourceInterface public function delete(string $dirName, string $fileName, string $extension) { try { - // Delete from only the first datasource - $this->getFirstDatasource()->delete($dirName, $fileName, $extension); + // Delete from only the active datasource + if ($this->forceDeleting) { + $this->getActiveDatasource()->forceDelete($dirName, $fileName, $extension); + } else { + $this->getActiveDatasource()->delete($dirName, $fileName, $extension); + } } catch (Exception $ex) { - // Check to see if this is a valid path to delete - $path = $this->makeFilePath($dirName, $fileName, $extension); + // Only attempt to do an insert-delete when not force deleting the record + if (!$this->forceDeleting) { + // Check to see if this is a valid path to delete + $path = $this->makeFilePath($dirName, $fileName, $extension); - if (in_array($path, $this->getValidPaths($dirName))) { - // Retrieve the current record - $record = $this->selectOne($dirName, $fileName, $extension); + if (in_array($path, $this->getValidPaths($dirName))) { + // Retrieve the current record + $record = $this->selectOne($dirName, $fileName, $extension); - // Insert the current record into the first datasource so we can mark it as deleted - $this->insert($dirName, $fileName, $extension, $record['content']); + // Insert the current record into the active datasource so we can mark it as deleted + $this->insert($dirName, $fileName, $extension, $record['content']); - // Perform the deletion on the newly inserted record - $this->delete($dirName, $fileName, $extension); - } else { - throw (new DeleteFileException)->setInvalidPath($path); + // Perform the deletion on the newly inserted record + $this->delete($dirName, $fileName, $extension); + } else { + throw (new DeleteFileException)->setInvalidPath($path); + } } } diff --git a/modules/cms/controllers/Index.php b/modules/cms/controllers/Index.php index 64dbf4484..6f29b0429 100644 --- a/modules/cms/controllers/Index.php +++ b/modules/cms/controllers/Index.php @@ -409,7 +409,7 @@ class Index extends Controller Flash::success(Lang::get('cms::lang.editor.commit_success', ['type' => $type])); } - return $this->getUpdateResponse($template, $type); + return array_merge($this->getUpdateResponse($template, $type), ['forceReload' => true]); } /** @@ -421,6 +421,7 @@ class Index extends Controller { $this->validateRequestTheme(); $type = Request::input('templateType'); + $template = $this->loadTemplate($type, trim(Request::input('templatePath'))); if ($this->canResetTemplate($template)) { @@ -431,7 +432,7 @@ class Index extends Controller Flash::success(Lang::get('cms::lang.editor.reset_success', ['type' => $type])); } - return $this->getUpdateResponse($template, $type); + return array_merge($this->getUpdateResponse($template, $type), ['forceReload' => true]); } // From 80b5c4182591cac1941a54e27ba756256f16fc54 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Fri, 23 Nov 2018 14:41:18 -0600 Subject: [PATCH 20/29] Improve compatibility with RainLab.Pages --- modules/cms/assets/js/october.cmspage.js | 11 +++++----- modules/cms/controllers/Index.php | 1 - .../cms/controllers/index/_button_commit.htm | 9 +++++++++ .../cms/controllers/index/_button_reset.htm | 9 +++++++++ .../index/_common_toolbar_actions.htm | 20 ++----------------- 5 files changed, 25 insertions(+), 25 deletions(-) create mode 100644 modules/cms/controllers/index/_button_commit.htm create mode 100644 modules/cms/controllers/index/_button_reset.htm diff --git a/modules/cms/assets/js/october.cmspage.js b/modules/cms/assets/js/october.cmspage.js index 0c28f5aa8..af1efc8a9 100644 --- a/modules/cms/assets/js/october.cmspage.js +++ b/modules/cms/assets/js/october.cmspage.js @@ -281,12 +281,6 @@ CmsPage.prototype.onAjaxSuccess = function(ev, context, data) { var element = ev.target - // Reload the form if the server has requested it - if (data.forceReload) { - this.reloadForm(element) - return; - } - // Update the visibilities of the commit & reset buttons $('[data-control=commit-button]', element).toggleClass('hide', !data.canCommit) $('[data-control=reset-button]', element).toggleClass('hide', !data.canReset) @@ -325,6 +319,11 @@ if (context.handler == 'onSave' && (!data['X_OCTOBER_ERROR_FIELDS'] && !data['X_OCTOBER_ERROR_MESSAGE'])) { $(element).trigger('unchange.oc.changeMonitor') } + + // Reload the form if the server has requested it + if (data.forceReload) { + this.reloadForm(element) + } } CmsPage.prototype.onAjaxError = function(ev, context, message, data, jqXHR) { diff --git a/modules/cms/controllers/Index.php b/modules/cms/controllers/Index.php index 6f29b0429..82aca30b4 100644 --- a/modules/cms/controllers/Index.php +++ b/modules/cms/controllers/Index.php @@ -421,7 +421,6 @@ class Index extends Controller { $this->validateRequestTheme(); $type = Request::input('templateType'); - $template = $this->loadTemplate($type, trim(Request::input('templatePath'))); if ($this->canResetTemplate($template)) { diff --git a/modules/cms/controllers/index/_button_commit.htm b/modules/cms/controllers/index/_button_commit.htm new file mode 100644 index 000000000..cf08583dd --- /dev/null +++ b/modules/cms/controllers/index/_button_commit.htm @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/modules/cms/controllers/index/_button_reset.htm b/modules/cms/controllers/index/_button_reset.htm new file mode 100644 index 000000000..eac4f53d4 --- /dev/null +++ b/modules/cms/controllers/index/_button_reset.htm @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/modules/cms/controllers/index/_common_toolbar_actions.htm b/modules/cms/controllers/index/_common_toolbar_actions.htm index 07aaa19c4..e26bbf6f8 100644 --- a/modules/cms/controllers/index/_common_toolbar_actions.htm +++ b/modules/cms/controllers/index/_common_toolbar_actions.htm @@ -1,22 +1,6 @@ - +makePartial('button_commit'); ?> - +makePartial('button_reset'); ?> + data-control="delete-button"> + - - - - \ No newline at end of file +makePartial('button_lastmodified'); ?> \ No newline at end of file From 52fe8586e31e3454d7baa346737d822ead29e5e1 Mon Sep 17 00:00:00 2001 From: Farrow Date: Sat, 1 Dec 2018 18:36:40 +0000 Subject: [PATCH 22/29] Make Inspector element available outside of dragscroll contexts Credit to @Farrow. Update to the javascript cmsPage onInspectorShowing event handler to make it check whether the event target has a dragScroll attached to it (which it previously assumed it did). This allows the inspector to be used in other locations on the backend which are outside of a dragscroll without causing a javascript error. --- modules/cms/assets/js/october.cmspage.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/cms/assets/js/october.cmspage.js b/modules/cms/assets/js/october.cmspage.js index af1efc8a9..425a0bd48 100644 --- a/modules/cms/assets/js/october.cmspage.js +++ b/modules/cms/assets/js/october.cmspage.js @@ -386,7 +386,12 @@ } CmsPage.prototype.onInspectorShowing = function(ev, data) { - $(ev.currentTarget).closest('[data-control="toolbar"]').data('oc.dragScroll').goToElement(ev.currentTarget, data.callback) + var $dragScroll = $(ev.currentTarget).closest('[data-control="toolbar"]').data('oc.dragScroll') + if ($dragScroll) { + $dragScroll.goToElement(ev.currentTarget, data.callback) + } else { + data.callback(); + } ev.stopPropagation() } From 5f78fd4df90645e053ad0ec4f37da16d3b74dc80 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Sun, 16 Dec 2018 09:43:33 -0600 Subject: [PATCH 23/29] Initial implementation attempt for theme:sync command --- modules/cms/classes/AutoDatasource.php | 32 ++++-- modules/system/console/ThemeSync.php | 132 +++++++++++++++++++++++-- 2 files changed, 148 insertions(+), 16 deletions(-) diff --git a/modules/cms/classes/AutoDatasource.php b/modules/cms/classes/AutoDatasource.php index 59cb73139..ad41527de 100644 --- a/modules/cms/classes/AutoDatasource.php +++ b/modules/cms/classes/AutoDatasource.php @@ -34,7 +34,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface /** * @var string The key for the datasource to perform CRUD operations on */ - protected $activeDatasourceKey = ''; + public $activeDatasourceKey = ''; /** * Create a new datasource instance. @@ -87,11 +87,9 @@ class AutoDatasource extends Datasource implements DatasourceInterface { $result = false; - $keys = array_keys($this->datasources); - if (in_array($source, $keys)) { - // Get the datasource's cache index key - $cacheIndex = array_search($source, $keys); + $sourcePaths = $this->getSourcePaths($source); + if (!empty($paths)) { // Generate the path list($name, $extension) = $model->getFileNameParts(); $path = $this->makeFilePath($model->getObjectTypeDirName(), $name, $extension); @@ -99,7 +97,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface // Deleted paths are included as being handled by a datasource // The functionality built on this will need to make sure they // include deleted records when actually performing sycning actions - if (isset($this->pathCache[$cacheIndex][$path])) { + if (isset($sourcePaths[$path])) { $result = true; } } @@ -107,6 +105,28 @@ class AutoDatasource extends Datasource implements DatasourceInterface return $result; } + /** + * Get the available paths for the specified datasource key + * + * @param string $source The string key of the datasource to check + * @return void + */ + public function getSourcePaths(string $source) + { + $result = []; + + $keys = array_keys($this->datasources); + if (in_array($source, $keys)) { + // Get the datasource's cache index key + $cacheIndex = array_search($source, $keys); + + // Return the available paths + $result = $this->pathCache[$cacheIndex]; + } + + return $result; + } + /** * Push the provided model to the specified datasource * diff --git a/modules/system/console/ThemeSync.php b/modules/system/console/ThemeSync.php index 9c009ad02..39630a6bb 100644 --- a/modules/system/console/ThemeSync.php +++ b/modules/system/console/ThemeSync.php @@ -1,10 +1,17 @@ error("cms.enableDatabaseLayer is not enabled, enable it first and try again."); } @@ -69,6 +92,7 @@ class ThemeSync extends Command if (!$themeExists) { return $this->error(sprintf('The theme %s does not exist.', $themeName)); } + $theme = Theme::load($themeName); // Get the target and source datasources $availableSources = ['filesystem', 'database']; @@ -80,28 +104,116 @@ class ThemeSync extends Command if (!in_array($target, $availableSources)) { $this->error(sprintf("Provided --target of %s is invalid. Allowed: database, filesystem"), $target); } + $this->source = $source; + $this->target = $target; // Get the paths + // @TODO: Use the model classes to call listInTheme instead to get all handled paths instead of the other way round + // @TODO: Will probably have to interact directly with the datasources to do the syncing, not sure how much AutoDatasource will be useful here $paths = $this->option('paths') ?: null; if ($paths) { $paths = array_map('trim', explode(',', $paths)); + } else { + $paths = $theme->getDatasource()->getSourcePaths($source); + + // Get the Halcyon model classes to use when filtering the paths to be synced + $validModels = []; + $modelClasses = [ + Meta::class, + Page::class, + Layout::class, + Content::class, + Partial::class, + ]; + /** + * @event system.console.theme.sync.getModelClasses + * Get the Halcyon model classes to use when filtering the paths to be synced + * + * Example usage: + * + * Event::listen('system.console.theme.sync.getModelClasses', function () { + * return [ + * Meta::class, + * Page::class, + * Layout::class, + * Content::class, + * Partial::class, + * ]; + * }); + * + */ + $results = Event::fire('system.console.theme.sync.getAvailableModelClasses'); + foreach ($results as $result) { + $modelClasses += $result; + } + foreach ($modelClasses as $class) { + $validModels[] = new $class; + } + + foreach ($paths as $path => $exists) { + foreach ($validModels as $model) { + if (starts_with($path, $model->getObjectTypeDirName() . '/') && + in_array(pathinfo($path, PATHINFO_EXTENSION), $model->getAllowedExtensions()) + ) { + // Skip to the next path + continue 2; + } + } + + // If we've made it here, this path doesn't get to proceed + unset($paths[$path]); + } + unset($validModels); } // Confirm with the user - if (!$this->confirmToProceed(sprintf('This will REPLACE the provided paths from "themes/%s" on the %s with content from the %s', $themeName, $target, $source), function () { return true; })) { + if (!$this->confirmToProceed(sprintf('This will OVERWRITE the %s provided paths in "themes/%s" on the %s with content from the %s', count($paths), $themeName, $target, $source), function () { return true; })) { return; } try { - // TODO: Actually implement the functionality + $this->info('Syncing files, please wait...'); + $progress = $this->output->createProgressBar(count($paths)); - $this->info(sprintf('The theme %s has been synced from the %s to the %s.', $themeName, $source, $target)); + $this->datasource = $theme->getDatasource(); + + + foreach ($paths as $path) { + // $this->datasource->pushToSource($this->getModelForPath($path), $target); + $progress->advance(); + } + + $progress->finish(); + $this->info(''); + $this->info(sprintf('The theme %s has been successfully synced from the %s to the %s.', $themeName, $source, $target)); } catch (Exception $ex) { $this->error($ex->getMessage()); } } + + /** + * Get the correct Halcyon model for the provided path from the source datasource + * + * @param string $path + * @return void + */ + protected function getModelForPath(string $path) + { + $originalSource = $this->datasource->activeDatasourceKey; + $this->datasource->activeDatasourceKey = $this->source; + + // $model::load($this->theme, $fileName); + + $this->datasource->activeDatasourceKey = $originalSource; + + // return $model; + } + + + + /** * Get the console command arguments. * @return array From ab6023f3e9869bb25d80ba0d97ba2ea889a626f5 Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Thu, 11 Apr 2019 23:36:13 -0600 Subject: [PATCH 24/29] Fixed typo Credit to @bennothommo Co-Authored-By: LukeTowers --- modules/cms/classes/AutoDatasource.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/cms/classes/AutoDatasource.php b/modules/cms/classes/AutoDatasource.php index ad41527de..20c57bc59 100644 --- a/modules/cms/classes/AutoDatasource.php +++ b/modules/cms/classes/AutoDatasource.php @@ -89,7 +89,7 @@ class AutoDatasource extends Datasource implements DatasourceInterface $sourcePaths = $this->getSourcePaths($source); - if (!empty($paths)) { + if (!empty($sourcePaths)) { // Generate the path list($name, $extension) = $model->getFileNameParts(); $path = $this->makeFilePath($model->getObjectTypeDirName(), $name, $extension); @@ -507,4 +507,4 @@ class AutoDatasource extends Datasource implements DatasourceInterface } return $paths; } -} \ No newline at end of file +} From 28ac50ab28cbec71d6cbad3e176e212372f38992 Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Fri, 12 Apr 2019 00:13:39 -0600 Subject: [PATCH 25/29] Fix for models that don't yet exist Credit to @bennothommo. Fixes https://github.com/octobercms/october/pull/3908#issuecomment-447291101 Co-Authored-By: LukeTowers --- modules/cms/classes/AutoDatasource.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/cms/classes/AutoDatasource.php b/modules/cms/classes/AutoDatasource.php index 20c57bc59..dca84da58 100644 --- a/modules/cms/classes/AutoDatasource.php +++ b/modules/cms/classes/AutoDatasource.php @@ -85,6 +85,10 @@ class AutoDatasource extends Datasource implements DatasourceInterface */ public function sourceHasModel(string $source, Model $model) { + if (!$model->exists) { + return false; + } + $result = false; $sourcePaths = $this->getSourcePaths($source); From 7b2a772525c66fa960a756cb076dbf80ad1e2c58 Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Fri, 19 Apr 2019 23:07:48 +0800 Subject: [PATCH 26/29] Finish theme:sync command (#4276) Credit to @bennothommo --- modules/cms/ServiceProvider.php | 17 +++ modules/system/console/ThemeSync.php | 169 ++++++++++++++++----------- 2 files changed, 115 insertions(+), 71 deletions(-) diff --git a/modules/cms/ServiceProvider.php b/modules/cms/ServiceProvider.php index db411be3c..40e170787 100644 --- a/modules/cms/ServiceProvider.php +++ b/modules/cms/ServiceProvider.php @@ -29,6 +29,7 @@ class ServiceProvider extends ModuleServiceProvider $this->registerComponents(); $this->registerThemeLogging(); $this->registerCombinerEvents(); + $this->registerHalcyonModels(); /* * Backend specific @@ -324,4 +325,20 @@ class ServiceProvider extends ModuleServiceProvider } }); } + + /** + * Registers the models to be made available to the theme database layer + */ + protected function registerHalcyonModels() + { + Event::listen('system.console.theme.sync.getAvailableModelClasses', function () { + return [ + Classes\Meta::class, + Classes\Page::class, + Classes\Layout::class, + Classes\Content::class, + Classes\Partial::class + ]; + }); + } } diff --git a/modules/system/console/ThemeSync.php b/modules/system/console/ThemeSync.php index 39630a6bb..9aee5c019 100644 --- a/modules/system/console/ThemeSync.php +++ b/modules/system/console/ThemeSync.php @@ -6,12 +6,6 @@ use Exception; use Cms\Classes\Theme; use Cms\Classes\ThemeManager; -use Cms\Classes\Meta; -use Cms\Classes\Page; -use Cms\Classes\Layout; -use Cms\Classes\Content; -use Cms\Classes\Partial; - use Illuminate\Console\Command; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; @@ -102,86 +96,116 @@ class ThemeSync extends Command $source = 'database'; } if (!in_array($target, $availableSources)) { - $this->error(sprintf("Provided --target of %s is invalid. Allowed: database, filesystem"), $target); + return $this->error(sprintf("Provided --target of %s is invalid. Allowed: database, filesystem", $target)); } $this->source = $source; $this->target = $target; - // Get the paths - // @TODO: Use the model classes to call listInTheme instead to get all handled paths instead of the other way round - // @TODO: Will probably have to interact directly with the datasources to do the syncing, not sure how much AutoDatasource will be useful here - $paths = $this->option('paths') ?: null; - if ($paths) { - $paths = array_map('trim', explode(',', $paths)); + // Get the theme paths, taking into account if the user has specified paths + $userPaths = $this->option('paths') ?: null; + $themePaths = array_keys($theme->getDatasource()->getSourcePaths($source)); + + if (!isset($userPaths)) { + $paths = $themePaths; } else { - $paths = $theme->getDatasource()->getSourcePaths($source); + $paths = []; + $userPaths = array_map('trim', explode(',', $userPaths)); - // Get the Halcyon model classes to use when filtering the paths to be synced - $validModels = []; - $modelClasses = [ - Meta::class, - Page::class, - Layout::class, - Content::class, - Partial::class, - ]; - /** - * @event system.console.theme.sync.getModelClasses - * Get the Halcyon model classes to use when filtering the paths to be synced - * - * Example usage: - * - * Event::listen('system.console.theme.sync.getModelClasses', function () { - * return [ - * Meta::class, - * Page::class, - * Layout::class, - * Content::class, - * Partial::class, - * ]; - * }); - * - */ - $results = Event::fire('system.console.theme.sync.getAvailableModelClasses'); - foreach ($results as $result) { - $modelClasses += $result; - } - foreach ($modelClasses as $class) { - $validModels[] = new $class; - } + foreach ($userPaths as $userPath) { + foreach ($themePaths as $themePath) { + $pregMatch = '/' . str_replace('/', '\/', $userPath) . '/i'; - foreach ($paths as $path => $exists) { - foreach ($validModels as $model) { - if (starts_with($path, $model->getObjectTypeDirName() . '/') && - in_array(pathinfo($path, PATHINFO_EXTENSION), $model->getAllowedExtensions()) - ) { - // Skip to the next path - continue 2; + if ($userPath === $themePath || preg_match($pregMatch, $themePath)) { + $paths[] = $themePath; } } - - // If we've made it here, this path doesn't get to proceed - unset($paths[$path]); } - unset($validModels); + } + unset($userPaths); + unset($themePaths); + + // Determine valid paths based on the models made available for syncing + $validPaths = []; + + /** + * @event system.console.theme.sync.getAvailableModelClasses + * Defines the Halcyon models to be made available to the `theme:sync` tool. + * + * Example usage: + * + * Event::listen('system.console.theme.sync.getAvailableModelClasses', function () { + * return [ + * Meta::class, + * Page::class, + * Layout::class, + * Content::class, + * Partial::class, + * ]; + * }); + * + */ + $results = Event::fire('system.console.theme.sync.getAvailableModelClasses'); + $validModels = []; + + foreach ($results as $result) { + if (!is_iterable($result)) { + continue; + } + + foreach ($result as $model) { + $class = new $model; + + if ($class instanceof \October\Rain\Halcyon\Model) { + $validModels[] = $class; + } + } + unset($class); + } + unset($results); + + // Check each path and map it to a corresponding model + foreach ($paths as $path) { + foreach ($validModels as $model) { + if ( + starts_with($path, $model->getObjectTypeDirName() . '/') + && in_array(pathinfo($path, PATHINFO_EXTENSION), $model->getAllowedExtensions()) + && file_exists($theme->getPath($theme->getDirName()) . '/' . $path) + ) { + $validPaths[$path] = get_class($model); + + // Skip to the next path + continue 2; + } + } + } + unset($paths); + unset($validModels); + + if (count($validPaths) === 0) { + return $this->error(sprintf('No applicable paths found for %s.', $source)); } // Confirm with the user - if (!$this->confirmToProceed(sprintf('This will OVERWRITE the %s provided paths in "themes/%s" on the %s with content from the %s', count($paths), $themeName, $target, $source), function () { return true; })) { + if (!$this->confirmToProceed(sprintf('This will OVERWRITE the %s provided paths in "themes/%s" on the %s with content from the %s', count($validPaths), $themeName, $target, $source), function () { return true; })) { return; } try { $this->info('Syncing files, please wait...'); - $progress = $this->output->createProgressBar(count($paths)); + $progress = $this->output->createProgressBar(count($validPaths)); $this->datasource = $theme->getDatasource(); + foreach ($validPaths as $path => $model) { + $entity = $this->getModelForPath($path, $model, $theme); + if (!isset($entity)) { + continue; + } - foreach ($paths as $path) { - // $this->datasource->pushToSource($this->getModelForPath($path), $target); + $this->datasource->pushToSource($entity, $target); $progress->advance(); } + unset($validPaths); $progress->finish(); $this->info(''); @@ -194,26 +218,29 @@ class ThemeSync extends Command /** - * Get the correct Halcyon model for the provided path from the source datasource + * Get the correct Halcyon model for the provided path from the source datasource and load the requested path data. * * @param string $path - * @return void + * @param string $model + * @param \Cms\Classes\Theme $theme + * @return \October\Rain\Halycon\Model */ - protected function getModelForPath(string $path) + protected function getModelForPath(string $path, string $model, \Cms\Classes\Theme $theme) { $originalSource = $this->datasource->activeDatasourceKey; $this->datasource->activeDatasourceKey = $this->source; - // $model::load($this->theme, $fileName); + $class = new $model; + $entity = $model::load($theme, str_replace($class->getObjectTypeDirName() . '/', '', $path)); + if (!isset($entity)) { + return null; + } $this->datasource->activeDatasourceKey = $originalSource; - // return $model; + return $entity; } - - - /** * Get the console command arguments. * @return array @@ -232,7 +259,7 @@ class ThemeSync extends Command protected function getOptions() { return [ - ['paths', null, InputOption::VALUE_REQUIRED, 'Comma-separated specific paths (relative to provided theme directory) to specificaly sync. Default is all paths'], + ['paths', null, InputOption::VALUE_REQUIRED, 'Comma-separated specific paths (relative to provided theme directory) to specificaly sync. Default is all paths. You may use regular expressions.'], ['target', null, InputOption::VALUE_REQUIRED, 'The target of the sync, the other will be used as the source. Defaults to "database", can be "filesystem"'], ['force', null, InputOption::VALUE_NONE, 'Force the operation to run.'], ]; From 17cea816d83a1f8df04601f69137e17658df0e03 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Sat, 1 Jun 2019 12:40:17 +1000 Subject: [PATCH 27/29] enableDatabaseLayer -> databaseTemplates --- config/cms.php | 60 ++++++++++++++-------------- modules/cms/classes/Theme.php | 2 +- modules/system/console/ThemeSync.php | 2 +- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/config/cms.php b/config/cms.php index 49e649c27..7d176c68f 100644 --- a/config/cms.php +++ b/config/cms.php @@ -56,7 +56,7 @@ return [ | Back-end login remember |-------------------------------------------------------------------------- | - | Define live duration of backend sessions : + | Define live duration of backend sessions: | | true - session never expire (cookie expiration in 5 years) | @@ -211,6 +211,32 @@ return [ 'enableAssetDeepHashing' => null, + /* + |-------------------------------------------------------------------------- + | Database-driven Themes + |-------------------------------------------------------------------------- + | + | Stores theme templates in the database instead of the filesystem. + | + | false - All theme templates are sourced from the filesystem. + | + | true - Source theme templates from the database with fallback to the filesytem. + | + | null - Setting equal to the inverse of app.debug: debug enabled, this disabled. + | + | The database layer stores all modified CMS files in the database. Files that are + | not modified continue to be loaded from the filesystem. The `theme:sync $themeDir` + | console command is available to populate the database from the filesystem with + | the `--toFile` flag to sync in the other direction (database to filesystem) and + | the `--paths="/path/to/file.md,/path/to/file2.md" flag to sync only specific files. + | + | Files modified in the database are cached to indicate that they should be loaded + | from the database. + | + */ + + 'databaseTemplates' => false, + /* |-------------------------------------------------------------------------- | Public plugins path @@ -346,20 +372,20 @@ return [ */ 'forceBytecodeInvalidation' => true, - + /* |-------------------------------------------------------------------------- | Twig Strict Variables |-------------------------------------------------------------------------- | - | If strict_variables is disabled, Twig will silently ignore invalid + | If strict_variables is disabled, Twig will silently ignore invalid | variables (variables and or attributes/methods that do not exist) and | replace them with a null value. When enabled, Twig throws an exception | instead. If set to null, it is enabled when debug mode (app.debug) is | enabled. | */ - + 'enableTwigStrictVariables' => false, /* @@ -381,30 +407,4 @@ return [ 'restrictBaseDir' => true, - /* - |-------------------------------------------------------------------------- - | CMS Database Layer - |-------------------------------------------------------------------------- - | - | Enables the database layer for the CMS content files. - | - | Allowed values: - | - false: Database layer is disabled, the FileDatasource is used - | - true: Database layer is enabled, the AutoDatasource is used - | - null: Setting equal to the inverse of app.debug: debug enabled, this disabled - | - | The database layer stores all modified CMS files in the database. - | Files that are not modified continue to be loaded from the filesystem. - | The `theme:sync $themeDir` console command is available to populate the - | database from the filesystem with the `--toFile` flag to sync in the - | other direction (database to filesystem) and the `--paths="/path/to/file.md,/path/to/file2.md" - | flag to sync only specific files. - | - | Files modified in the database are cached to indicate that they should - | be loaded from the database. - | - */ - - 'enableDatabaseLayer' => false, - ]; diff --git a/modules/cms/classes/Theme.php b/modules/cms/classes/Theme.php index ab54a35ce..2b48e10e5 100644 --- a/modules/cms/classes/Theme.php +++ b/modules/cms/classes/Theme.php @@ -513,7 +513,7 @@ class Theme */ public static function databaseLayerEnabled() { - $enableDbLayer = Config::get('cms.enableDatabaseLayer', false); + $enableDbLayer = Config::get('cms.databaseTemplates', false); if (is_null($enableDbLayer)) { $enableDbLayer = !Config::get('app.debug', false); } diff --git a/modules/system/console/ThemeSync.php b/modules/system/console/ThemeSync.php index 9aee5c019..ca950a76a 100644 --- a/modules/system/console/ThemeSync.php +++ b/modules/system/console/ThemeSync.php @@ -72,7 +72,7 @@ class ThemeSync extends Command // Check to see if the DB layer is enabled if (!Theme::databaseLayerEnabled()) { - return $this->error("cms.enableDatabaseLayer is not enabled, enable it first and try again."); + return $this->error("cms.databaseTemplates is not enabled, enable it first and try again."); } // Check to see if the provided theme exists From 8c398e7ad51684b862063ba32e491e4a8df02458 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Sat, 1 Jun 2019 12:45:29 +1000 Subject: [PATCH 28/29] cms_theme_contents -> cms_theme_templates --- modules/cms/classes/Theme.php | 2 +- ...nts.php => 2018_11_01_000001_Db_Cms_Theme_Templates.php} | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename modules/cms/database/migrations/{2018_11_01_000001_Db_Cms_Theme_Contents.php => 2018_11_01_000001_Db_Cms_Theme_Templates.php} (76%) diff --git a/modules/cms/classes/Theme.php b/modules/cms/classes/Theme.php index 2b48e10e5..1b675ef43 100644 --- a/modules/cms/classes/Theme.php +++ b/modules/cms/classes/Theme.php @@ -532,7 +532,7 @@ class Theme if (!$resolver->hasDatasource($this->dirName)) { if (static::databaseLayerEnabled()) { $datasource = new AutoDatasource([ - 'database' => new DbDatasource($this->dirName, 'cms_theme_contents'), + 'database' => new DbDatasource($this->dirName, 'cms_theme_templates'), 'filesystem' => new FileDatasource($this->getPath(), App::make('files')), ]); } else { diff --git a/modules/cms/database/migrations/2018_11_01_000001_Db_Cms_Theme_Contents.php b/modules/cms/database/migrations/2018_11_01_000001_Db_Cms_Theme_Templates.php similarity index 76% rename from modules/cms/database/migrations/2018_11_01_000001_Db_Cms_Theme_Contents.php rename to modules/cms/database/migrations/2018_11_01_000001_Db_Cms_Theme_Templates.php index 0ddb5631f..f81a4ba79 100644 --- a/modules/cms/database/migrations/2018_11_01_000001_Db_Cms_Theme_Contents.php +++ b/modules/cms/database/migrations/2018_11_01_000001_Db_Cms_Theme_Templates.php @@ -3,11 +3,11 @@ use October\Rain\Database\Schema\Blueprint; use October\Rain\Database\Updates\Migration; -class DbCmsThemeContents extends Migration +class DbCmsThemeTemplates extends Migration { public function up() { - Schema::create('cms_theme_contents', function (Blueprint $table) { + Schema::create('cms_theme_templates', function (Blueprint $table) { $table->engine = 'InnoDB'; $table->increments('id'); $table->string('source')->index(); @@ -21,6 +21,6 @@ class DbCmsThemeContents extends Migration public function down() { - Schema::dropIfExists('cms_theme_contents'); + Schema::dropIfExists('cms_theme_templates'); } } From 6f021d5f20092bbf11edf5dc74037a0479460fed Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Sat, 1 Jun 2019 13:42:29 +1000 Subject: [PATCH 29/29] Refactor ThemeSync console command --- modules/system/console/ThemeSync.php | 43 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/modules/system/console/ThemeSync.php b/modules/system/console/ThemeSync.php index ca950a76a..35f466614 100644 --- a/modules/system/console/ThemeSync.php +++ b/modules/system/console/ThemeSync.php @@ -17,7 +17,7 @@ use Symfony\Component\Console\Input\InputArgument; * * - name defaults to the currently active theme * - --paths defaults to all paths within the theme, otherwise comma-separated list of paths relative to the theme directory - * - --target defaults to "database", the source will whichever of filesystem vs database is not the target + * - --target defaults to "filesystem", the source will whichever of filesystem vs database is not the target * - --force bypasses the confirmation request * * @package october\system @@ -90,13 +90,13 @@ class ThemeSync extends Command // Get the target and source datasources $availableSources = ['filesystem', 'database']; - $target = $this->option('target') ?: 'database'; + $target = $this->option('target') ?: 'filesystem'; $source = 'filesystem'; if ($target === 'filesystem') { $source = 'database'; } if (!in_array($target, $availableSources)) { - return $this->error(sprintf("Provided --target of %s is invalid. Allowed: database, filesystem", $target)); + return $this->error(sprintf("Provided --target of %s is invalid. Allowed: filesystem, database", $target)); } $this->source = $source; $this->target = $target; @@ -107,7 +107,8 @@ class ThemeSync extends Command if (!isset($userPaths)) { $paths = $themePaths; - } else { + } + else { $paths = []; $userPaths = array_map('trim', explode(',', $userPaths)); @@ -121,8 +122,6 @@ class ThemeSync extends Command } } } - unset($userPaths); - unset($themePaths); // Determine valid paths based on the models made available for syncing $validPaths = []; @@ -144,24 +143,22 @@ class ThemeSync extends Command * }); * */ - $results = Event::fire('system.console.theme.sync.getAvailableModelClasses'); + $eventResults = Event::fire('system.console.theme.sync.getAvailableModelClasses'); $validModels = []; - foreach ($results as $result) { - if (!is_iterable($result)) { + foreach ($eventResults as $result) { + if (!is_array($result)) { continue; } - foreach ($result as $model) { - $class = new $model; + foreach ($result as $modelClass) { + $modelObj = new $modelClass; - if ($class instanceof \October\Rain\Halcyon\Model) { - $validModels[] = $class; + if ($modelObj instanceof \October\Rain\Halcyon\Model) { + $validModels[] = $modelObj; } } - unset($class); } - unset($results); // Check each path and map it to a corresponding model foreach ($paths as $path) { @@ -178,8 +175,6 @@ class ThemeSync extends Command } } } - unset($paths); - unset($validModels); if (count($validPaths) === 0) { return $this->error(sprintf('No applicable paths found for %s.', $source)); @@ -205,7 +200,6 @@ class ThemeSync extends Command $this->datasource->pushToSource($entity, $target); $progress->advance(); } - unset($validPaths); $progress->finish(); $this->info(''); @@ -225,13 +219,18 @@ class ThemeSync extends Command * @param \Cms\Classes\Theme $theme * @return \October\Rain\Halycon\Model */ - protected function getModelForPath(string $path, string $model, \Cms\Classes\Theme $theme) + protected function getModelForPath($path, $modelClass, $theme) { $originalSource = $this->datasource->activeDatasourceKey; $this->datasource->activeDatasourceKey = $this->source; - $class = new $model; - $entity = $model::load($theme, str_replace($class->getObjectTypeDirName() . '/', '', $path)); + $modelObj = new $modelClass; + + $entity = $modelClass::load( + $theme, + str_replace($modelObj->getObjectTypeDirName() . '/', '', $path) + ); + if (!isset($entity)) { return null; } @@ -260,7 +259,7 @@ class ThemeSync extends Command { return [ ['paths', null, InputOption::VALUE_REQUIRED, 'Comma-separated specific paths (relative to provided theme directory) to specificaly sync. Default is all paths. You may use regular expressions.'], - ['target', null, InputOption::VALUE_REQUIRED, 'The target of the sync, the other will be used as the source. Defaults to "database", can be "filesystem"'], + ['target', null, InputOption::VALUE_REQUIRED, 'The target of the sync, the other will be used as the source. Defaults to "filesystem", can be "database"'], ['force', null, InputOption::VALUE_NONE, 'Force the operation to run.'], ]; }