diff --git a/CHANGELOG b/CHANGELOG index e3d074252..59ffdd79e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,19 +13,25 @@ Version 1.1a to be released Version 1.0.7 to be released ---------------------------- +- Bug #367: CUploadedFile may fail if a form contains multiple file uploads in different array dimensions (Qiang) +- Bug #368: CUploadedFile::getInstance() should return null if no file is uploaded (Qiang) - Bug #372: CCacheHttpSession should initialize cache first before using it (Qiang) - Bug #388: 'params' options passed to linkButton are not cleared after submit (Qiang) - Bug #393: Greek language code should be 'el' insead of 'gr' (Qiang) +- Bug #402: CNumberFormatter does not format decimals and percentages correctly (Qiang) - Bug #404: AR would fail when CDbLogRoute uses the same DB connection (Qiang) - Bug: CMemCache has a typo when using memcached (Qiang) - Bug: COciCommandBuilder is referencing undefined variable (Qiang) - Bug: yiic webapp may generate incorrect path to yii.php (Qiang) - Bug: SQL with OFFSET generated by command builder for Oracle is incorrect (Qiang) - Bug: yiic shell model command may fail when a foreign key is named as ID (Qiang) +- Bug: yiic shell controller command does not generate correct controller class file when the controller is under a sub-folder (Qiang) - Chg #391: defaultScope should not be applied to UPDATE (Qiang) - New #360: Added anchor parameter to CController::redirect (Qiang) +- New #375: Added support to allow logout a user without cleaning up non-auth session data (Qiang) - New #378: Added support to allow dynamically turning off and on log routes (Qiang) - New #396: Improved error display when running yiic commands (Qiang) +- New #406: Added support to allow stopping saving and deletion by an ActiveRecord behavior (Qiang) - New: Rolled back the change about treating tinyint(1) in MySQL as boolean (Qiang) - New: Added support for displaying call stack information in trace messages (Qiang) - New: Added 'index' option to AR relations so that related objects can be indexed by specific column value (Qiang) diff --git a/UPGRADE b/UPGRADE index a1ccaffad..0ccfcef00 100644 --- a/UPGRADE +++ b/UPGRADE @@ -21,6 +21,9 @@ Upgrading from v1.0.6 DELETE queries. It is only applied to SELECT queries. You should be aware of this change if you override CActiveRecord::defaultScope() in your code. +- The signature of CWebUser::logout() is changed. If you override this method, +you will need to modify your method declaration accordingly. + Upgrading from v1.0.5 --------------------- diff --git a/framework/base/CModelBehavior.php b/framework/base/CModelBehavior.php index a5bf94677..303b9f111 100644 --- a/framework/base/CModelBehavior.php +++ b/framework/base/CModelBehavior.php @@ -36,7 +36,7 @@ class CModelBehavior extends CBehavior /** * Responds to {@link CModel::onBeforeValidate} event. * Overrides this method if you want to handle the corresponding event of the {@link owner}. - * You may set {@link CModelEvent::isValid} to be false if you want to stop the current validation process. + * You may set {@link CModelEvent::isValid} to be false to quit the validation process. * @param CModelEvent event parameter */ public function beforeValidate($event) diff --git a/framework/base/CModelEvent.php b/framework/base/CModelEvent.php index 05def6f46..0782ca27e 100644 --- a/framework/base/CModelEvent.php +++ b/framework/base/CModelEvent.php @@ -22,8 +22,11 @@ class CModelEvent extends CEvent { /** - * @var boolean whether the model is valid. Defaults to true. - * If this is set false, {@link CModel::validate()} will return false and quit the current validation process. + * @var boolean whether the model is in valid status and should continue its normal method execution cycles. Defaults to true. + * For example, when this event is raised in a {@link CFormModel} object when executing {@link CModel::beforeValidate}, + * if this property is false by the event handler, the {@link CModel::validate} method will quit after handling this event. + * If true, the normal execution cycles will continue, including performing the real validations and calling + * {@link CModel::afterValidate}. */ public $isValid=true; } diff --git a/framework/cli/commands/shell/ModelCommand.php b/framework/cli/commands/shell/ModelCommand.php index 9a8e8813c..a73e3e27c 100644 --- a/framework/cli/commands/shell/ModelCommand.php +++ b/framework/cli/commands/shell/ModelCommand.php @@ -378,6 +378,7 @@ EOD; foreach($table->columns as $column) { $label=ucwords(trim(strtolower(str_replace(array('-','_'),' ',preg_replace('/(?name))))); + $label=preg_replace('/\s+/',' ',$label); if(strcasecmp(substr($label,-3),' id')===0) $label=substr($label,0,-3); $labels[]="'{$column->name}'=>'$label'"; diff --git a/framework/cli/views/shell/model/model.php b/framework/cli/views/shell/model/model.php index 5111c03c9..3a10a533e 100644 --- a/framework/cli/views/shell/model/model.php +++ b/framework/cli/views/shell/model/model.php @@ -15,7 +15,7 @@ class extends CActiveRecord { /** - * The followings are the available columns in table '': + * The followings are the available columns in table '': * @var type.' $'.$column->name."\n"; ?> diff --git a/framework/db/ar/CActiveRecord.php b/framework/db/ar/CActiveRecord.php index 4274607d8..55a02130f 100644 --- a/framework/db/ar/CActiveRecord.php +++ b/framework/db/ar/CActiveRecord.php @@ -750,8 +750,9 @@ abstract class CActiveRecord extends CModel */ protected function beforeSave() { - $this->onBeforeSave(new CEvent($this)); - return true; + $event=new CModelEvent($this); + $this->onBeforeSave($event); + return $event->isValid; } /** @@ -774,8 +775,9 @@ abstract class CActiveRecord extends CModel */ protected function beforeDelete() { - $this->onBeforeDelete(new CEvent($this)); - return true; + $event=new CModelEvent($this); + $this->onBeforeDelete($event); + return $event->isValid; } /** diff --git a/framework/db/ar/CActiveRecordBehavior.php b/framework/db/ar/CActiveRecordBehavior.php index 4ccbc21f8..3df3a799b 100644 --- a/framework/db/ar/CActiveRecordBehavior.php +++ b/framework/db/ar/CActiveRecordBehavior.php @@ -41,7 +41,8 @@ class CActiveRecordBehavior extends CModelBehavior /** * Responds to {@link CActiveRecord::onBeforeSave} event. * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. - * @param CEvent event parameter + * You may set {@link CModelEvent::isValid} to be false to quit the saving process. + * @param CModelEvent event parameter */ public function beforeSave($event) { @@ -50,7 +51,7 @@ class CActiveRecordBehavior extends CModelBehavior /** * Responds to {@link CActiveRecord::onAfterSave} event. * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. - * @param CEvent event parameter + * @param CModelEvent event parameter */ public function afterSave($event) { @@ -59,6 +60,7 @@ class CActiveRecordBehavior extends CModelBehavior /** * Responds to {@link CActiveRecord::onBeforeDelete} event. * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. + * You may set {@link CModelEvent::isValid} to be false to quit the deletion process. * @param CEvent event parameter */ public function beforeDelete($event) diff --git a/framework/i18n/CNumberFormatter.php b/framework/i18n/CNumberFormatter.php index fab4c40f2..716bd566f 100644 --- a/framework/i18n/CNumberFormatter.php +++ b/framework/i18n/CNumberFormatter.php @@ -138,8 +138,9 @@ class CNumberFormatter extends CComponent * @param array format with the following structure: *
* array(
- * 'decimalDigits'=>2, // number of required digits after decimal point; if -1, it means we should drop decimal point
- * 'integerDigits'=>1, // number of required digits before decimal point
+ * 'decimalDigits'=>2, // number of required digits after decimal point; 0s will be padded if not enough digits; if -1, it means we should drop decimal point
+ * 'maxDecimalDigits'=>3, // maximum number of digits after decimal point. Additional digits will be truncated.
+ * 'integerDigits'=>1, // number of required digits before decimal point; 0s will be padded if not enough digits
* 'groupSize1'=>3, // the primary grouping size; if 0, it means no grouping
* 'groupSize2'=>0, // the secondary grouping size; if 0, it means no secondary grouping
* 'positivePrefix'=>'+', // prefix to positive number
@@ -156,17 +157,24 @@ class CNumberFormatter extends CComponent
{
$negative=$value<0;
$value=abs($value*$format['multiplier']);
- if($format['decimalDigits']>=0)
- $value=round($value,$format['decimalDigits']);
- list($integer,$decimal)=explode('.',sprintf('%F',$value));
-
- if($format['decimalDigits']>=0)
+ if($format['maxDecimalDigits']>=0)
+ $value=round($value,$format['maxDecimalDigits']);
+ $value="$value";
+ if(($pos=strpos($value,'.'))!==false)
{
- $decimal=rtrim(substr($decimal,0,$format['decimalDigits']),'0');
- $decimal=$this->_locale->getNumberSymbol('decimal').str_pad($decimal,$format['decimalDigits'],'0');
+ $integer=substr($value,0,$pos);
+ $decimal=substr($value,$pos+1);
}
else
+ {
+ $integer=$value;
$decimal='';
+ }
+
+ if($format['decimalDigits']>strlen($decimal))
+ $decimal=str_pad($decimal,$format['decimalDigits'],'0');
+ if(strlen($decimal)>0)
+ $decimal=$this->_locale->getNumberSymbol('decimal').$decimal;
$integer=str_pad($integer,$format['integerDigits'],'0',STR_PAD_LEFT);
if($format['groupSize1']>0 && strlen($integer)>$format['groupSize1'])
@@ -202,13 +210,22 @@ class CNumberFormatter extends CComponent
// find out prefix and suffix for positive and negative patterns
$patterns=explode(';',$pattern);
- list($format['positivePrefix'],$format['positiveSuffix'])=preg_split('/[#,\.0]+/',$patterns[0]);
- if(isset($patterns[1])) // with a negative pattern
- list($format['negativePrefix'],$format['negativeSuffix'])=preg_split('/[#,\.0]+/',$patterns[1]);
+ $format['positivePrefix']=$format['positiveSuffix']=$format['negativePrefix']=$format['negativeSuffix']='';
+ if(preg_match('/^(.*?)[#,\.0]+(.*?)$/',$patterns[0],$matches))
+ {
+ $format['positivePrefix']=$matches[1];
+ $format['positiveSuffix']=$matches[2];
+ }
+
+ if(isset($patterns[1]) && preg_match('/^(.*?)[#,\.0]+(.*?)$/',$patterns[1],$matches)) // with a negative pattern
+ {
+ $format['negativePrefix']=$matches[1];
+ $format['negativeSuffix']=$matches[2];
+ }
else
{
- $format['negativePrefix']=$this->_locale->getNumberSymbol('minusSign');
- $format['negativeSuffix']='';
+ $format['negativePrefix']=$this->_locale->getNumberSymbol('minusSign').$format['positivePrefix'];
+ $format['negativeSuffix']=$format['positiveSuffix'];
}
$pattern=$patterns[0];
@@ -227,21 +244,30 @@ class CNumberFormatter extends CComponent
$format['decimalDigits']=$pos2-$pos;
else
$format['decimalDigits']=0;
+ if(($pos3=strrpos($pattern,'#'))>=$pos2)
+ $format['maxDecimalDigits']=$pos3-$pos;
+ else
+ $format['maxDecimalDigits']=$format['decimalDigits'];
$pattern=substr($pattern,0,$pos);
}
else // no decimal part
- $format['decimalDigits']=-1; // do not display decimal point
+ {
+ $format['decimalDigits']=0;
+ $format['maxDecimalDigits']=0;
+ }
// find out things about integer part
- if(($pos=strpos($pattern,'0'))!==false)
- $format['integerDigits']=strlen(str_replace(',','',substr($pattern,$pos)));
+ $p=str_replace(',','',$pattern);
+ if(($pos=strpos($p,'0'))!==false)
+ $format['integerDigits']=strrpos($p,'0')-$pos+1;
else
$format['integerDigits']=0;
// find out group sizes. some patterns may have two different group sizes
+ $p=str_replace('#','0',$pattern);
if(($pos=strrpos($pattern,','))!==false)
{
- $format['groupSize1']=strlen($pattern)-$pos-1;
- if(($pos2=strrpos(substr($pattern,0,$pos),','))!==false)
+ $format['groupSize1']=strrpos($p,'0')-$pos;
+ if(($pos2=strrpos(substr($p,0,$pos),','))!==false)
$format['groupSize2']=$pos-$pos2-1;
else
$format['groupSize2']=0;
diff --git a/framework/logging/CLogRouter.php b/framework/logging/CLogRouter.php
index faf11b2cd..767721372 100644
--- a/framework/logging/CLogRouter.php
+++ b/framework/logging/CLogRouter.php
@@ -73,7 +73,7 @@ class CLogRouter extends CApplicationComponent
*/
public function getRoutes()
{
- return $this->_routes;
+ return new CMap($this->_routes);
}
/**
@@ -98,7 +98,7 @@ class CLogRouter extends CApplicationComponent
public function collectLogs($param)
{
$logger=Yii::getLogger();
- foreach($this->getRoutes() as $route)
+ foreach($this->_routes as $route)
{
if($route->enabled)
$route->collectLogs($logger);
diff --git a/framework/web/CUploadedFile.php b/framework/web/CUploadedFile.php
index 9fb2a9d89..7eb0145aa 100644
--- a/framework/web/CUploadedFile.php
+++ b/framework/web/CUploadedFile.php
@@ -66,20 +66,16 @@ class CUploadedFile extends CComponent
{
if(is_array($info['name']))
{
- $first=reset($info['name']);
$keys=array_keys($info['name']);
- if(is_array($first))
+ foreach($keys as $key)
{
- foreach($keys as $key)
+ if(is_array($info['name'][$key]))
{
$subKeys=array_keys($info['name'][$key]);
foreach($subKeys as $subKey)
$files["{$class}[{$key}][{$subKey}]"]=new CUploadedFile($info['name'][$key][$subKey],$info['tmp_name'][$key][$subKey],$info['type'][$key][$subKey],$info['size'][$key][$subKey],$info['error'][$key][$subKey]);
}
- }
- else
- {
- foreach($keys as $key)
+ else
$files["{$class}[{$key}]"]=new CUploadedFile($info['name'][$key],$info['tmp_name'][$key],$info['type'][$key],$info['size'][$key],$info['error'][$key]);
}
}
@@ -89,7 +85,7 @@ class CUploadedFile extends CComponent
}
}
- return isset($files[$name]) ? $files[$name] : null;
+ return isset($files[$name]) && $files[$name]->getError()!=UPLOAD_ERR_NO_FILE ? $files[$name] : null;
}
/**
diff --git a/framework/web/auth/CWebUser.php b/framework/web/auth/CWebUser.php
index 9dc075fbb..3f3141f59 100644
--- a/framework/web/auth/CWebUser.php
+++ b/framework/web/auth/CWebUser.php
@@ -52,6 +52,7 @@ class CWebUser extends CApplicationComponent implements IWebUser
{
const FLASH_KEY_PREFIX='Yii.CWebUser.flash.';
const FLASH_COUNTERS='Yii.CWebUser.flash.counters';
+ const STATES_VAR='__states';
/**
* @var boolean whether to enable cookie-based login. Defaults to false.
@@ -186,13 +187,21 @@ class CWebUser extends CApplicationComponent implements IWebUser
/**
* Logs out the current user.
- * The session will be destroyed.
+ * This will remove authentication-related session data.
+ * If the parameter is true, the whole session will be destroyed as well.
+ * @param boolean whether to destroy the whole session. Defaults to true. If false,
+ * then {@link clearStates} will be called, which removes only the data stored via {@link setState}.
+ * This parameter has been available since version 1.0.7. Before 1.0.7, the behavior
+ * is to destroy the whole session.
*/
- public function logout()
+ public function logout($destroySession=true)
{
if($this->allowAutoLogin)
Yii::app()->getRequest()->getCookies()->remove($this->getStateKeyPrefix());
- $this->clearStates();
+ if($destroySession)
+ Yii::app()->getSession()->destroy();
+ else
+ $this->clearStates();
}
/**
@@ -226,7 +235,7 @@ class CWebUser extends CApplicationComponent implements IWebUser
*/
public function getName()
{
- if(($name=$this->getState('_name'))!==null)
+ if(($name=$this->getState('__name'))!==null)
return $name;
else
return $this->guestName;
@@ -239,7 +248,7 @@ class CWebUser extends CApplicationComponent implements IWebUser
*/
public function setName($value)
{
- $this->setState('_name',$value);
+ $this->setState('__name',$value);
}
/**
@@ -251,7 +260,7 @@ class CWebUser extends CApplicationComponent implements IWebUser
*/
public function getReturnUrl()
{
- return $this->getState('_returnUrl',Yii::app()->getRequest()->getScriptUrl());
+ return $this->getState('__returnUrl',Yii::app()->getRequest()->getScriptUrl());
}
/**
@@ -259,7 +268,7 @@ class CWebUser extends CApplicationComponent implements IWebUser
*/
public function setReturnUrl($value)
{
- $this->setState('_returnUrl',$value);
+ $this->setState('__returnUrl',$value);
}
/**
@@ -418,11 +427,18 @@ class CWebUser extends CApplicationComponent implements IWebUser
/**
* Clears all user identity information from persistent storage.
- * The default implementation simply destroys the session.
+ * This will remove the data stored via {@link setState}.
*/
public function clearStates()
{
- Yii::app()->getSession()->destroy();
+ $keys=array_keys($_SESSION);
+ $prefix=$this->getStateKeyPrefix();
+ $n=strlen($prefix);
+ foreach($keys as $key)
+ {
+ if(!strncmp($key,$prefix,$n))
+ unset($_SESSION[$key]);
+ }
}
/**
@@ -495,7 +511,7 @@ class CWebUser extends CApplicationComponent implements IWebUser
protected function saveIdentityStates()
{
$states=array();
- foreach($this->getState('__states',array()) as $name=>$dummy)
+ foreach($this->getState(self::STATES_VAR,array()) as $name=>$dummy)
$states[$name]=$this->getState($name);
return $states;
}
@@ -506,18 +522,16 @@ class CWebUser extends CApplicationComponent implements IWebUser
*/
protected function loadIdentityStates($states)
{
+ $names=array();
if(is_array($states))
{
- $names=array();
foreach($states as $name=>$value)
{
$this->setState($name,$value);
$names[$name]=true;
}
- $this->setState('__states',$names);
}
- else
- $this->setState('__states',array());
+ $this->setState(self::STATES_VAR,$names);
}
/**
diff --git a/tests/unit/framework/i18n/CNumberFormatterTest.php b/tests/unit/framework/i18n/CNumberFormatterTest.php
new file mode 100644
index 000000000..e1fefd825
--- /dev/null
+++ b/tests/unit/framework/i18n/CNumberFormatterTest.php
@@ -0,0 +1,70 @@
+usFormatter=new CNumberFormatter('en_us');
+ $this->deFormatter=new CNumberFormatter('de');
+ }
+
+ public function testFormatCurrency()
+ {
+ $numbers=array(
+ array(0, '$0.00', '0,00 $'),
+ array(100, '$100.00', '100,00 $'),
+ array(-100, '($100.00)', '−100,00 $'),
+ array(100.123, '$100.12', '100,12 $'),
+ array(100.1, '$100.10', '100,10 $'),
+ array(100.126, '$100.13', '100,13 $'),
+ array(1000.126, '$1,000.13', '1.000,13 $'),
+ array(1000000.123, '$1,000,000.12', '1.000.000,12 $'),
+ );
+
+ foreach($numbers as $number)
+ {
+ $this->assertEquals($number[1],$this->usFormatter->formatCurrency($number[0],'USD'));
+ $this->assertEquals($number[2],$this->deFormatter->formatCurrency($number[0],'USD'));
+ }
+ }
+
+ public function testFormatDecimal()
+ {
+ $numbers=array(
+ array(0, '0', '0'),
+ array(100, '100', '100'),
+ array(-100, '-100', '−100'),
+ array(100.123, '100.123', '100,123'),
+ array(100.1, '100.1', '100,1'),
+ array(100.1206, '100.121', '100,121'),
+ array(1000.1206, '1,000.121', '1.000,121'),
+ array(1000000.123, '1,000,000.123', '1.000.000,123'),
+ );
+
+ foreach($numbers as $number)
+ {
+ $this->assertEquals($number[1],$this->usFormatter->formatDecimal($number[0]));
+ $this->assertEquals($number[2],$this->deFormatter->formatDecimal($number[0]));
+ }
+ }
+
+ public function testFormatPercentage()
+ {
+ $numbers=array(
+ array(0, '0%', '0 %'),
+ array(0.123, '12%', '12 %'),
+ array(-0.123, '-12%', '−12 %'),
+ array(10.12, '1,012%', '1.012 %'),
+ array(10000.1, '1,000,010%', '1.000.010 %'),
+ );
+
+ foreach($numbers as $number)
+ {
+ $this->assertEquals($number[1],$this->usFormatter->formatPercentage($number[0]));
+ $this->assertEquals($number[2],$this->deFormatter->formatPercentage($number[0]));
+ }
+ }
+}