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);