diff --git a/modules/cms/ServiceProvider.php b/modules/cms/ServiceProvider.php index 28f57e5c2..2a528b8b4 100644 --- a/modules/cms/ServiceProvider.php +++ b/modules/cms/ServiceProvider.php @@ -4,7 +4,6 @@ use Lang; use Backend; use BackendMenu; use BackendAuth; -use System\Classes\MarkupManager; use Backend\Classes\WidgetManager; use October\Rain\Support\ModuleServiceProvider; @@ -93,48 +92,6 @@ class ServiceProvider extends ModuleServiceProvider ]); }); - /* - * Register markup tags - */ - MarkupManager::instance()->registerCallback(function($manager){ - $manager->registerFunctions([ - // Global helpers - 'post' => 'post', - - // Form helpers - 'form_ajax' => ['Form', 'ajax'], - 'form_open' => ['Form', 'open'], - 'form_close' => ['Form', 'close'], - 'form_token' => ['Form', 'token'], - 'form_session_key' => ['Form', 'sessionKey'], - 'form_token' => ['Form', 'token'], - 'form_model' => ['Form', 'model'], - 'form_label' => ['Form', 'label'], - 'form_text' => ['Form', 'text'], - 'form_password' => ['Form', 'password'], - 'form_checkbox' => ['Form', 'checkbox'], - 'form_radio' => ['Form', 'radio'], - 'form_file' => ['Form', 'file'], - 'form_select' => ['Form', 'select'], - 'form_select_range' => ['Form', 'selectRange'], - 'form_select_month' => ['Form', 'selectMonth'], - 'form_submit' => ['Form', 'submit'], - 'form_macro' => ['Form', '__call'], - 'form_value' => ['Form', 'value'], - ]); - - $manager->registerFilters([ - // String helpers - 'slug' => ['Str', 'slug'], - 'plural' => ['Str', 'plural'], - 'singular' => ['Str', 'singular'], - 'finish' => ['Str', 'finish'], - 'snake' => ['Str', 'snake'], - 'camel' => ['Str', 'camel'], - 'studly' => ['Str', 'studly'], - ]); - }); - /* * Register widgets */ diff --git a/modules/cms/classes/CombineAssets.php b/modules/cms/classes/CombineAssets.php index f1e41418d..be749c15d 100644 --- a/modules/cms/classes/CombineAssets.php +++ b/modules/cms/classes/CombineAssets.php @@ -4,6 +4,7 @@ use URL; use File; use Lang; use Cache; +use Route; use Config; use Request; use Response; @@ -193,7 +194,13 @@ class CombineAssets */ protected function getCombinedUrl($outputFilename = 'undefined.css') { - return URL::action('Cms\Classes\Controller@combine', [$outputFilename], false); + $combineAction = 'Cms\Classes\Controller@combine'; + $actionExists = Route::getRoutes()->getByAction($combineAction) !== null; + + if ($actionExists) + return URL::action($combineAction, [$outputFilename], false); + else + return Request::getBasePath().'/combine/'.$outputFilename; } /** diff --git a/modules/system/ServiceProvider.php b/modules/system/ServiceProvider.php index dba82c288..430934ab3 100644 --- a/modules/system/ServiceProvider.php +++ b/modules/system/ServiceProvider.php @@ -10,6 +10,7 @@ use BackendAuth; use Twig_Environment; use Twig_Loader_String; use System\Classes\ErrorHandler; +use System\Classes\MarkupManager; use System\Classes\PluginManager; use System\Classes\SettingsManager; use System\Twig\Engine as TwigEngine; @@ -179,6 +180,44 @@ class ServiceProvider extends ModuleServiceProvider ]); }); + /* + * Register markup tags + */ + MarkupManager::instance()->registerCallback(function($manager){ + $manager->registerFunctions([ + // Functions + 'post' => 'post', + 'link_to' => 'link_to', + 'link_to_asset' => 'link_to_asset', + 'link_to_route' => 'link_to_route', + 'link_to_action' => 'link_to_action', + 'asset' => 'asset', + 'action' => 'action', + 'url' => 'url', + 'route' => 'route', + 'secure_url' => 'secure_url', + 'secure_asset' => 'secure_asset', + + // Classes + 'str_*' => ['Str', '*'], + 'url_*' => ['URL', '*'], + 'html_*' => ['HTML', '*'], + 'form_*' => ['Form', '*'], + 'form_macro' => ['Form', '__call'], + ]); + + $manager->registerFilters([ + // Classes + 'slug' => ['Str', 'slug'], + 'plural' => ['Str', 'plural'], + 'singular' => ['Str', 'singular'], + 'finish' => ['Str', 'finish'], + 'snake' => ['Str', 'snake'], + 'camel' => ['Str', 'camel'], + 'studly' => ['Str', 'studly'], + ]); + }); + /* * Register settings */ diff --git a/modules/system/classes/MarkupManager.php b/modules/system/classes/MarkupManager.php index 969e7886f..6a6453900 100644 --- a/modules/system/classes/MarkupManager.php +++ b/modules/system/classes/MarkupManager.php @@ -1,5 +1,10 @@ listExtensions(self::EXTENSION_TOKEN_PARSER); } + /** + * Makes a set of Twig functions for use in a twig extension. + * @param array $functions Current collection + * @return array + */ + public function makeTwigFunctions($functions = []) + { + if (!is_array($functions)) + $functions = []; + + foreach ($this->listFunctions() as $name => $callable) { + + /* + * Handle a wildcard function + */ + if (strpos($name, '*') !== false && $this->isWildCallable($callable)) { + $callable = function($name) use ($callable) { + $arguments = array_slice(func_get_args(), 1); + $method = $this->isWildCallable($callable, Str::camel($name)); + return call_user_func_array($method, $arguments); + }; + } + + if (!is_callable($callable)) + throw new ApplicationException(sprintf('The markup function for %s is not callable.', $name)); + + $functions[] = new Twig_SimpleFunction($name, $callable, ['is_safe' => ['html']]); + } + + return $functions; + } + + /** + * Makes a set of Twig filters for use in a twig extension. + * @param array $filters Current collection + * @return array + */ + public function makeTwigFilters($filters = []) + { + if (!is_array($filters)) + $filters = []; + + foreach ($this->listFilters() as $name => $callable) { + + /* + * Handle a wildcard function + */ + if (strpos($name, '*') !== false && $this->isWildCallable($callable)) { + $callable = function($name) use ($callable) { + $arguments = array_slice(func_get_args(), 1); + $method = $this->isWildCallable($callable, Str::camel($name)); + return call_user_func_array($method, $arguments); + }; + } + + if (!is_callable($callable)) + throw new ApplicationException(sprintf('The markup filter for %s is not callable.', $name)); + + $filters[] = new Twig_SimpleFilter($name, $callable, ['is_safe' => ['html']]); + } + + return $filters; + } + + /** + * Makes a set of Twig token parsers for use in a twig extension. + * @param array $parsers Current collection + * @return array + */ + public function makeTwigTokenParsers($parsers = []) + { + if (!is_array($parsers)) + $parsers = []; + + $extraParsers = $this->listTokenParsers(); + foreach ($extraParsers as $obj) { + if (!$obj instanceof Twig_TokenParser) + continue; + + $parsers[] = $obj; + } + + return $parsers; + } + + /** + * Tests if a callable type contains a wildcard, also acts as a + * utility to replace the wildcard with a string. + * @param callable $callable + * @param string $replaceWith + * @return mixed + */ + protected function isWildCallable($callable, $replaceWith = false) + { + $isWild = false; + + if (is_string($callable) && strpos($callable, '*') !== false) + $isWild = $replaceWith ? str_replace('*', $replaceWith, $callable) : true; + + if (is_array($callable)) { + if (is_string($callable[0]) && strpos($callable[0], '*') !== false) { + if ($replaceWith) { + $isWild = $callable; + $isWild[0] = str_replace('*', $replaceWith, $callable[0]); + } + else + $isWild = true; + } + + if (!empty($callable[1]) && strpos($callable[1], '*') !== false) { + if ($replaceWith) { + $isWild = $isWild ?: $callable; + $isWild[1] = str_replace('*', $replaceWith, $callable[1]); + } + else + $isWild = true; + } + } + + return $isWild; + } + } \ No newline at end of file diff --git a/modules/system/twig/Extension.php b/modules/system/twig/Extension.php index c51a8450b..71483a294 100644 --- a/modules/system/twig/Extension.php +++ b/modules/system/twig/Extension.php @@ -52,12 +52,7 @@ class Extension extends Twig_Extension /* * Include extensions provided by plugins */ - foreach ($this->markupManager->listFunctions() as $name => $callable) { - if (!is_callable($callable)) - throw new ApplicationException(sprintf('The markup function for %s is not callable.', $name)); - - $functions[] = new Twig_SimpleFunction($name, $callable, ['is_safe' => ['html']]); - } + $functions = $this->markupManager->makeTwigFunctions($functions); return $functions; } @@ -76,12 +71,7 @@ class Extension extends Twig_Extension /* * Include extensions provided by plugins */ - foreach ($this->markupManager->listFilters() as $name => $callable) { - if (!is_callable($callable)) - throw new ApplicationException(sprintf('The markup filter for %s is not callable.', $name)); - - $filters[] = new Twig_SimpleFilter($name, $callable, ['is_safe' => ['html']]); - } + $filters = $this->markupManager->makeTwigFilters($filters); return $filters; } @@ -95,13 +85,10 @@ class Extension extends Twig_Extension { $parsers = []; - $extraParsers = $this->markupManager->listTokenParsers(); - foreach ($extraParsers as $obj) { - if (!$obj instanceof Twig_TokenParser) - continue; - - $parsers[] = $obj; - } + /* + * Include extensions provided by plugins + */ + $parsers = $this->markupManager->makeTwigTokenParsers($parsers); return $parsers; } diff --git a/tests/unit/cms/classes/ControllerTest.php b/tests/unit/cms/classes/ControllerTest.php index 4c04e7f2f..cae7f33d3 100644 --- a/tests/unit/cms/classes/ControllerTest.php +++ b/tests/unit/cms/classes/ControllerTest.php @@ -5,7 +5,8 @@ use Cms\Classes\Theme; class ControllerTest extends TestCase { - public function tearDown() { + public function tearDown() + { Mockery::close(); } diff --git a/tests/unit/system/classes/MarkupManagerTest.php b/tests/unit/system/classes/MarkupManagerTest.php new file mode 100644 index 000000000..f22c4c799 --- /dev/null +++ b/tests/unit/system/classes/MarkupManagerTest.php @@ -0,0 +1,111 @@ +getMethod($name); + $method->setAccessible(true); + return $method->invokeArgs($object, $params); + } + + public static function getProtectedProperty($object, $name) + { + $className = get_class($object); + $class = new ReflectionClass($className); + $property = $class->getProperty($name); + $property->setAccessible(true); + return $property->getValue($object); + } + + public static function setProtectedProperty($object, $name, $value) + { + $className = get_class($object); + $class = new ReflectionClass($className); + $property = $class->getProperty($name); + $property->setAccessible(true); + return $property->setValue($object, $value); + } + + // + // Tests + // + + public function testIsWildCallable() + { + $manager = MarkupManager::instance(); + + /* + * Negatives + */ + $callable = 'something'; + $result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]); + $this->assertFalse($result); + + $callable = ['Form', 'open']; + $result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]); + $this->assertFalse($result); + + $callable = function() { return 'O, Hai!'; }; + $result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]); + $this->assertFalse($result); + + /* + * String + */ + $callable = 'something_*'; + $result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]); + $this->assertTrue($result); + + $result = self::callProtectedMethod($manager, 'isWildCallable', [$callable, 'delicious']); + $this->assertEquals('something_delicious', $result); + + /* + * Array + */ + $callable = ['Class', 'foo_*']; + $result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]); + $this->assertTrue($result); + + $result = self::callProtectedMethod($manager, 'isWildCallable', [$callable, 'bar']); + $this->assertTrue(isset($result[0])); + $this->assertTrue(isset($result[1])); + $this->assertEquals('Class', $result[0]); + $this->assertEquals('foo_bar', $result[1]); + + $callable = ['My*', 'method']; + $result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]); + $this->assertTrue($result); + + $result = self::callProtectedMethod($manager, 'isWildCallable', [$callable, 'Class']); + $this->assertTrue(isset($result[0])); + $this->assertTrue(isset($result[1])); + $this->assertEquals('MyClass', $result[0]); + $this->assertEquals('method', $result[1]); + + $callable = ['My*', 'my*']; + $result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]); + $this->assertTrue($result); + + $result = self::callProtectedMethod($manager, 'isWildCallable', [$callable, 'Food']); + $this->assertTrue(isset($result[0])); + $this->assertTrue(isset($result[1])); + $this->assertEquals('MyFood', $result[0]); + $this->assertEquals('myFood', $result[1]); + } + +} \ No newline at end of file