From 77d3ab8b6702b171e6ff793b2f11a0c27f7f5897 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Mon, 16 Mar 2015 19:00:39 +1100 Subject: [PATCH] Finish basic theme utilities: properties, import/export, duplicate, delete --- modules/backend/classes/Controller.php | 5 +- .../cms/assets/css/october.theme-selector.css | 5 + .../assets/less/october.theme-selector.less | 5 + modules/cms/classes/Theme.php | 2 +- modules/cms/classes/theme/fields.yaml | 10 +- modules/cms/controllers/Themes.php | 222 +++++++++++++++++- .../controllers/themes/_theme_create_form.htm | 54 +++++ .../themes/_theme_duplicate_form.htm | 75 ++++++ .../controllers/themes/_theme_export_form.htm | 63 +++++ .../controllers/themes/_theme_fields_form.htm | 2 +- .../controllers/themes/_theme_import_form.htm | 57 +++++ .../cms/controllers/themes/_theme_list.htm | 9 + .../controllers/themes/_theme_list_item.htm | 33 ++- modules/cms/controllers/themes/download.htm | 13 + modules/cms/models/ThemeExport.php | 141 +++++++++++ modules/cms/models/ThemeImport.php | 139 +++++++++++ modules/cms/models/themeexport/fields.yaml | 14 ++ modules/cms/models/themeimport/fields.yaml | 25 ++ 18 files changed, 855 insertions(+), 19 deletions(-) create mode 100644 modules/cms/controllers/themes/_theme_create_form.htm create mode 100644 modules/cms/controllers/themes/_theme_duplicate_form.htm create mode 100644 modules/cms/controllers/themes/_theme_export_form.htm create mode 100644 modules/cms/controllers/themes/_theme_import_form.htm create mode 100644 modules/cms/controllers/themes/download.htm create mode 100644 modules/cms/models/ThemeExport.php create mode 100644 modules/cms/models/ThemeImport.php create mode 100644 modules/cms/models/themeexport/fields.yaml create mode 100644 modules/cms/models/themeimport/fields.yaml diff --git a/modules/backend/classes/Controller.php b/modules/backend/classes/Controller.php index 3fba8270a..7638d61a8 100644 --- a/modules/backend/classes/Controller.php +++ b/modules/backend/classes/Controller.php @@ -317,7 +317,8 @@ class Controller extends Extendable // Execute the action $result = call_user_func_array([$this, $actionName], $parameters); - if ($result instanceof RedirectResponse) { + // Expecting \Response and \RedirectResponse + if ($result instanceof \Symfony\Component\HttpFoundation\Response) { return $result; } @@ -402,10 +403,10 @@ class Controller extends Extendable */ if ($result instanceof RedirectResponse) { $responseContents['X_OCTOBER_REDIRECT'] = $result->getTargetUrl(); + } /* * No redirect is used, look for any flash messages */ - } elseif (Flash::check()) { $responseContents['#layout-flash-messages'] = $this->makeLayoutPartial('flash_messages'); } diff --git a/modules/cms/assets/css/october.theme-selector.css b/modules/cms/assets/css/october.theme-selector.css index 6cf246c36..0b2f99cbb 100644 --- a/modules/cms/assets/css/october.theme-selector.css +++ b/modules/cms/assets/css/october.theme-selector.css @@ -95,6 +95,10 @@ .theme-selector-layout .layout-row.links .theme-description { border-bottom: 1px solid #f2f3f4; } +.theme-selector-layout .create-new-theme { + margin-bottom: 10px; +} +.theme-selector-layout .create-new-theme, .theme-selector-layout .find-more-themes { background: #ecf0f1; color: #2b3e50; @@ -105,6 +109,7 @@ -moz-border-radius: 4px; border-radius: 4px; } +.theme-selector-layout .create-new-theme:hover, .theme-selector-layout .find-more-themes:hover { background: #1795f1; color: white; diff --git a/modules/cms/assets/less/october.theme-selector.less b/modules/cms/assets/less/october.theme-selector.less index ee4bc042b..7e0a6cb23 100644 --- a/modules/cms/assets/less/october.theme-selector.less +++ b/modules/cms/assets/less/october.theme-selector.less @@ -112,6 +112,11 @@ } } + .create-new-theme { + margin-bottom: 10px; + } + + .create-new-theme, .find-more-themes { background: #ecf0f1; color: #2b3e50; diff --git a/modules/cms/classes/Theme.php b/modules/cms/classes/Theme.php index b66c65cc5..01bdc3780 100644 --- a/modules/cms/classes/Theme.php +++ b/modules/cms/classes/Theme.php @@ -287,7 +287,7 @@ class Theme public function writeConfig($values = [], $overwrite = false) { if (!$overwrite) { - $values = $values + $this->getConfig(); + $values = $values + (array) $this->getConfig(); } $path = $this->getPath().'/theme.yaml'; diff --git a/modules/cms/classes/theme/fields.yaml b/modules/cms/classes/theme/fields.yaml index 408ee75fd..cd79ba0a2 100644 --- a/modules/cms/classes/theme/fields.yaml +++ b/modules/cms/classes/theme/fields.yaml @@ -10,10 +10,18 @@ tabs: label: Name placeholder: New theme name span: auto + required: true attributes: default-focus: 1 - directory_name: + dir_name@create: + label: Directory name + span: auto + placeholder: The destination theme directory + preset: name + required: true + + dir_name@update: label: Directory name disabled: true span: auto diff --git a/modules/cms/controllers/Themes.php b/modules/cms/controllers/Themes.php index 7baf9f946..92978349e 100644 --- a/modules/cms/controllers/Themes.php +++ b/modules/cms/controllers/Themes.php @@ -1,15 +1,20 @@ pageTitle = 'cms::lang.theme.settings_menu'; BackendMenu::setContext('October.System', 'system', 'settings'); SettingsManager::setContext('October.Cms', 'theme'); + + /* + * Enable AJAX for Form widgets + */ + if (post('mode') == 'import') { + $this->makeImportFormWidget($this->findThemeObject())->bindToController(); + } } public function index() @@ -57,41 +69,153 @@ class Themes extends Controller ]; } + public function index_onDelete() + { + $theme = $this->findThemeObject(); + + if ($theme->isActiveTheme()) { + throw new ApplicationException('Cannot delete the active theme, try making another theme active first.'); + } + + $themePath = $theme->getPath(); + if (File::isDirectory($themePath)) { + File::deleteDirectory($themePath); + } + + Flash::success('Deleted theme successfully!'); + return Redirect::refresh(); + } + // // Theme properties // - public function index_onLoadThemeFieldsForm() + public function index_onLoadFieldsForm() { $theme = $this->findThemeObject(); - $this->vars['widget'] = $this->makeThemeFieldsFormWidget($theme); + $this->vars['widget'] = $this->makeFieldsFormWidget($theme); $this->vars['themeDir'] = $theme->getDirName(); return $this->makePartial('theme_fields_form'); } - public function index_onSaveThemeFields() + public function index_onSaveFields() { $theme = $this->findThemeObject(); - $widget = $this->makeThemeFieldsFormWidget($theme); + $widget = $this->makeFieldsFormWidget($theme); $theme->writeConfig($widget->getSaveData()); return ['#themeListItem-'.$theme->getId() => $this->makePartial('theme_list_item', ['theme' => $theme])]; } - protected function makeThemeFieldsFormWidget($theme) + protected function makeFieldsFormWidget($theme) { $widgetConfig = $this->makeConfig('~/modules/cms/classes/theme/fields.yaml'); $widgetConfig->alias = 'form'.studly_case($theme->getDirName()); $widgetConfig->model = $theme; $widgetConfig->data = $theme->getConfig(); - $widgetConfig->data['directory_name'] = $theme->getDirName(); + $widgetConfig->data['dir_name'] = $theme->getDirName(); $widgetConfig->arrayName = 'Theme'; + $widgetConfig->context = 'update'; $widget = $this->makeWidget('Backend\Widgets\Form', $widgetConfig); return $widget; } + // + // Create theme + // + + public function index_onLoadCreateForm() + { + $this->vars['widget'] = $this->makeCreateFormWidget(); + return $this->makePartial('theme_create_form'); + } + + public function index_onCreate() + { + $widget = $this->makeCreateFormWidget(); + $data = $widget->getSaveData(); + $newDirName = trim(array_get($data, 'dir_name')); + $destinationPath = themes_path().'/'.$newDirName; + + $data = array_except($data, 'dir_name'); + + if (!strlen(trim(array_get($data, 'name')))) { + throw new ValidationException(['name' => 'Please specify a name for the theme.']); + } + + if (!preg_match('/^[a-z0-9\_\-]+$/i', $newDirName)) { + throw new ValidationException(['dir_name' => 'Name can contain only digits, Latin letters and the following symbols: _-']); + } + + if (File::isDirectory($destinationPath)) { + throw new ValidationException(['dir_name' => 'Desired theme directory already exists.']); + } + + File::makeDirectory($destinationPath); + File::makeDirectory($destinationPath.'/assets'); + File::makeDirectory($destinationPath.'/content'); + File::makeDirectory($destinationPath.'/layouts'); + File::makeDirectory($destinationPath.'/pages'); + File::makeDirectory($destinationPath.'/partials'); + File::put($destinationPath.'/theme.yaml', ''); + + $theme = CmsTheme::load($newDirName); + $theme->writeConfig($data); + + Flash::success('Created theme successfully!'); + return Redirect::refresh(); + } + + protected function makeCreateFormWidget() + { + $widgetConfig = $this->makeConfig('~/modules/cms/classes/theme/fields.yaml'); + $widgetConfig->alias = 'formCreateTheme'; + $widgetConfig->model = new CmsTheme; + $widgetConfig->arrayName = 'Theme'; + $widgetConfig->context = 'create'; + + $widget = $this->makeWidget('Backend\Widgets\Form', $widgetConfig); + return $widget; + } + + // + // Duplicate + // + + public function index_onLoadDuplicateForm() + { + $theme = $this->findThemeObject(); + $this->vars['themeDir'] = $theme->getDirName(); + + return $this->makePartial('theme_duplicate_form'); + } + + public function index_onDuplicateTheme() + { + $theme = $this->findThemeObject(); + $newDirName = trim(post('new_dir_name')); + $sourcePath = $theme->getPath(); + $destinationPath = themes_path().'/'.$newDirName; + + if (!preg_match('/^[a-z0-9\_\-]+$/i', $newDirName)) { + throw new ValidationException(['new_dir_name' => 'Name can contain only digits, Latin letters and the following symbols: _-']); + } + + if (File::isDirectory($destinationPath)) { + throw new ValidationException(['new_dir_name' => 'Duplicate theme directory already exists.']); + } + + File::copyDirectory($sourcePath, $destinationPath); + $newTheme = CmsTheme::load($newDirName); + $newName = $newTheme->getConfigValue('name') . ' - Copy'; + $newTheme->writeConfig(['name' => $newName]); + + Flash::success('Duplicated theme successfully!'); + return Redirect::refresh(); + } + // // Theme customization // @@ -149,6 +273,90 @@ class Themes extends Controller } } + // + // Theme export + // + + public function index_onLoadExportForm() + { + $theme = $this->findThemeObject(); + $this->vars['widget'] = $this->makeExportFormWidget($theme); + $this->vars['themeDir'] = $theme->getDirName(); + + return $this->makePartial('theme_export_form'); + } + + public function index_onExport() + { + $theme = $this->findThemeObject(); + $widget = $this->makeExportFormWidget($theme); + + $model = new ThemeExport; + $file = $model->export($theme, $widget->getSaveData()); + + return Backend::redirect('cms/themes/download/'.$file.'/'.$theme->getDirName().'.zip'); + } + + public function download($name, $outputName = null) + { + try { + $this->pageTitle = 'Download theme export archive'; + return ThemeExport::download($name, $outputName); + } + catch (Exception $ex) { + $this->handleError($ex); + } + } + + protected function makeExportFormWidget($theme) + { + $widgetConfig = $this->makeConfig('~/modules/cms/models/themeexport/fields.yaml'); + $widgetConfig->alias = 'form'.studly_case($theme->getDirName()); + $widgetConfig->model = new ThemeExport; + $widgetConfig->model->theme = $theme; + $widgetConfig->arrayName = 'ThemeExport'; + + $widget = $this->makeWidget('Backend\Widgets\Form', $widgetConfig); + return $widget; + } + + // + // Theme import + // + + public function index_onLoadImportForm() + { + $theme = $this->findThemeObject(); + $this->vars['widget'] = $this->makeImportFormWidget($theme); + $this->vars['themeDir'] = $theme->getDirName(); + + return $this->makePartial('theme_import_form'); + } + + public function index_onImport() + { + $theme = $this->findThemeObject(); + $widget = $this->makeImportFormWidget($theme); + + $model = new ThemeImport; + $model->import($theme, $widget->getSaveData(), $widget->getSessionKey()); + + Flash::success('Imported theme successfully!'); + return Redirect::refresh(); + } + + protected function makeImportFormWidget($theme) + { + $widgetConfig = $this->makeConfig('~/modules/cms/models/themeimport/fields.yaml'); + $widgetConfig->alias = 'form'.studly_case($theme->getDirName()); + $widgetConfig->model = new ThemeImport; + $widgetConfig->model->theme = $theme; + $widgetConfig->arrayName = 'ThemeImport'; + + $widget = $this->makeWidget('Backend\Widgets\Form', $widgetConfig); + return $widget; + } + // // Helpers // diff --git a/modules/cms/controllers/themes/_theme_create_form.htm b/modules/cms/controllers/themes/_theme_create_form.htm new file mode 100644 index 000000000..ab0972bc5 --- /dev/null +++ b/modules/cms/controllers/themes/_theme_create_form.htm @@ -0,0 +1,54 @@ + 'themeCreateForm', + 'data-popup-load-indicator' => true +]) ?> + + + + fatalError): ?> + + + + + + + + + + + + + + diff --git a/modules/cms/controllers/themes/_theme_duplicate_form.htm b/modules/cms/controllers/themes/_theme_duplicate_form.htm new file mode 100644 index 000000000..9f117e250 --- /dev/null +++ b/modules/cms/controllers/themes/_theme_duplicate_form.htm @@ -0,0 +1,75 @@ + 'themeDuplicateForm', + 'data-popup-load-indicator' => true, +]) ?> + + + + + + fatalError): ?> + + + + + + + + + + + + + + diff --git a/modules/cms/controllers/themes/_theme_export_form.htm b/modules/cms/controllers/themes/_theme_export_form.htm new file mode 100644 index 000000000..93d334c15 --- /dev/null +++ b/modules/cms/controllers/themes/_theme_export_form.htm @@ -0,0 +1,63 @@ + 'themeExportForm', + 'data-popup-load-indicator' => true, + 'data-request-success' => 'closeExportThemePopup()' +]) ?> + + + + + + fatalError): ?> + + + + + + + + + + + + + + diff --git a/modules/cms/controllers/themes/_theme_fields_form.htm b/modules/cms/controllers/themes/_theme_fields_form.htm index 39586b2a9..5aec92b66 100644 --- a/modules/cms/controllers/themes/_theme_fields_form.htm +++ b/modules/cms/controllers/themes/_theme_fields_form.htm @@ -1,4 +1,4 @@ - 'themeFieldsForm', 'data-popup-load-indicator' => true ]) ?> diff --git a/modules/cms/controllers/themes/_theme_import_form.htm b/modules/cms/controllers/themes/_theme_import_form.htm new file mode 100644 index 000000000..a3896ed01 --- /dev/null +++ b/modules/cms/controllers/themes/_theme_import_form.htm @@ -0,0 +1,57 @@ + 'themeImportForm', + 'data-popup-load-indicator' => true, +]) ?> + + + + + + + fatalError): ?> + + + + + + + + + + + + + + diff --git a/modules/cms/controllers/themes/_theme_list.htm b/modules/cms/controllers/themes/_theme_list.htm index a221e4b92..99c7a3d7e 100644 --- a/modules/cms/controllers/themes/_theme_list.htm +++ b/modules/cms/controllers/themes/_theme_list.htm @@ -14,6 +14,15 @@
+ + Create a new blank theme + Edit properties -
diff --git a/modules/cms/controllers/themes/download.htm b/modules/cms/controllers/themes/download.htm new file mode 100644 index 000000000..976a5aa98 --- /dev/null +++ b/modules/cms/controllers/themes/download.htm @@ -0,0 +1,13 @@ + + + + +fatalError): ?> + +

