Merge branch 'master' into 2.3

Conflicts:
	lib/Cake/VERSION.txt
This commit is contained in:
mark_story 2012-07-18 22:12:51 -04:00
commit 3c6b50953b
28 changed files with 390 additions and 49 deletions

View file

@ -414,7 +414,7 @@ class ModelTask extends BakeTask {
for ($i = 1, $m = $defaultChoice / 2; $i < $m; $i++) {
$line = sprintf("%2d. %s", $i, $this->_validations[$i]);
$optionText .= $line . str_repeat(" ", 31 - strlen($line));
$optionText .= sprintf("%2d. %s", $m + $i, $this->_validations[$m + $i]);
$optionText .= sprintf("%2d. %s\n", $m + $i, $this->_validations[$m + $i]);
}
$this->out($optionText);
$this->out(__d('cake_console', "%s - Do not do any validation on this field.", $defaultChoice));

View file

@ -165,23 +165,13 @@ class Shell extends Object {
if ($this->stdout == null) {
$this->stdout = new ConsoleOutput('php://stdout');
}
CakeLog::config('stdout', array(
'engine' => 'ConsoleLog',
'types' => array('notice', 'info'),
'stream' => $this->stdout,
));
if ($this->stderr == null) {
$this->stderr = new ConsoleOutput('php://stderr');
}
CakeLog::config('stderr', array(
'engine' => 'ConsoleLog',
'types' => array('emergency', 'alert', 'critical', 'error', 'warning', 'debug'),
'stream' => $this->stderr,
));
if ($this->stdin == null) {
$this->stdin = new ConsoleInput('php://stdin');
}
$this->_useLogger();
$parent = get_parent_class($this);
if ($this->tasks !== null && $this->tasks !== false) {
$this->_mergeVars(array('tasks'), $parent, true);
@ -379,6 +369,10 @@ class Shell extends Object {
return false;
}
if (!empty($this->params['quiet'])) {
$this->_useLogger(false);
}
$this->command = $command;
if (!empty($this->params['help'])) {
return $this->_displayHelp($command);
@ -825,4 +819,29 @@ class Shell extends Object {
return current(App::path('plugins')) . $pluginName . DS;
}
/**
* Used to enable or disable logging stream output to stdout and stderr
* If you don't wish to see in your stdout or stderr everything that is logged
* through CakeLog, call this function with first param as false
*
* @param boolean $enable wheter to enable CakeLog output or not
* @return void
**/
protected function _useLogger($enable = true) {
if (!$enable) {
CakeLog::drop('stdout');
CakeLog::drop('stderr');
return;
}
CakeLog::config('stdout', array(
'engine' => 'ConsoleLog',
'types' => array('notice', 'info'),
'stream' => $this->stdout,
));
CakeLog::config('stderr', array(
'engine' => 'ConsoleLog',
'types' => array('emergency', 'alert', 'critical', 'error', 'warning', 'debug'),
'stream' => $this->stderr,
));
}
}

View file

@ -229,7 +229,7 @@ class SecurityComponent extends Component {
}
}
$this->generateToken($controller->request);
if ($isPost) {
if ($isPost && is_array($controller->request->data)) {
unset($controller->request->data['_Token']);
}
}
@ -585,12 +585,13 @@ class SecurityComponent extends Component {
* @param string $method Method to execute
* @param array $params Parameters to send to method
* @return mixed Controller callback method's response
* @throws BadRequestException When a the blackholeCallback is not callable.
*/
protected function _callback(Controller $controller, $method, $params = array()) {
if (is_callable(array($controller, $method))) {
return call_user_func_array(array(&$controller, $method), empty($params) ? null : $params);
} else {
return null;
throw new BadRequestException(__d('cake_dev', 'The request has been black-holed'));
}
}

View file

@ -49,11 +49,16 @@ class ConsoleLog extends BaseLog {
*/
public function __construct($config = array()) {
parent::__construct($config);
if (DS == '\\' && !(bool)env('ANSICON')) {
$outputAs = ConsoleOutput::PLAIN;
} else {
$outputAs = ConsoleOutput::COLOR;
}
$config = Hash::merge(array(
'stream' => 'php://stderr',
'types' => null,
'scopes' => array(),
'outputAs' => ConsoleOutput::COLOR,
'outputAs' => $outputAs,
), $this->_config);
$config = $this->config($config);
if ($config['stream'] instanceof ConsoleOutput) {

View file

@ -393,6 +393,16 @@ class TranslateBehavior extends ModelBehavior {
$conditions = array('model' => $model->alias, 'foreign_key' => $model->id);
$RuntimeModel = $this->translateModel($model);
$fields = array_merge($this->settings[$model->alias], $this->runtime[$model->alias]['fields']);
if ($created) {
foreach ($fields as $field) {
if (!isset($tempData[$field])) {
//set the field value to an empty string
$tempData[$field] = '';
}
}
}
foreach ($tempData as $field => $value) {
unset($conditions['content']);
$conditions['field'] = $field;

View file

@ -124,11 +124,13 @@ class TreeBehavior extends ModelBehavior {
*/
public function beforeDelete(Model $Model, $cascade = true) {
extract($this->settings[$Model->alias]);
$data = current($Model->find('first', array(
$data = $Model->find('first', array(
'conditions' => array($Model->alias . '.' . $Model->primaryKey => $Model->id),
'fields' => array($Model->alias . '.' . $left, $Model->alias . '.' . $right),
'recursive' => -1)));
$this->_deletedRow = $data;
'recursive' => -1));
if ($data) {
$this->_deletedRow = current($data);
}
return true;
}
@ -369,8 +371,7 @@ class TreeBehavior extends ModelBehavior {
$valuePath = array('%s%s', '{n}.tree_prefix', $valuePath);
} else {
$valuePath[0] = '{' . (count($valuePath) - 1) . '}' . $valuePath[0];
$valuePath[] = '{n}.tree_prefix';
array_unshift($valuePath, '%s' . $valuePath[0], '{n}.tree_prefix');
}
$order = $Model->alias . '.' . $left . ' asc';
$results = $Model->find('all', compact('conditions', 'fields', 'order', 'recursive'));

View file

@ -285,6 +285,7 @@ class BehaviorCollection extends ObjectCollection implements CakeEventListener {
'Model.beforeFind' => 'trigger',
'Model.afterFind' => 'trigger',
'Model.beforeValidate' => 'trigger',
'Model.afterValidate' => 'trigger',
'Model.beforeSave' => 'trigger',
'Model.afterSave' => 'trigger',
'Model.beforeDelete' => 'trigger',

View file

@ -146,6 +146,17 @@ class ModelBehavior extends Object {
return true;
}
/**
* afterValidate is called just after model data was validated, you can use this callback
* to perform any data cleanup or preparation if needed
*
* @param Model $model Model using this behavior
* @return mixed False will stop this event from being passed to other behaviors
*/
public function afterValidate(Model $model) {
return true;
}
/**
* beforeSave is called before a model is saved. Returning false from a beforeSave callback
* will abort the save operation.

View file

@ -175,7 +175,8 @@ class CakeRequest implements ArrayAccess {
if (env('HTTP_X_HTTP_METHOD_OVERRIDE')) {
$this->data['_method'] = env('HTTP_X_HTTP_METHOD_OVERRIDE');
}
if (isset($this->data['_method'])) {
$isArray = is_array($this->data);
if ($isArray && isset($this->data['_method'])) {
if (!empty($_SERVER)) {
$_SERVER['REQUEST_METHOD'] = $this->data['_method'];
} else {
@ -183,8 +184,7 @@ class CakeRequest implements ArrayAccess {
}
unset($this->data['_method']);
}
if (isset($this->data['data'])) {
if ($isArray && isset($this->data['data'])) {
$data = $this->data['data'];
if (count($this->data) <= 1) {
$this->data = $data;

View file

@ -43,7 +43,7 @@ class Dispatcher implements CakeEventListener {
/**
* Event manager, used to handle dispatcher filters
*
* @var CakeEventMaanger
* @var CakeEventManager
*/
protected $_eventManager;
@ -62,7 +62,7 @@ class Dispatcher implements CakeEventListener {
* Returns the CakeEventManager instance or creates one if none was
* creted. Attaches the default listeners and filters
*
* @return CakeEventmanger
* @return CakeEventManager
*/
public function getEventManager() {
if (!$this->_eventManager) {

View file

@ -497,9 +497,9 @@ class CakeRoute {
$named = array();
foreach ($params['named'] as $key => $value) {
if (is_array($value)) {
$flat = Hash::flatten($value, '][');
$flat = Hash::flatten($value, '%5D%5B');
foreach ($flat as $namedKey => $namedValue) {
$named[] = $key . "[$namedKey]" . $separator . rawurlencode($namedValue);
$named[] = $key . "%5B{$namedKey}%5D" . $separator . rawurlencode($namedValue);
}
} else {
$named[] = $key . $separator . rawurlencode($value);

View file

@ -80,6 +80,10 @@ class ShellTestShell extends Shell {
return $this->_mergeVars($properties, $class, $normalize);
}
public function useLogger($enable = true) {
$this->_useLogger($enable);
}
}
/**
@ -840,4 +844,32 @@ TEXT;
$this->assertContains($this->Shell->testMessage, $contents);
}
/**
* Tests that _useLogger works properly
*
* @return void
**/
public function testProtectedUseLogger() {
CakeLog::drop('stdout');
CakeLog::drop('stderr');
$this->Shell->useLogger(true);
$this->assertNotEmpty(CakeLog::stream('stdout'));
$this->assertNotEmpty(CakeLog::stream('stderr'));
$this->Shell->useLogger(false);
$this->assertFalse(CakeLog::stream('stdout'));
$this->assertFalse(CakeLog::stream('stderr'));
}
/**
* Test file and console and logging quiet output
*/
public function testQuietLog() {
$output = $this->getMock('ConsoleOutput', array(), array(), '', false);
$error = $this->getMock('ConsoleOutput', array(), array(), '', false);
$in = $this->getMock('ConsoleInput', array(), array(), '', false);
$this->Shell = $this->getMock('ShellTestShell', array('_useLogger'), array($output, $error, $in));
$this->Shell->expects($this->once())->method('_useLogger')->with(false);
$this->Shell->runCommand('foo', array('--quiet'));
}
}

View file

@ -107,6 +107,20 @@ class SecurityTestController extends Controller {
}
class BrokenCallbackController extends Controller {
public $name = 'UncallableCallback';
public $components = array('Session', 'TestSecurity');
public function index() {
}
protected function _fail() {
}
}
/**
* SecurityComponentTest class
*
@ -161,6 +175,25 @@ class SecurityComponentTest extends CakeTestCase {
unset($this->Controller);
}
/**
* Test that requests are still blackholed when controller has incorrect
* visibility keyword in the blackhole callback
*
* @expectedException BadRequestException
*/
public function testBlackholeWithBrokenCallback() {
$request = new CakeRequest('posts/index', false);
$request->addParams(array(
'controller' => 'posts', 'action' => 'index')
);
$this->Controller = new BrokenCallbackController($request);
$this->Controller->Components->init($this->Controller);
$this->Controller->Security = $this->Controller->TestSecurity;
$this->Controller->Security->blackHoleCallback = '_fail';
$this->Controller->Security->startup($this->Controller);
$this->Controller->Security->blackHole($this->Controller, 'csrf');
}
/**
* test that initialize can set properties.
*

View file

@ -116,4 +116,21 @@ class ConsoleLogTest extends CakeTestCase {
$this->assertContains($message, $logOutput);
}
/**
* test default value of stream 'outputAs'
*/
public function testDefaultOutputAs() {
TestCakeLog::config('test_console_log', array(
'engine' => 'TestConsoleLog',
));
if (DS == '\\' && !(bool)env('ANSICON')) {
$expected = ConsoleOutput::PLAIN;
} else {
$expected = ConsoleOutput::COLOR;
}
$stream = TestCakeLog::stream('test_console_log');
$config = $stream->config();
$this->assertEquals($expected, $config['outputAs']);
}
}

View file

@ -531,6 +531,34 @@ class TranslateBehaviorTest extends CakeTestCase {
$this->assertEquals($expected, $result);
}
/**
* Test that saving only some of the translated fields allows the record to be found again.
*
* @return void
*/
public function testSavePartialFields() {
$this->loadFixtures('Translate', 'TranslatedItem');
$TestModel = new TranslatedItem();
$TestModel->locale = 'spa';
$data = array(
'slug' => 'fourth_translated',
'title' => 'Leyenda #4',
);
$TestModel->create($data);
$TestModel->save();
$result = $TestModel->read();
$expected = array(
'TranslatedItem' => array(
'id' => $TestModel->id,
'translated_article_id' => null,
'locale' => 'spa',
'content' => '',
) + $data
);
$this->assertEquals($expected, $result);
}
/**
* testSaveUpdate method
*

View file

@ -914,6 +914,18 @@ class TreeBehaviorNumberTest extends CakeTestCase {
$this->assertSame($validTree, true);
}
/**
* Test deleting a record that doesn't exist.
*
* @return void
*/
public function testDeleteDoesNotExist() {
extract($this->settings);
$this->Tree = new $modelClass();
$this->Tree->initialize(2, 2);
$this->Tree->delete(99999);
}
/**
* testRemove method
*
@ -1269,6 +1281,26 @@ class TreeBehaviorNumberTest extends CakeTestCase {
$this->assertSame($expected, $result);
}
/**
* Test the formatting options of generateTreeList()
*
* @return void
*/
public function testGenerateTreeListFormatting() {
extract($this->settings);
$this->Tree = new $modelClass();
$this->Tree->initialize(2, 2);
$result = $this->Tree->generateTreeList(
null,
"{n}.$modelClass.id",
array('%s - %s', "{n}.$modelClass.id", "{n}.$modelClass.name")
);
$this->assertEquals('1 - 1. Root', $result[1]);
$this->assertEquals('_2 - 1.1', $result[2]);
$this->assertEquals('__3 - 1.1.1', $result[3]);
}
/**
* testArraySyntax method
*

View file

@ -194,6 +194,29 @@ class TestBehavior extends ModelBehavior {
}
}
/**
* afterValidate method
*
* @param Model $model
* @param bool $cascade
* @return void
*/
public function afterValidate(Model $model) {
$settings = $this->settings[$model->alias];
if (!isset($settings['afterValidate']) || $settings['afterValidate'] == 'off') {
return parent::afterValidate($model);
}
switch ($settings['afterValidate']) {
case 'on':
return false;
break;
case 'test':
$model->data = array('foo');
return true;
break;
}
}
/**
* beforeDelete method
*
@ -966,6 +989,27 @@ class BehaviorCollectionTest extends CakeTestCase {
$this->assertSame($Apple->whitelist, array('unknown', 'name'));
}
/**
* testBehaviorValidateAfterCallback method
*
* @return void
*/
public function testBehaviorValidateAfterCallback() {
$Apple = new Apple();
$Apple->Behaviors->attach('Test');
$this->assertSame($Apple->validates(), true);
$Apple->Behaviors->attach('Test', array('afterValidate' => 'on'));
$this->assertSame($Apple->validates(), true);
$this->assertSame($Apple->validationErrors, array());
$Apple->Behaviors->attach('Test', array('afterValidate' => 'test'));
$Apple->data = array('bar');
$Apple->validates();
$this->assertEquals(array('foo'), $Apple->data);
}
/**
* testBehaviorValidateMethods method
*

View file

@ -73,6 +73,7 @@ class CakeValidationRuleTest extends CakeTestCase {
$Rule->process('fieldName', $data, $methods);
$this->assertTrue($Rule->isValid());
}
/**
* tests that passing custom validation methods work
*
@ -98,6 +99,24 @@ class CakeValidationRuleTest extends CakeTestCase {
$this->assertFalse($Rule->isValid());
}
/**
* Make sure errors are triggered when validation is missing.
*
* @expectedException PHPUnit_Framework_Error_Warning
* @expectedExceptionMessage Could not find validation handler totallyMissing for fieldName
* @return void
*/
public function testCustomMethodMissingError() {
$def = array('rule' => array('totallyMissing'));
$data = array(
'fieldName' => 'some data'
);
$methods = array('mytestrule' => array($this, 'myTestRule'));
$Rule = new CakeValidationRule($def);
$Rule->process('fieldName', $data, $methods);
}
/**
* Test isRequired method
*

View file

@ -300,6 +300,22 @@ class CakeRequestTest extends CakeTestCase {
$this->assertEquals($data, $request->data);
}
/**
* test parsing json PUT data into the object.
*
* @return void
*/
public function testPutParsingJSON() {
$_SERVER['REQUEST_METHOD'] = 'PUT';
$_SERVER['CONTENT_TYPE'] = 'application/json';
$request = $this->getMock('TestCakeRequest', array('_readInput'));
$request->expects($this->at(0))->method('_readInput')
->will($this->returnValue('{Article":["title"]}'));
$request->reConstruct();
$this->assertEquals('{Article":["title"]}', $request->data);
}
/**
* test parsing of FILES array
*

View file

@ -1119,7 +1119,8 @@ class CakeEmailTest extends CakeTestCase {
$this->CakeEmail->template('image');
$this->CakeEmail->emailFormat('html');
$expected = '<img src="http://localhost/img/image.gif" alt="cool image" width="100" height="100" />';
$server = env('SERVER_NAME') ? env('SERVER_NAME') : 'localhost';
$expected = '<img src="http://' . $server . '/img/image.gif" alt="cool image" width="100" height="100" />';
$result = $this->CakeEmail->send();
$this->assertContains($expected, $result['message']);
}

View file

@ -798,7 +798,7 @@ class CakeRouteTest extends CakeTestCase {
)
);
$result = $route->match($url);
$expected = '/posts/index/filter[0]:one/filter[model]:value';
$expected = '/posts/index/filter%5B0%5D:one/filter%5Bmodel%5D:value';
$this->assertEquals($expected, $result);
$url = array(
@ -813,7 +813,7 @@ class CakeRouteTest extends CakeTestCase {
)
);
$result = $route->match($url);
$expected = '/posts/index/filter[0]:one/filter[model][0]:two/filter[model][order]:field';
$expected = '/posts/index/filter%5B0%5D:one/filter%5Bmodel%5D%5B0%5D:two/filter%5Bmodel%5D%5Border%5D:field';
$this->assertEquals($expected, $result);
}

View file

@ -1039,4 +1039,26 @@ XML;
$this->assertContains('mark &amp; mark', $result);
}
/**
* Test that entity loading is disabled by default.
*
* @return void
*/
public function testNoEntityLoading() {
$file = CAKE . 'VERSION.txt';
$xml = <<<XML
<!DOCTYPE cakephp [
<!ENTITY payload SYSTEM "file://$file" >]>
<request>
<xxe>&payload;</xxe>
</request>
XML;
try {
$result = Xml::build($xml);
$this->assertEquals('', (string)$result->xxe);
} catch (Exception $e) {
$this->assertTrue(true, 'A warning was raised meaning external entities were not loaded');
}
}
}

View file

@ -361,7 +361,14 @@ class HtmlHelperTest extends CakeTestCase {
$result = $this->Html->image('test.gif?one=two&three=four');
$this->assertTags($result, array('img' => array('src' => 'img/test.gif?one=two&amp;three=four', 'alt' => '')));
}
/**
* Test that image() works with fullBase and a webroot not equal to /
*
* @return void
*/
public function testImageWithFullBase() {
$result = $this->Html->image('test.gif', array('fullBase' => true));
$here = $this->Html->url('/', true);
$this->assertTags($result, array('img' => array('src' => $here . 'img/test.gif', 'alt' => '')));
@ -369,6 +376,15 @@ class HtmlHelperTest extends CakeTestCase {
$result = $this->Html->image('sub/test.gif', array('fullBase' => true));
$here = $this->Html->url('/', true);
$this->assertTags($result, array('img' => array('src' => $here . 'img/sub/test.gif', 'alt' => '')));
$request = $this->Html->request;
$request->webroot = '/myproject/';
$request->base = '/myproject';
Router::setRequestInfo($request);
$result = $this->Html->image('sub/test.gif', array('fullBase' => true));
$here = $this->Html->url('/', true);
$this->assertTags($result, array('img' => array('src' => $here . 'img/sub/test.gif', 'alt' => '')));
}
/**

View file

@ -194,6 +194,14 @@ class CakeTestFixture {
$db->execute($db->createSchema($this->Schema), array('log' => false));
$this->created[] = $db->configKeyName;
} catch (Exception $e) {
$msg = __d(
'cake_dev',
'Fixture creation for "%s" failed "%s"',
$this->table,
$e->getMessage()
);
CakeLog::error($msg);
trigger_error($msg, E_USER_WARNING);
return false;
}
return true;

View file

@ -201,6 +201,9 @@ class CakeBaseReporter extends PHPUnit_TextUI_ResultPrinter {
*/
public function endTest(PHPUnit_Framework_Test $test, $time) {
$this->numAssertions += $test->getNumAssertions();
if ($test->hasFailed()) {
return;
}
$this->paintPass($test, $time);
}

View file

@ -74,6 +74,8 @@ class Xml {
* ### Options
*
* - `return` Can be 'simplexml' to return object of SimpleXMLElement or 'domdocument' to return DOMDocument.
* - `loadEntities` Defaults to false. Set to true to enable loading of `<!ENTITY` definitions. This
* is disabled by default for security reasons.
* - If using array as input, you can pass `options` from Xml::fromArray.
*
* @param string|array $input XML string, a path to a file, an URL or an array
@ -86,32 +88,50 @@ class Xml {
$options = array('return' => (string)$options);
}
$defaults = array(
'return' => 'simplexml'
'return' => 'simplexml',
'loadEntities' => false,
);
$options = array_merge($defaults, $options);
if (is_array($input) || is_object($input)) {
return self::fromArray((array)$input, $options);
} elseif (strpos($input, '<') !== false) {
if ($options['return'] === 'simplexml' || $options['return'] === 'simplexmlelement') {
return new SimpleXMLElement($input, LIBXML_NOCDATA);
}
$dom = new DOMDocument();
$dom->loadXML($input);
return $dom;
return self::_loadXml($input, $options);
} elseif (file_exists($input) || strpos($input, 'http://') === 0 || strpos($input, 'https://') === 0) {
if ($options['return'] === 'simplexml' || $options['return'] === 'simplexmlelement') {
return new SimpleXMLElement($input, LIBXML_NOCDATA, true);
}
$dom = new DOMDocument();
$dom->load($input);
return $dom;
$input = file_get_contents($input);
return self::_loadXml($input, $options);
} elseif (!is_string($input)) {
throw new XmlException(__d('cake_dev', 'Invalid input.'));
}
throw new XmlException(__d('cake_dev', 'XML cannot be read.'));
}
/**
* Parse the input data and create either a SimpleXmlElement object or a DOMDocument.
*
* @param string $input The input to load.
* @param array $options The options to use. See Xml::build()
* @return SimpleXmlElement|DOMDocument.
*/
protected static function _loadXml($input, $options) {
$hasDisable = function_exists('libxml_disable_entity_loader');
$internalErrors = libxml_use_internal_errors(true);
if ($hasDisable && !$options['loadEntities']) {
libxml_disable_entity_loader(true);
}
if ($options['return'] === 'simplexml' || $options['return'] === 'simplexmlelement') {
$xml = new SimpleXMLElement($input, LIBXML_NOCDATA);
} else {
$xml = new DOMDocument();
$xml->loadXML($input);
}
if ($hasDisable && !$options['loadEntities']) {
libxml_disable_entity_loader(false);
}
libxml_use_internal_errors($internalErrors);
return $xml;
}
/**
* Transform an array into a SimpleXMLElement
*

View file

@ -313,10 +313,12 @@ class Helper extends Object {
$path = h($this->assetTimestamp($this->webroot($path)));
if (!empty($options['fullBase'])) {
if ($path[0] == '/') {
$path = substr($path, 1);
$base = $this->url('/', true);
$len = strlen($this->request->webroot);
if ($len) {
$base = substr($base, 0, -$len);
}
$path = $this->url('/', true) . $path;
$path = $base . $path;
}
}

View file

@ -701,7 +701,7 @@ class PaginatorHelper extends AppHelper {
if ($offset < $start - 1) {
$out .= $this->first($offset, compact('tag', 'separator', 'ellipsis', 'class'));
} else {
$out .= $this->first($offset, compact('tag', 'separator', 'class') + array('after' => $separator));
$out .= $this->first($offset, compact('tag', 'separator', 'class', 'ellipsis') + array('after' => $separator));
}
}
@ -735,7 +735,7 @@ class PaginatorHelper extends AppHelper {
if ($offset <= $last && $params['pageCount'] - $end > $offset) {
$out .= $this->last($offset, compact('tag', 'separator', 'ellipsis', 'class'));
} else {
$out .= $this->last($offset, compact('tag', 'separator', 'class') + array('before' => $separator));
$out .= $this->last($offset, compact('tag', 'separator', 'class', 'ellipsis') + array('before' => $separator));
}
}