diff --git a/CHANGELOG.md b/CHANGELOG.md index bdb7a20b7..d25d075dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +* **Build 246** (2015-04-21) + - Adds experimental command `october:mirror` for generating symbolic links in a public directory. + - Various performance improvements. + * **Build 239** (2015-04-06) - Installing plugins has a new interface and themes can now be installed using the back-end. diff --git a/modules/backend/classes/BackendController.php b/modules/backend/classes/BackendController.php index ff4bc43e8..6fb4ef53d 100644 --- a/modules/backend/classes/BackendController.php +++ b/modules/backend/classes/BackendController.php @@ -6,6 +6,7 @@ use File; use Config; use Illuminate\Routing\Controller as ControllerBase; use October\Rain\Router\Helper as RouterHelper; +use Closure; /** * The Backend controller class. @@ -16,6 +17,13 @@ use October\Rain\Router\Helper as RouterHelper; */ class BackendController extends ControllerBase { + use \October\Rain\Extension\ExtendableTrait; + + /** + * @var array Behaviors implemented by this controller. + */ + public $implement; + /** * @var string Allows early access to page action. */ @@ -26,6 +34,22 @@ class BackendController extends ControllerBase */ public static $params; + /** + * Instantiate a new BackendController instance. + */ + public function __construct() + { + $this->extendableConstruct(); + } + + /** + * Extend this object properties upon construction. + */ + public static function extend(Closure $callback) + { + self::extendableExtendCallback($callback); + } + /** * Finds and serves the requested backend controller. * If the controller cannot be found, returns the Cms page with the URL /404. diff --git a/modules/backend/formwidgets/FileUpload.php b/modules/backend/formwidgets/FileUpload.php index 6d936db2b..1b52d5f06 100644 --- a/modules/backend/formwidgets/FileUpload.php +++ b/modules/backend/formwidgets/FileUpload.php @@ -292,6 +292,10 @@ class FileUpload extends FormWidgetBase } try { + if (!Input::hasFile('file_data')) { + throw new ApplicationException('File missing from request'); + } + $uploadedFile = Input::file('file_data'); $validationRules = ['max:'.File::getMaxFilesize()]; diff --git a/modules/cms/classes/CmsController.php b/modules/cms/classes/CmsController.php new file mode 100644 index 000000000..ab884406f --- /dev/null +++ b/modules/cms/classes/CmsController.php @@ -0,0 +1,49 @@ +extendableConstruct(); + } + + /** + * Extend this object properties upon construction. + */ + public static function extend(Closure $callback) + { + self::extendableExtendCallback($callback); + } + + /** + * Finds and serves the request using the primary controller. + * @param string $url Specifies the requested page URL. + * If the parameter is omitted, the current URL used. + * @return string Returns the processed page content. + */ + public function run($url = null) + { + return App::make('Cms\Classes\Controller')->run($url); + } +} diff --git a/modules/cms/classes/CmsException.php b/modules/cms/classes/CmsException.php index e5d9b6853..82b47e9e4 100644 --- a/modules/cms/classes/CmsException.php +++ b/modules/cms/classes/CmsException.php @@ -3,7 +3,7 @@ use File; use Twig_Error; use Cms\Classes\SectionParser; -use ApplicationException; +use October\Rain\Exception\ApplicationException; use Exception; /** diff --git a/modules/cms/classes/Controller.php b/modules/cms/classes/Controller.php index 46558c1e3..4b0a65d65 100644 --- a/modules/cms/classes/Controller.php +++ b/modules/cms/classes/Controller.php @@ -664,7 +664,7 @@ class Controller list($componentName, $handlerName) = explode('::', $handler); $componentObj = $this->findComponentByName($componentName); - if ($componentObj && method_exists($componentObj, $handlerName)) { + if ($componentObj && $componentObj->methodExists($handlerName)) { $this->componentContext = $componentObj; $result = $componentObj->runAjaxHandler($handlerName); return ($result) ?: true; @@ -1076,7 +1076,7 @@ class Controller $url = substr($url, 1); } - $routeAction = 'Cms\Classes\Controller@run'; + $routeAction = 'Cms\Classes\CmsController@run'; $actionExists = Route::getRoutes()->getByAction($routeAction) !== null; if ($actionExists) { @@ -1288,50 +1288,4 @@ class Controller } } } - - // - // Keep Laravel Happy - // - - /** - * Get the middleware assigned to the controller. - * - * @return array - */ - public function getMiddleware() - { - return []; - } - - /** - * Get the registered "before" filters. - * - * @return array - */ - public function getBeforeFilters() - { - return []; - } - - /** - * Get the registered "after" filters. - * - * @return array - */ - public function getAfterFilters() - { - return []; - } - - /** - * Execute an action on the controller. - * - * @param string $method - * @param array $parameters - * @return \Symfony\Component\HttpFoundation\Response - */ - public function callAction($method, $parameters) - { - return call_user_func_array(array($this, $method), $parameters); - } } diff --git a/modules/cms/classes/Theme.php b/modules/cms/classes/Theme.php index 01bdc3780..47d87fd2a 100644 --- a/modules/cms/classes/Theme.php +++ b/modules/cms/classes/Theme.php @@ -308,12 +308,11 @@ class Theme public function getPreviewImageUrl() { $previewPath = '/assets/images/theme-preview.png'; - $path = $this->getPath().$previewPath; - if (!File::exists($path)) { - return URL::asset('modules/cms/assets/images/default-theme-preview.png'); + if (File::exists($this->getPath().$previewPath)) { + return URL::asset('themes/'.$this->getDirName().$previewPath); } - return URL::asset('themes/'.$this->getDirName().$previewPath); + return URL::asset('modules/cms/assets/images/default-theme-preview.png'); } /** diff --git a/modules/cms/routes.php b/modules/cms/routes.php index 5e84b32a6..f99bd1150 100644 --- a/modules/cms/routes.php +++ b/modules/cms/routes.php @@ -8,5 +8,5 @@ App::before(function ($request) { * The CMS module intercepts all URLs that were not * handled by the back-end modules. */ - Route::any('{slug}', 'Cms\Classes\Controller@run')->where('slug', '(.*)?'); + Route::any('{slug}', 'Cms\Classes\CmsController@run')->where('slug', '(.*)?'); }); diff --git a/modules/cms/twig/DebugExtension.php b/modules/cms/twig/DebugExtension.php index bd538fa8f..c49f1ca27 100644 --- a/modules/cms/twig/DebugExtension.php +++ b/modules/cms/twig/DebugExtension.php @@ -113,9 +113,12 @@ class DebugExtension extends Twig_Extension elseif (is_array($var)) { $caption = static::ARRAY_CAPTION; } - else { + elseif (is_object($var)) { $caption = [static::OBJECT_CAPTION, get_class($var)]; } + else { + $caption = [static::OBJECT_CAPTION, gettype($var)]; + } $result .= $this->dump($var, $caption); } @@ -443,6 +446,9 @@ class DebugExtension extends Twig_Extension $vars = []; foreach ($info->getProperties() as $property) { + if ($property->isStatic()) { + continue; // Only non-static + } if (!$property->isPublic()) { continue; // Only public } diff --git a/modules/system/ServiceProvider.php b/modules/system/ServiceProvider.php index 08029fb1d..0cfda6edb 100644 --- a/modules/system/ServiceProvider.php +++ b/modules/system/ServiceProvider.php @@ -345,16 +345,17 @@ class ServiceProvider extends ModuleServiceProvider */ protected function registerPrivilegedActions() { - $requests = ['/combine', '@/system/updates', '@/backend/auth']; + $requests = ['/combine', '@/system/updates', '@/system/install', '@/backend/auth']; $commands = ['october:up', 'october:update']; /* * Requests */ $path = RouterHelper::normalizeUrl(Request::path()); + $backendUri = RouterHelper::normalizeUrl(Config::get('cms.backendUri')); foreach ($requests as $request) { if (substr($request, 0, 1) == '@') { - $request = Config::get('cms.backendUri') . substr($request, 1); + $request = $backendUri . substr($request, 1); } if (stripos($path, $request) === 0) { diff --git a/modules/system/assets/css/updates/install.css b/modules/system/assets/css/updates/install.css index 7bcd70388..9ff5c3d71 100644 --- a/modules/system/assets/css/updates/install.css +++ b/modules/system/assets/css/updates/install.css @@ -7,6 +7,7 @@ margin: 0; padding: 10px 0; overflow: hidden; + /* clearfix */ } .product-list li button { position: absolute; @@ -38,9 +39,6 @@ padding-bottom: 10px; overflow: hidden; } -.plugin-list li:last-child { - border-bottom: none; -} .plugin-list li .image { float: left; margin-right: 15px; @@ -61,6 +59,9 @@ color: #C03F31; font-weight: 400; } +.plugin-list li:last-child { + border-bottom: none; +} .theme-list li { float: left; padding: 0; @@ -70,11 +71,6 @@ background: #fff; position: relative; border-radius: 3px; -} -.theme-list li:hover { - border-color: transparent; -} -.theme-list li { -webkit-transition: border .2s linear; -moz-transition: border .2s linear; transition: border .2s linear; @@ -86,9 +82,6 @@ width: 210px; height: 140px; } -.theme-list li:hover .image { - opacity: 0; -} .theme-list li .details { position: absolute; bottom: 0; @@ -97,9 +90,6 @@ padding: 10px; overflow: hidden; } -.theme-list li:hover .details { - opacity: 1; -} .theme-list li h4 { padding: 15px 0 0; margin: 0; @@ -111,32 +101,34 @@ text-transform: uppercase; font-size: 12px; } +.theme-list li:hover { + border-color: transparent; +} +.theme-list li:hover .image { + opacity: 0; +} +.theme-list li:hover .details { + opacity: 1; +} .suggested-products { padding: 0; } .suggested-products .product { padding: 0; } +.suggested-products .image { + float: left; + position: relative; +} .suggested-products .image img { width: 40px; height: 40px; margin-top: 10px; } -.suggested-themes .image img { - width: 60px; - height: 40px; -} -.suggested-products .image { - float: left; - position: relative; -} .suggested-products .details { margin-left: 50px; padding: 10px 0; } -.suggested-themes .details { - margin-left: 70px; -} .suggested-products .details h5 { margin: 0 0 3px; font-size: 14px; @@ -175,6 +167,13 @@ .suggested-products a:hover .image img { opacity: .5; } +.suggested-themes .image img { + width: 60px; + height: 40px; +} +.suggested-themes .details { + margin-left: 70px; +} /*! * Typeahead */ @@ -185,6 +184,24 @@ text-align: left; padding-bottom: 15px; } +.product-search > i.icon { + position: absolute; + top: 50%; + right: 15px; + font-size: 24px; + margin-top: -20px; + color: rgba(0, 0, 0, 0.35); +} +.product-search > i.icon.loading { + display: block; + width: 24px; + height: 24px; + background-image: url(../../../../backend/assets/images/loading-indicator-transparent.svg); + background-size: 24px 24px; + background-position: 50% 50%; + -webkit-animation: spin 1s linear infinite; + animation: spin 1s linear infinite; +} .twitter-typeahead { width: 100%; } diff --git a/modules/system/assets/js/updates/install.js b/modules/system/assets/js/updates/install.js index 1f4c0f462..2a37cdb81 100644 --- a/modules/system/assets/js/updates/install.js +++ b/modules/system/assets/js/updates/install.js @@ -53,7 +53,21 @@ var engine = new Bloodhound({ name: 'products', method: 'POST', - remote: window.location.pathname + '?search=' + searchType + '&query=%QUERY', + remote: { + url: window.location.pathname + '?search=' + searchType + '&query=%QUERY', + ajax: { + beforeSend: function() { + $('.icon', $form).hide() + $('.icon.loading', $form).show() + $el.data('searchReady', false) + }, + complete: function() { + $('.icon', $form).show() + $('.icon.loading', $form).hide() + $el.data('searchReady', true) + } + } + }, datumTokenizer: function(d) { return Bloodhound.tokenizers.whitespace(d.val) }, @@ -92,6 +106,9 @@ $el = $(el), $input = $el.find('.product-search-input.tt-input:first') + if (!$input.data('searchReady')) + return + $el.popup() $input.typeahead('val', '') diff --git a/modules/system/assets/less/updates/install.less b/modules/system/assets/less/updates/install.less index 338e0d84e..631dc9b4a 100644 --- a/modules/system/assets/less/updates/install.less +++ b/modules/system/assets/less/updates/install.less @@ -205,6 +205,25 @@ margin: 0 auto 0 auto; text-align: left; padding-bottom: 15px; + + > i.icon { + position: absolute; + top: 50%; + right: 15px; + font-size: 24px; + margin-top: -20px; + color: rgba(0,0,0,.35); + } + + > i.icon.loading { + display: block; + width: 24px; + height: 24px; + background-image: url(../../../../backend/assets/images/loading-indicator-transparent.svg); + background-size: 24px 24px; + background-position: 50% 50%; + .animation(spin 1s linear infinite); + } } .twitter-typeahead { width: 100%; @@ -247,11 +266,9 @@ .tt-suggestion { font-size: 14px; line-height: 18px; - + { - .tt-suggestion { - font-size: 14px; - border-top: 1px solid #ccc; - } + + .tt-suggestion { + font-size: 14px; + border-top: 1px solid #ccc; } } .tt-suggestions { diff --git a/modules/system/classes/PluginManager.php b/modules/system/classes/PluginManager.php index fa06eac42..d4cf86418 100644 --- a/modules/system/classes/PluginManager.php +++ b/modules/system/classes/PluginManager.php @@ -210,6 +210,14 @@ class PluginManager View::addNamespace($pluginNamespace, $viewsPath); } + /* + * Add init, if available + */ + $initFile = $pluginPath . '/init.php'; + if (!self::$noInit && File::exists($initFile)) { + require $initFile; + } + /* * Add routes, if available */ diff --git a/modules/system/controllers/updates/_install_plugins.htm b/modules/system/controllers/updates/_install_plugins.htm index 378baf0d3..32dcdbe72 100644 --- a/modules/system/controllers/updates/_install_plugins.htm +++ b/modules/system/controllers/updates/_install_plugins.htm @@ -14,6 +14,8 @@ placeholder="search plugins to install..." data-search-type="plugins" /> + + diff --git a/modules/system/controllers/updates/_install_themes.htm b/modules/system/controllers/updates/_install_themes.htm index 9b1f3d803..eb88c0bce 100644 --- a/modules/system/controllers/updates/_install_themes.htm +++ b/modules/system/controllers/updates/_install_themes.htm @@ -14,6 +14,8 @@ placeholder="search themes to install..." data-search-type="themes" /> + + diff --git a/modules/system/lang/ru/lang.php b/modules/system/lang/ru/lang.php index e05cb5cc6..e65890f32 100644 --- a/modules/system/lang/ru/lang.php +++ b/modules/system/lang/ru/lang.php @@ -23,7 +23,7 @@ return [ 'ru' => 'Russian', 'se' => 'Swedish', 'sk' => 'Slovak (Slovakia)', - 'tr' => 'Turkish' + 'tr' => 'Turkish', 'nb-no' => 'Norwegian (Bokmål)' ], 'directory' => [ diff --git a/modules/system/lang/sv/lang.php b/modules/system/lang/sv/lang.php index d6ebb114c..0a45ed9c1 100644 --- a/modules/system/lang/sv/lang.php +++ b/modules/system/lang/sv/lang.php @@ -23,7 +23,7 @@ return [ 'ru' => 'Russian', 'se' => 'Swedish', 'sk' => 'Slovak (Slovakia)', - 'tr' => 'Turkish' + 'tr' => 'Turkish', 'nb-no' => 'Norska (Bokmål)' ], 'directory' => [ diff --git a/modules/system/models/EventLog.php b/modules/system/models/EventLog.php index d70f9fc32..a8d34157c 100644 --- a/modules/system/models/EventLog.php +++ b/modules/system/models/EventLog.php @@ -2,6 +2,7 @@ use Str; use Model; +use Exception; /** * Model for logging system errors and debug trace messages @@ -38,7 +39,10 @@ class EventLog extends Model $record->details = (array) $details; } - $record->save(); + try { + $record->save(); + } + catch (Exception $ex) {} return $record; } diff --git a/modules/system/traits/ViewMaker.php b/modules/system/traits/ViewMaker.php index c7d409820..830f970a5 100644 --- a/modules/system/traits/ViewMaker.php +++ b/modules/system/traits/ViewMaker.php @@ -50,7 +50,8 @@ trait ViewMaker public function makePartial($partial, $params = [], $throwException = true) { if (!File::isPathSymbol($partial) && realpath($partial) === false) { - $partial = '_' . strtolower($partial) . '.htm'; + $folder = strpos($partial, '/') !== false ? dirname($partial) . '/' : ''; + $partial = $folder . '_' . strtolower(basename($partial)).'.htm'; } $partialPath = $this->getViewPath($partial); @@ -136,7 +137,8 @@ trait ViewMaker public function makeLayoutPartial($partial, $params = []) { if (!File::isLocalPath($partial) && !File::isPathSymbol($partial)) { - $partial = '_' . strtolower($partial); + $folder = strpos($partial, '/') !== false ? dirname($partial) . '/' : ''; + $partial = $folder . '_' . strtolower(basename($partial)); } return $this->makeLayout($partial, $params);