Browse Source

Merge branch 'master' into 3.1

# Conflicts:
#	src/Controller/Controller.php
antograssiot 10 years ago
parent
commit
dbc2dbe648

+ 6 - 4
src/Controller/Controller.php

@@ -29,6 +29,7 @@ use Cake\Routing\Router;
 use Cake\Utility\MergeVariablesTrait;
 use Cake\View\ViewVarsTrait;
 use LogicException;
+use ReflectionClass;
 use ReflectionException;
 use ReflectionMethod;
 use RuntimeException;
@@ -601,7 +602,7 @@ class Controller implements EventListenerInterface, EventDispatcherInterface
     protected function _viewPath()
     {
         $viewPath = $this->name;
-        if (isset($this->request->params['prefix'])) {
+        if (!empty($this->request->params['prefix'])) {
             $prefixes = array_map(
                 'Cake\Utility\Inflector::camelize',
                 explode('/', $this->request->params['prefix'])
@@ -681,6 +682,10 @@ class Controller implements EventListenerInterface, EventDispatcherInterface
      */
     public function isAction($action)
     {
+        $baseClass = new ReflectionClass('Cake\Controller\Controller');
+        if ($baseClass->hasMethod($action)) {
+            return false;
+        }
         try {
             $method = new ReflectionMethod($this, $action);
         } catch (ReflectionException $e) {
@@ -689,9 +694,6 @@ class Controller implements EventListenerInterface, EventDispatcherInterface
         if (!$method->isPublic()) {
             return false;
         }
-        if ($method->getDeclaringClass()->name === 'Cake\Controller\Controller') {
-            return false;
-        }
         return true;
     }
 

+ 1 - 1
src/Database/Dialect/SqlserverDialectTrait.php

@@ -65,7 +65,7 @@ trait SqlserverDialectTrait
         }
 
         if ($offset !== null && !$query->clause('order')) {
-            $query->order($query->newExpr()->add('SELECT NULL'));
+            $query->order($query->newExpr()->add('(SELECT NULL)'));
         }
 
         if ($this->_version() < 11 && $offset !== null) {

+ 1 - 1
src/Database/Expression/OrderByExpression.php

@@ -49,7 +49,7 @@ class OrderByExpression extends QueryExpression
         $order = [];
         foreach ($this->_conditions as $k => $direction) {
             if ($direction instanceof ExpressionInterface) {
-                $direction = sprintf('(%s)', $direction->sql($generator));
+                $direction = $direction->sql($generator);
             }
             $order[] = is_numeric($k) ? $direction : sprintf('%s %s', $k, $direction);
         }

+ 72 - 0
src/Database/Expression/OrderClauseExpression.php

@@ -0,0 +1,72 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.0.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Database\Expression;
+
+use Cake\Database\ExpressionInterface;
+use Cake\Database\Expression\FieldInterface;
+use Cake\Database\Expression\FieldTrait;
+use Cake\Database\ValueBinder;
+
+/**
+ * An expression object for complex ORDER BY clauses
+ *
+ * @internal
+ */
+class OrderClauseExpression implements ExpressionInterface, FieldInterface
+{
+    use FieldTrait;
+
+    /**
+     * The direction of sorting.
+     *
+     * @var string
+     */
+    protected $_direction;
+
+    /**
+     * Constructor
+     *
+     * @param \Cake\Database\ExpressionInterface|string $field The field to order on.
+     * @param string $direction The direction to sort on.
+     */
+    public function __construct($field, $direction)
+    {
+        $this->_field = $field;
+        $this->_direction = strtolower($direction) === 'asc' ? 'ASC' : 'DESC';
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function sql(ValueBinder $generator)
+    {
+        $field = $this->_field;
+        if ($field instanceof ExpressionInterface) {
+            $field = $field->sql($generator);
+        }
+        return sprintf("%s %s", $field, $this->_direction);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function traverse(callable $visitor)
+    {
+        if ($this->_field instanceof ExpressionInterface) {
+            $visitor($this->_field);
+            $this->_field->traverse($visitor);
+        }
+    }
+}

+ 57 - 1
src/Database/Query.php

@@ -16,6 +16,7 @@ namespace Cake\Database;
 
 use Cake\Database\Exception;
 use Cake\Database\Expression\OrderByExpression;
+use Cake\Database\Expression\OrderClauseExpression;
 use Cake\Database\Expression\QueryExpression;
 use Cake\Database\Expression\ValuesExpression;
 use Cake\Database\Statement\CallbackStatement;
@@ -930,6 +931,9 @@ class Query implements ExpressionInterface, IteratorAggregate
      *
      * ``ORDER BY (id %2 = 0), title ASC``
      *
+     * If you need to set complex expressions as order conditions, you
+     * should use `orderAsc()` or `orderDesc()`.
+     *
      * @param array|\Cake\Database\ExpressionInterface|string $fields fields to be added to the list
      * @param bool $overwrite whether to reset order with field list or not
      * @return $this
@@ -945,13 +949,65 @@ class Query implements ExpressionInterface, IteratorAggregate
         }
 
         if (!$this->_parts['order']) {
-            $this->_parts['order'] = new OrderByExpression;
+            $this->_parts['order'] = new OrderByExpression();
         }
         $this->_conjugate('order', $fields, '', []);
         return $this;
     }
 
     /**
+     * Add an ORDER BY clause with an ASC direction.
+     *
+     * This method allows you to set complex expressions
+     * as order conditions unlike order()
+     *
+     * @param string|\Cake\Database\QueryExpression $field The field to order on.
+     * @param bool $overwrite Whether or not to reset the order clauses.
+     * @return $this
+     */
+    public function orderAsc($field, $overwrite = false)
+    {
+        if ($overwrite) {
+            $this->_parts['order'] = null;
+        }
+        if (!$field) {
+            return $this;
+        }
+
+        if (!$this->_parts['order']) {
+            $this->_parts['order'] = new OrderByExpression();
+        }
+        $this->_parts['order']->add(new OrderClauseExpression($field, 'ASC'));
+        return $this;
+    }
+
+    /**
+     * Add an ORDER BY clause with an ASC direction.
+     *
+     * This method allows you to set complex expressions
+     * as order conditions unlike order()
+     *
+     * @param string|\Cake\Database\QueryExpression $field The field to order on.
+     * @param bool $overwrite Whether or not to reset the order clauses.
+     * @return $this
+     */
+    public function orderDesc($field, $overwrite = false)
+    {
+        if ($overwrite) {
+            $this->_parts['order'] = null;
+        }
+        if (!$field) {
+            return $this;
+        }
+
+        if (!$this->_parts['order']) {
+            $this->_parts['order'] = new OrderByExpression();
+        }
+        $this->_parts['order']->add(new OrderClauseExpression($field, 'DESC'));
+        return $this;
+    }
+
+    /**
      * Adds a single or multiple fields to be used in the GROUP BY clause for this query.
      * Fields can be passed as an array of strings, array of expression
      * objects, a single expression or a single string.

+ 7 - 1
src/Mailer/Email.php

@@ -1013,10 +1013,16 @@ class Email implements JsonSerializable, Serializable
      */
     protected function _constructTransport($name)
     {
-        if (!isset(static::$_transportConfig[$name]['className'])) {
+        if (!isset(static::$_transportConfig[$name])) {
             throw new InvalidArgumentException(sprintf('Transport config "%s" is missing.', $name));
         }
 
+        if (!isset(static::$_transportConfig[$name]['className'])) {
+            throw new InvalidArgumentException(
+                sprintf('Transport config "%s" is invalid, the required `className` option is missing', $name)
+            );
+        }
+
         $config = static::$_transportConfig[$name];
 
         if (is_object($config['className'])) {

+ 3 - 0
src/Routing/Filter/ControllerFactoryFilter.php

@@ -73,6 +73,9 @@ class ControllerFactoryFilter extends DispatcherFilter
             );
             $namespace .= '/' . implode('/', $prefixes);
         }
+        if (strpos($controller, '\\') !== false || strpos($controller, '.') !== false) {
+            return false;
+        }
         $className = false;
         if ($pluginPath . $controller) {
             $className = App::classname($pluginPath . $controller, $namespace, 'Controller');

+ 3 - 2
src/Routing/Filter/LocaleSelectorFilter.php

@@ -15,6 +15,7 @@
 namespace Cake\Routing\Filter;
 
 use Cake\Event\Event;
+use Cake\I18n\I18n;
 use Cake\Routing\DispatcherFilter;
 use Locale;
 
@@ -48,7 +49,7 @@ class LocaleSelectorFilter extends DispatcherFilter
     }
 
     /**
-     * Inspects the request for the Accept-Language header and sets the default
+     * Inspects the request for the Accept-Language header and sets the
      * Locale for the current runtime if it matches the list of valid locales
      * as passed in the configuration.
      *
@@ -64,6 +65,6 @@ class LocaleSelectorFilter extends DispatcherFilter
             return;
         }
 
-        Locale::setDefault($locale);
+        I18n::locale($locale);
     }
 }

+ 13 - 0
tests/TestCase/Controller/ControllerTest.php

@@ -95,6 +95,16 @@ class TestController extends ControllerTestAppController
     public $modelClass = 'Comments';
 
     /**
+     * beforeFilter handler
+     *
+     * @param \Cake\Event\Event $event
+     * @retun void
+     */
+    public function beforeFilter(Event $event)
+    {
+    }
+
+    /**
      * index method
      *
      * @param mixed $testId
@@ -875,6 +885,9 @@ class ControllerTest extends TestCase
         $this->assertEquals('Admin' . DS . 'Super' . DS . 'Posts', $Controller->getView()->viewPath);
 
         $request = new Request('pages/home');
+        $request->addParams([
+            'prefix' => false
+        ]);
         $Controller = new \TestApp\Controller\PagesController($request, $response);
         $Controller->eventManager()->on('Controller.beforeRender', function (Event $e) {
             return $e->subject()->response;

+ 79 - 0
tests/TestCase/Database/QueryTest.php

@@ -1494,6 +1494,85 @@ class QueryTest extends TestCase
     }
 
     /**
+     * Test orderAsc() and its input types.
+     *
+     * @return void
+     */
+    public function testSelectOrderAsc()
+    {
+        $query = new Query($this->connection);
+        $query->select(['id'])
+            ->from('articles')
+            ->orderAsc('id');
+
+        $sql = $query->sql();
+        $result = $query->execute()->fetchAll('assoc');
+        $expected = [
+            ['id' => 1],
+            ['id' => 2],
+            ['id' => 3],
+        ];
+        $this->assertEquals($expected, $result);
+        $this->assertQuotedQuery(
+            'SELECT <id> FROM <articles> ORDER BY <id> ASC',
+            $sql,
+            !$this->autoQuote
+        );
+
+        $query = new Query($this->connection);
+        $query->select(['id'])
+            ->from('articles')
+            ->orderAsc($query->func()->concat(['id' => 'literal', '3']));
+
+        $result = $query->execute()->fetchAll('assoc');
+        $expected = [
+            ['id' => 1],
+            ['id' => 2],
+            ['id' => 3],
+        ];
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * Test orderDesc() and its input types.
+     *
+     * @return void
+     */
+    public function testSelectOrderDesc()
+    {
+        $query = new Query($this->connection);
+        $query->select(['id'])
+            ->from('articles')
+            ->orderDesc('id');
+        $sql = $query->sql();
+        $result = $query->execute()->fetchAll('assoc');
+        $expected = [
+            ['id' => 3],
+            ['id' => 2],
+            ['id' => 1],
+        ];
+        $this->assertEquals($expected, $result);
+        $this->assertQuotedQuery(
+            'SELECT <id> FROM <articles> ORDER BY <id> DESC',
+            $sql,
+            !$this->autoQuote
+        );
+
+        $query = new Query($this->connection);
+        $query->select(['id'])
+            ->from('articles')
+            ->orderDesc($query->func()->concat(['id' => 'literal', '3']));
+
+        $result = $query->execute()->fetchAll('assoc');
+        $expected = [
+            ['id' => 3],
+            ['id' => 2],
+            ['id' => 1],
+        ];
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
      * Tests that group by fields can be passed similar to select fields
      * and that it sends the correct query to the database
      *

+ 14 - 0
tests/TestCase/Mailer/EmailTest.php

@@ -814,6 +814,20 @@ class EmailTest extends TestCase
     }
 
     /**
+     * Test that using misconfigured transports fails.
+     *
+     * @expectedException \InvalidArgumentException
+     * @expectedExceptionMessage Transport config "debug" is invalid, the required `className` option is missing
+     */
+    public function testTransportMissingClassName()
+    {
+        Email::dropTransport('debug');
+        Email::configTransport('debug', []);
+
+        $this->CakeEmail->transport('debug');
+    }
+
+    /**
      * Test configuring a transport.
      *
      * @return void

+ 26 - 0
tests/TestCase/ORM/QueryRegressionTest.php

@@ -1022,4 +1022,30 @@ class QueryRegressionTest extends TestCase
 
         $this->assertEquals([['name' => 'nate', 'tag' => 'tag1']], $results);
     }
+
+    /**
+     * Test expression based ordering with unions.
+     *
+     * @return void
+     */
+    public function testComplexOrderWithUnion()
+    {
+        $table = TableRegistry::get('Comments');
+        $query = $table->find();
+        $inner = $table->find()
+            ->select(['content' => 'comment'])
+            ->where(['id >' => 3]);
+        $inner2 = $table->find()
+            ->select(['content' => 'comment'])
+            ->where(['id <' => 3]);
+
+        $order = $query->func()->concat(['content' => 'literal', 'test']);
+
+        $query->select(['inside.content'])
+            ->from(['inside' => $inner->unionAll($inner2)])
+            ->orderAsc($order);
+
+        $results = $query->toArray();
+        $this->assertCount(5, $results);
+    }
 }

+ 48 - 1
tests/TestCase/Routing/DispatcherTest.php

@@ -23,7 +23,6 @@ use Cake\Network\Response;
 use Cake\Network\Session;
 use Cake\Routing\Dispatcher;
 use Cake\Routing\Filter\ControllerFactoryFilter;
-use Cake\Routing\Router;
 use Cake\TestSuite\TestCase;
 use Cake\Utility\Inflector;
 
@@ -410,6 +409,54 @@ class DispatcherTest extends TestCase
     }
 
     /**
+     * test forbidden controller names.
+     *
+     * @expectedException \Cake\Routing\Exception\MissingControllerException
+     * @expectedExceptionMessage Controller class TestPlugin.Tests could not be found.
+     * @return void
+     */
+    public function testDispatchBadPluginName()
+    {
+        Plugin::load('TestPlugin');
+
+        $request = new Request([
+            'url' => 'TestPlugin.Tests/index',
+            'params' => [
+                'plugin' => '',
+                'controller' => 'TestPlugin.Tests',
+                'action' => 'index',
+                'pass' => [],
+                'return' => 1
+            ]
+        ]);
+        $response = $this->getMock('Cake\Network\Response');
+        $this->dispatcher->dispatch($request, $response);
+    }
+
+    /**
+     * test forbidden controller names.
+     *
+     * @expectedException \Cake\Routing\Exception\MissingControllerException
+     * @expectedExceptionMessage Controller class TestApp\Controller\PostsController could not be found.
+     * @return void
+     */
+    public function testDispatchBadName()
+    {
+        $request = new Request([
+            'url' => 'TestApp%5CController%5CPostsController/index',
+            'params' => [
+                'plugin' => '',
+                'controller' => 'TestApp\Controller\PostsController',
+                'action' => 'index',
+                'pass' => [],
+                'return' => 1
+            ]
+        ]);
+        $response = $this->getMock('Cake\Network\Response');
+        $this->dispatcher->dispatch($request, $response);
+    }
+
+    /**
      * Test dispatcher filters being called.
      *
      * @return void

+ 6 - 5
tests/TestCase/Routing/Filter/LocaleSelectorFilterTest.php

@@ -15,6 +15,7 @@
 namespace Cake\Test\TestCase\Routing\Filter;
 
 use Cake\Event\Event;
+use Cake\I18n\I18n;
 use Cake\Network\Request;
 use Cake\Routing\Filter\LocaleSelectorFilter;
 use Cake\TestSuite\TestCase;
@@ -63,19 +64,19 @@ class LocaleSelectorFilterTest extends TestCase
             'environment' => ['HTTP_ACCEPT_LANGUAGE' => 'en-GB,en;q=0.8,es;q=0.6,da;q=0.4']
         ]);
         $filter->beforeDispatch(new Event('name', null, ['request' => $request]));
-        $this->assertEquals('en_GB', Locale::getDefault());
+        $this->assertEquals('en_GB', I18n::locale());
 
         $request = new Request([
             'environment' => ['HTTP_ACCEPT_LANGUAGE' => 'es_VE,en;q=0.8,es;q=0.6,da;q=0.4']
         ]);
         $filter->beforeDispatch(new Event('name', null, ['request' => $request]));
-        $this->assertEquals('es_VE', Locale::getDefault());
+        $this->assertEquals('es_VE', I18n::locale());
 
         $request = new Request([
             'environment' => ['HTTP_ACCEPT_LANGUAGE' => 'en;q=0.4,es;q=0.6,da;q=0.8']
         ]);
         $filter->beforeDispatch(new Event('name', null, ['request' => $request]));
-        $this->assertEquals('da', Locale::getDefault());
+        $this->assertEquals('da', I18n::locale());
     }
 
     /**
@@ -97,7 +98,7 @@ class LocaleSelectorFilterTest extends TestCase
             ]
         ]);
         $filter->beforeDispatch(new Event('name', null, ['request' => $request]));
-        $this->assertEquals('es_VE', Locale::getDefault());
+        $this->assertEquals('es_VE', I18n::locale());
 
         Locale::setDefault('en_US');
         $request = new Request([
@@ -106,6 +107,6 @@ class LocaleSelectorFilterTest extends TestCase
             ]
         ]);
         $filter->beforeDispatch(new Event('name', null, ['request' => $request]));
-        $this->assertEquals('en_US', Locale::getDefault());
+        $this->assertEquals('en_US', I18n::locale());
     }
 }