fatalError) ?>

+

Return to themes list

+ + \ No newline at end of file diff --git a/modules/cms/models/ThemeExport.php b/modules/cms/models/ThemeExport.php new file mode 100644 index 000000000..dc9247f25 --- /dev/null +++ b/modules/cms/models/ThemeExport.php @@ -0,0 +1,141 @@ + null, + 'themeName' => null, + 'dirName' => null, + 'folders' => [ + 'assets' => true, + 'pages' => true, + 'layouts' => true, + 'partials' => true, + 'content' => true, + ] + ]; + + public function getFoldersOptions() + { + return [ + 'assets' => 'Assets', + 'pages' => 'Pages', + 'layouts' => 'Layouts', + 'partials' => 'Partials', + 'content' => 'Content', + ]; + } + + public function setThemeAttribute($theme) + { + if (!$theme instanceof CmsTheme) return; + + $this->attributes['themeName'] = $theme->getConfigValue('name', $theme->getDirName()); + $this->attributes['dirName'] = $theme->getDirName(); + $this->attributes['theme'] = $theme; + } + + public function export($theme, $data = []) + { + $this->theme = $theme; + $this->fill($data); + + try { + $themePath = $this->theme->getPath(); + $tempPath = temp_path() . '/'.uniqid('oc'); + $zipName = uniqid('oc'); + $zipPath = temp_path().'/'.$zipName; + + if (!@mkdir($tempPath)) + throw new ApplicationException('Unable to create directory '.$tempPath); + + if (!@mkdir($metaPath = $tempPath . '/meta')) + throw new ApplicationException('Unable to create directory '.$metaPath); + + File::copy($themePath.'/theme.yaml', $tempPath.'/theme.yaml'); + File::copyDirectory($themePath.'/meta', $metaPath); + + foreach ($this->folders as $folder) { + if (!array_key_exists($folder, $this->getFoldersOptions())) continue; + File::copyDirectory($themePath.'/'.$folder, $tempPath.'/'.$folder); + } + + Zip::make($zipPath, $tempPath); + File::deleteDirectory($tempPath); + } + catch (Exception $ex) { + + if (strlen($tempPath) && File::isDirectory($tempPath)) { + File::deleteDirectory($tempPath); + } + + if (strlen($zipPath) && File::isFile($zipPath)) { + File::delete($zipPath); + } + + throw $ex; + } + + return $zipName; + } + + public static function download($name, $outputName = null) + { + if (!preg_match('/^oc[0-9a-z]*$/i', $name)) { + throw new ApplicationException('File not found'); + } + + $zipPath = temp_path() . '/' . $name; + if (!file_exists($zipPath)) { + throw new ApplicationException('File not found'); + } + + $headers = Response::download($zipPath, $outputName)->headers->all(); + $result = Response::make(File::get($zipPath), 200, $headers); + + @unlink($zipPath); + + return $result; + } + +} \ No newline at end of file diff --git a/modules/cms/models/ThemeImport.php b/modules/cms/models/ThemeImport.php new file mode 100644 index 000000000..85c12ddd2 --- /dev/null +++ b/modules/cms/models/ThemeImport.php @@ -0,0 +1,139 @@ + ['System\Models\File'] + ]; + + /** + * @var array Make the model's attributes public so behaviors can modify them. + */ + public $attributes = [ + 'theme' => null, + 'themeName' => null, + 'dirName' => null, + 'overwrite' => true, + 'folders' => [ + 'assets' => true, + 'pages' => true, + 'layouts' => true, + 'partials' => true, + 'content' => true, + ] + ]; + + public function getFoldersOptions() + { + return [ + 'assets' => 'Assets', + 'pages' => 'Pages', + 'layouts' => 'Layouts', + 'partials' => 'Partials', + 'content' => 'Content', + ]; + } + + public function setThemeAttribute($theme) + { + if (!$theme instanceof CmsTheme) return; + + $this->attributes['themeName'] = $theme->getConfigValue('name', $theme->getDirName()); + $this->attributes['dirName'] = $theme->getDirName(); + $this->attributes['theme'] = $theme; + } + + public function import($theme, $data = [], $sessionKey = null) + { + @set_time_limit(3600); + + $this->theme = $theme; + $this->fill($data); + + try + { + $file = $this->uploaded_file()->withDeferred($sessionKey)->first(); + if (!$file) { + throw new ApplicationException('There is no file attached to import!'); + } + + $themePath = $this->theme->getPath(); + $tempPath = temp_path() . '/'.uniqid('oc'); + $zipName = uniqid('oc'); + $zipPath = temp_path().'/'.$zipName; + + File::put($zipPath, $file->getContents()); + + if (!@mkdir($tempPath)) + throw new ApplicationException('Unable to create directory '.$tempPath); + + Zip::extract($zipPath, $tempPath); + + // if (File::isFile($tempPath.'/theme.yaml')) { + // File::copy($tempPath.'/theme.yaml', $themePath.'/theme.yaml'); + // } + + if (File::isDirectory($tempPath.'/meta')) { + File::copyDirectory($tempPath.'/meta', $themePath.'/meta'); + } + + foreach ($this->folders as $folder) { + if (!array_key_exists($folder, $this->getFoldersOptions())) continue; + File::copyDirectory($tempPath.'/'.$folder, $themePath.'/'.$folder); + } + + File::deleteDirectory($tempPath); + File::delete($zipPath); + $file->delete(); + } + catch (Exception $ex) { + + if (!empty($tempPath) && File::isDirectory($tempPath)) { + File::deleteDirectory($tempPath); + } + + if (!empty($zipPath) && File::isFile($zipPath)) { + File::delete($zipPath); + } + + throw $ex; + } + } + +} \ No newline at end of file diff --git a/modules/cms/models/themeexport/fields.yaml b/modules/cms/models/themeexport/fields.yaml new file mode 100644 index 000000000..928c09af4 --- /dev/null +++ b/modules/cms/models/themeexport/fields.yaml @@ -0,0 +1,14 @@ +# =================================== +# Field Definitions +# =================================== + +fields: + + themeName: + label: Theme + disabled: true + + folders: + label: Folders + commentAbove: Please select the theme folders you would like to export + type: checkboxlist diff --git a/modules/cms/models/themeimport/fields.yaml b/modules/cms/models/themeimport/fields.yaml new file mode 100644 index 000000000..c1879a875 --- /dev/null +++ b/modules/cms/models/themeimport/fields.yaml @@ -0,0 +1,25 @@ +# =================================== +# Field Definitions +# =================================== + +fields: + + themeName: + label: Theme + disabled: true + + uploaded_file: + label: Theme archive file + type: fileupload + mode: file + fileTypes: zip + + overwrite: + label: Overwrite existing files + comment: Untick this box to only import new files + type: checkbox + + folders: + label: Folders + commentAbove: Please select the theme folders you would like to import + type: checkboxlist