diff --git a/framework/YiiBase.php b/framework/YiiBase.php index 00694369219..9014aac2da2 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -80,7 +80,7 @@ class YiiBase */ public static function getVersion() { - return '1.1.16'; + return '1.1.17'; } /** @@ -434,7 +434,7 @@ public static function autoload($className,$classMapOnly=false) else // class name with namespace in PHP 5.3 { $namespace=str_replace('\\','.',ltrim($className,'\\')); - if(($path=self::getPathOfAlias($namespace))!==false) + if(($path=self::getPathOfAlias($namespace))!==false && is_file($path.'.php')) include($path.'.php'); else return false; @@ -652,6 +652,7 @@ public static function registerAutoloader($callback, $append=false) 'CApplicationComponent' => '/base/CApplicationComponent.php', 'CBehavior' => '/base/CBehavior.php', 'CComponent' => '/base/CComponent.php', + 'CDbStatePersister' => '/base/CDbStatePersister.php', 'CErrorEvent' => '/base/CErrorEvent.php', 'CErrorHandler' => '/base/CErrorHandler.php', 'CException' => '/base/CException.php', diff --git a/framework/base/CApplication.php b/framework/base/CApplication.php index bb2e409c8e9..8c44e27fb65 100644 --- a/framework/base/CApplication.php +++ b/framework/base/CApplication.php @@ -73,6 +73,7 @@ * @property CPhpMessageSource $coreMessages The core message translations. * @property CMessageSource $messages The application message translations. * @property CHttpRequest $request The request component. + * @property CFormatter $format The formatter component. * @property CUrlManager $urlManager The URL manager component. * @property CController $controller The currently active controller. Null is returned in this base class. * @property string $baseUrl The relative URL for the application. @@ -530,6 +531,15 @@ public function getUrlManager() return $this->getComponent('urlManager'); } + /** + * Returns the formatter component. + * @return CFormatter the formatter component + */ + public function getFormat() + { + return $this->getComponent('format'); + } + /** * @return CController the currently active controller. Null is returned in this base class. * @since 1.1.8 diff --git a/framework/base/CErrorHandler.php b/framework/base/CErrorHandler.php index f6b0ca54121..73227140ca5 100644 --- a/framework/base/CErrorHandler.php +++ b/framework/base/CErrorHandler.php @@ -556,7 +556,7 @@ protected function getHttpHeader($httpCode, $replacement='') 100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', - 118 => 'Connection timed out', + 118 => 'Connection Timed Out', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', @@ -573,7 +573,7 @@ protected function getHttpHeader($httpCode, $replacement='') 304 => 'Not Modified', 305 => 'Use Proxy', 307 => 'Temporary Redirect', - 310 => 'Too many Redirect', + 310 => 'Too Many Redirect', 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', @@ -582,7 +582,7 @@ protected function getHttpHeader($httpCode, $replacement='') 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', - 408 => 'Request Time-out', + 408 => 'Request Timeout', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', @@ -590,24 +590,30 @@ protected function getHttpHeader($httpCode, $replacement='') 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Long', 415 => 'Unsupported Media Type', - 416 => 'Requested range unsatisfiable', - 417 => 'Expectation failed', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', 418 => 'I’m a teapot', 422 => 'Unprocessable entity', 423 => 'Locked', 424 => 'Method failure', 425 => 'Unordered Collection', 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', 449 => 'Retry With', 450 => 'Blocked by Windows Parental Controls', + 451 => 'Unavailable For Legal Reasons', 500 => 'Internal Server Error', 501 => 'Not Implemented', - 502 => 'Bad Gateway ou Proxy Error', + 502 => 'Bad Gateway', 503 => 'Service Unavailable', - 504 => 'Gateway Time-out', - 505 => 'HTTP Version not supported', - 507 => 'Insufficient storage', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 507 => 'Insufficient Storage', 509 => 'Bandwidth Limit Exceeded', + 510 => 'Not Extended', + 511 => 'Network Authentication Required', ); if(isset($httpCodes[$httpCode])) return $httpCodes[$httpCode]; diff --git a/framework/base/CSecurityManager.php b/framework/base/CSecurityManager.php index a285ceb0048..6f339c6da11 100644 --- a/framework/base/CSecurityManager.php +++ b/framework/base/CSecurityManager.php @@ -59,6 +59,7 @@ class CSecurityManager extends CApplicationComponent /** * @var boolean if encryption key should be validated + * @deprecated */ public $validateEncryptionKey=true; @@ -95,6 +96,7 @@ class CSecurityManager extends CApplicationComponent private $_encryptionKey; private $_mbstring; + public function init() { parent::init(); @@ -527,18 +529,20 @@ protected function validateEncryptionKey($key) { if(is_string($key)) { - $supportedKeyLengths=mcrypt_module_get_supported_key_sizes($this->cryptAlgorithm); + $cryptAlgorithm = is_array($this->cryptAlgorithm) ? $this->cryptAlgorithm[0] : $this->cryptAlgorithm; + + $supportedKeyLengths=mcrypt_module_get_supported_key_sizes($cryptAlgorithm); if($supportedKeyLengths) { if(!in_array($this->strlen($key),$supportedKeyLengths)) { - throw new CException(Yii::t('yii','Encryption key length can be {keyLengths}',array('{keyLengths}'=>implode(',',$supportedKeyLengths).'.'))); + throw new CException(Yii::t('yii','Encryption key length can be {keyLengths}.',array('{keyLengths}'=>implode(',',$supportedKeyLengths)))); } } - elseif(isset(self::$encryptionKeyMinimumLengths[$this->cryptAlgorithm])) + elseif(isset(self::$encryptionKeyMinimumLengths[$cryptAlgorithm])) { - $minLength=self::$encryptionKeyMinimumLengths[$this->cryptAlgorithm]; - $maxLength=mcrypt_module_get_algo_key_size($this->cryptAlgorithm); + $minLength=self::$encryptionKeyMinimumLengths[$cryptAlgorithm]; + $maxLength=mcrypt_module_get_algo_key_size($cryptAlgorithm); if($this->strlen($key)<$minLength || $this->strlen($key)>$maxLength) throw new CException(Yii::t('yii','Encryption key length must be between {minLength} and {maxLength}.',array('{minLength}'=>$minLength,'{maxLength}'=>$maxLength))); } @@ -566,6 +570,7 @@ public function legacyDecrypt($data,$key=null,$cipher='des') $key=Yii::app()->getGlobalState(self::STATE_ENCRYPTION_KEY); if(!$key) throw new CException(Yii::t('yii','No encryption key specified.')); + $key = md5($key); } if(extension_loaded('mcrypt')) @@ -581,7 +586,7 @@ public function legacyDecrypt($data,$key=null,$cipher='des') else throw new CException(Yii::t('yii','CSecurityManager requires PHP mcrypt extension to be loaded in order to use data encryption feature.')); - $derivedKey=$this->substr(md5($key),0,mcrypt_enc_get_key_size($module)); + $derivedKey=$this->substr($key,0,mcrypt_enc_get_key_size($module)); $ivSize=mcrypt_enc_get_iv_size($module); $iv=$this->substr($data,0,$ivSize); mcrypt_generic_init($module,$derivedKey,$iv); diff --git a/framework/base/CStatePersister.php b/framework/base/CStatePersister.php index b2987229277..86e3d9d2ef3 100644 --- a/framework/base/CStatePersister.php +++ b/framework/base/CStatePersister.php @@ -33,7 +33,6 @@ * * CStatePersister is a core application component used to store global application state. * It may be accessed via {@link CApplication::getStatePersister()}. - * page state persistent method based on cache. * * @author Qiang Xue * @package system.base @@ -82,20 +81,46 @@ public function load() $cacheKey='Yii.CStatePersister.'.$stateFile; if(($value=$cache->get($cacheKey))!==false) return unserialize($value); - elseif(($content=@file_get_contents($stateFile))!==false) + else { - $cache->set($cacheKey,$content,0,new CFileCacheDependency($stateFile)); - return unserialize($content); + if(($content=$this->getContent($stateFile))!==false) + { + $unserialized_content=unserialize($content); + // If it can't be unserialized, don't cache it: + if ($unserialized_content!==false || $content=="") + $cache->set($cacheKey,$content,0,new CFileCacheDependency($stateFile)); + return $unserialized_content; + } + else + return null; } - else - return null; } - elseif(($content=@file_get_contents($stateFile))!==false) + elseif(($content=$this->getContent($stateFile))!==false) return unserialize($content); else return null; } - + + /** + * Loads content from file using a shared lock to avoid data corruption when reading + * the file while it is being written by save() + * + * @return string file contents + * @since 1.1.17 + */ + protected function getContent($filename) + { + $file=@fopen($filename,"r"); + if($file && flock($file,LOCK_SH)) + { + $contents=@file_get_contents($filename); + flock($file,LOCK_UN); + fclose($file); + return $contents; + } + return false; + } + /** * Saves application state in persistent storage. * @param mixed $state state data (must be serializable). diff --git a/framework/caching/CApcCache.php b/framework/caching/CApcCache.php index 0fbbb1f20e8..0a8f56cc836 100644 --- a/framework/caching/CApcCache.php +++ b/framework/caching/CApcCache.php @@ -22,6 +22,16 @@ */ class CApcCache extends CCache { + /** + * @var boolean whether to use apcu or apc as the underlying caching extension. + * If true {@link http://pecl.php.net/package/apcu apcu} will be used. + * If false {@link http://pecl.php.net/package/apc apc}. will be used. + * Defaults to false. + * @since 1.1.17 + */ + public $useApcu=false; + + /** * Initializes this application component. * This method is required by the {@link IApplicationComponent} interface. @@ -31,8 +41,10 @@ class CApcCache extends CCache public function init() { parent::init(); - if(!extension_loaded('apc')) - throw new CException(Yii::t('yii','CApcCache requires PHP apc extension to be loaded.')); + $extension=$this->useApcu ? 'apcu' : 'apc'; + if(!extension_loaded($extension)) + throw new CException(Yii::t('yii',"CApcCache requires PHP {extension} extension to be loaded.", + array('{extension}'=>$extension))); } /** @@ -43,7 +55,7 @@ public function init() */ protected function getValue($key) { - return apc_fetch($key); + return $this->useApcu ? apcu_fetch($key) : apc_fetch($key); } /** @@ -53,7 +65,7 @@ protected function getValue($key) */ protected function getValues($keys) { - return apc_fetch($keys); + return $this->useApcu ? apcu_fetch($keys) : apc_fetch($keys); } /** @@ -67,7 +79,7 @@ protected function getValues($keys) */ protected function setValue($key,$value,$expire) { - return apc_store($key,$value,$expire); + return $this->useApcu ? apcu_store($key,$value,$expire) : apc_store($key,$value,$expire); } /** @@ -81,7 +93,7 @@ protected function setValue($key,$value,$expire) */ protected function addValue($key,$value,$expire) { - return apc_add($key,$value,$expire); + return $this->useApcu ? apcu_add($key,$value,$expire) : apc_add($key,$value,$expire); } /** @@ -92,7 +104,7 @@ protected function addValue($key,$value,$expire) */ protected function deleteValue($key) { - return apc_delete($key); + return $this->useApcu ? apcu_delete($key) : apc_delete($key); } /** @@ -103,9 +115,6 @@ protected function deleteValue($key) */ protected function flushValues() { - if(extension_loaded('apcu')) - return apc_clear_cache(); - - return apc_clear_cache('user'); + return $this->useApcu ? apcu_clear_cache() : apc_clear_cache('user'); } } diff --git a/framework/caching/CMemCache.php b/framework/caching/CMemCache.php index 34e05de4039..148298e2aeb 100644 --- a/framework/caching/CMemCache.php +++ b/framework/caching/CMemCache.php @@ -114,7 +114,7 @@ public function getMemCache() $extension=$this->useMemcached ? 'memcached' : 'memcache'; if(!extension_loaded($extension)) throw new CException(Yii::t('yii',"CMemCache requires PHP {extension} extension to be loaded.", - array('{extension}'=>$extension))); + array('{extension}'=>$extension))); return $this->_cache=$this->useMemcached ? new Memcached : new Memcache; } } diff --git a/framework/caching/CRedisCache.php b/framework/caching/CRedisCache.php index 56b347a62a9..3d19b84077e 100644 --- a/framework/caching/CRedisCache.php +++ b/framework/caching/CRedisCache.php @@ -30,6 +30,7 @@ * 'hostname'=>'localhost', * 'port'=>6379, * 'database'=>0, + * 'options'=>STREAM_CLIENT_CONNECT, * ), * ), * ) @@ -59,6 +60,11 @@ class CRedisCache extends CCache * @var int the redis database to use. This is an integer value starting from 0. Defaults to 0. */ public $database=0; + /** + * @var int the options to pass to the flags parameter of stream_socket_client when connecting to the redis server. Defaults to STREAM_CLIENT_CONNECT. + * @see http://php.net/manual/en/function.stream-socket-client.php + */ + public $options=STREAM_CLIENT_CONNECT; /** * @var float timeout to use for connection to redis. If not set the timeout set in php.ini will be used: ini_get("default_socket_timeout") */ @@ -79,7 +85,8 @@ protected function connect() $this->hostname.':'.$this->port, $errorNumber, $errorDescription, - $this->timeout ? $this->timeout : ini_get("default_socket_timeout") + $this->timeout ? $this->timeout : ini_get("default_socket_timeout"), + $this->options ); if ($this->_socket) { @@ -118,7 +125,7 @@ public function executeCommand($name,$params=array()) array_unshift($params,$name); $command='*'.count($params)."\r\n"; foreach($params as $arg) - $command.='$'.strlen($arg)."\r\n".$arg."\r\n"; + $command.='$'.$this->byteLength($arg)."\r\n".$arg."\r\n"; fwrite($this->_socket,$command); @@ -155,7 +162,7 @@ private function parseResponse() if(($block=fread($this->_socket,$length))===false) throw new CException('Failed reading data from redis connection socket.'); $data.=$block; - $length-=(function_exists('mb_strlen') ? mb_strlen($block,'8bit') : strlen($block)); + $length-=$this->byteLength($block); } return substr($data,0,-2); case '*': // Multi-bulk replies @@ -169,6 +176,17 @@ private function parseResponse() } } + /** + * Counting amount of bytes in a string. + * + * @param string $str + * @return int + */ + private function byteLength($str) + { + return function_exists('mb_strlen') ? mb_strlen($str, '8bit') : strlen($str); + } + /** * Retrieves a value from cache with a specified key. * This is the implementation of the method declared in the parent class. diff --git a/framework/cli/views/webapp/protected/config/main.php b/framework/cli/views/webapp/protected/config/main.php index 052c5eca9ac..777f285ad9e 100644 --- a/framework/cli/views/webapp/protected/config/main.php +++ b/framework/cli/views/webapp/protected/config/main.php @@ -55,7 +55,7 @@ 'errorHandler'=>array( // use 'site/error' action to display errors - 'errorAction'=>'site/error', + 'errorAction'=>YII_DEBUG ? null : 'site/error', ), 'log'=>array( diff --git a/framework/collections/CListIterator.php b/framework/collections/CListIterator.php index 8f2f91b0699..79fe13d61eb 100644 --- a/framework/collections/CListIterator.php +++ b/framework/collections/CListIterator.php @@ -27,10 +27,6 @@ class CListIterator implements Iterator * @var integer index of the current item */ private $_i; - /** - * @var integer count of the data items - */ - private $_c; /** * Constructor. @@ -40,7 +36,6 @@ public function __construct(&$data) { $this->_d=&$data; $this->_i=0; - $this->_c=count($this->_d); } /** @@ -88,6 +83,6 @@ public function next() */ public function valid() { - return $this->_i<$this->_c; + return $this->_i_d); } } diff --git a/framework/console/CConsoleCommand.php b/framework/console/CConsoleCommand.php index d29c4a0a980..10bdecca435 100644 --- a/framework/console/CConsoleCommand.php +++ b/framework/console/CConsoleCommand.php @@ -295,14 +295,14 @@ public function getOptionHelp() { $options=array(); $class=new ReflectionClass(get_class($this)); - foreach($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) - { - $name=$method->getName(); - if(!strncasecmp($name,'action',6) && strlen($name)>6) - { - $name=substr($name,6); - $name[0]=strtolower($name[0]); - $help=$name; + foreach($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) + { + $name=$method->getName(); + if(!strncasecmp($name,'action',6) && strlen($name)>6) + { + $name=substr($name,6); + $name[0]=strtolower($name[0]); + $help=$name; foreach($method->getParameters() as $param) { @@ -322,9 +322,9 @@ public function getOptionHelp() $help.=" --$name=value"; } $options[]=$help; - } - } - return $options; + } + } + return $options; } /** @@ -355,11 +355,11 @@ public function usageError($message) * by the function will be saved into the target file. *
  • params: optional, the parameters to be passed to the callback
  • * + * @param boolean $overwriteAll whether to overwrite all files. * @see buildFileList */ - public function copyFiles($fileList) + public function copyFiles($fileList,$overwriteAll=false) { - $overwriteAll=false; foreach($fileList as $name=>$file) { $source=strtr($file['source'],'/\\',DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR); diff --git a/framework/console/CConsoleCommandBehavior.php b/framework/console/CConsoleCommandBehavior.php index 0b18b2abf80..241bb4bef41 100644 --- a/framework/console/CConsoleCommandBehavior.php +++ b/framework/console/CConsoleCommandBehavior.php @@ -1,53 +1,53 @@ - - * @link http://www.yiiframework.com/ - * @copyright 2008-2013 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -/** - * CConsoleCommandBehavior is a base class for behaviors that are attached to a console command component. - * - * @property CConsoleCommand $owner The owner model that this behavior is attached to. - * - * @author Evgeny Blinov - * @package system.console - * @since 1.1.11 - */ -class CConsoleCommandBehavior extends CBehavior -{ - /** - * Declares events and the corresponding event handler methods. - * The default implementation returns 'onAfterConstruct', 'onBeforeValidate' and 'onAfterValidate' events and handlers. - * If you override this method, make sure you merge the parent result to the return value. - * @return array events (array keys) and the corresponding event handler methods (array values). - * @see CBehavior::events - */ - public function events() - { - return array( - 'onBeforeAction' => 'beforeAction', - 'onAfterAction' => 'afterAction' - ); - } - /** - * Responds to {@link CConsoleCommand::onBeforeAction} event. - * Override this method and make it public if you want to handle the corresponding event of the {@link CBehavior::owner owner}. - * @param CConsoleCommandEvent $event event parameter - */ - protected function beforeAction($event) - { - } - - /** - * Responds to {@link CConsoleCommand::onAfterAction} event. - * Override this method and make it public if you want to handle the corresponding event of the {@link CBehavior::owner owner}. - * @param CConsoleCommandEvent $event event parameter - */ - protected function afterAction($event) - { - } + + * @link http://www.yiiframework.com/ + * @copyright 2008-2013 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CConsoleCommandBehavior is a base class for behaviors that are attached to a console command component. + * + * @property CConsoleCommand $owner The owner model that this behavior is attached to. + * + * @author Evgeny Blinov + * @package system.console + * @since 1.1.11 + */ +class CConsoleCommandBehavior extends CBehavior +{ + /** + * Declares events and the corresponding event handler methods. + * The default implementation returns 'onAfterConstruct', 'onBeforeValidate' and 'onAfterValidate' events and handlers. + * If you override this method, make sure you merge the parent result to the return value. + * @return array events (array keys) and the corresponding event handler methods (array values). + * @see CBehavior::events + */ + public function events() + { + return array( + 'onBeforeAction' => 'beforeAction', + 'onAfterAction' => 'afterAction' + ); + } + /** + * Responds to {@link CConsoleCommand::onBeforeAction} event. + * Override this method and make it public if you want to handle the corresponding event of the {@link CBehavior::owner owner}. + * @param CConsoleCommandEvent $event event parameter + */ + protected function beforeAction($event) + { + } + + /** + * Responds to {@link CConsoleCommand::onAfterAction} event. + * Override this method and make it public if you want to handle the corresponding event of the {@link CBehavior::owner owner}. + * @param CConsoleCommandEvent $event event parameter + */ + protected function afterAction($event) + { + } } \ No newline at end of file diff --git a/framework/db/CDbCommand.php b/framework/db/CDbCommand.php index 3bd1d8587ad..6dc1ae49698 100644 --- a/framework/db/CDbCommand.php +++ b/framework/db/CDbCommand.php @@ -705,7 +705,7 @@ public function from($tables) { if(strpos($table,'(')===false) { - if(preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/',$table,$matches)) // with alias + if(preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/',$table,$matches)) // with alias $tables[$i]=$this->_connection->quoteTableName($matches[1]).' '.$this->_connection->quoteTableName($matches[2]); else $tables[$i]=$this->_connection->quoteTableName($table); @@ -1591,7 +1591,7 @@ private function joinInternal($type, $table, $conditions='', $params=array()) { if(strpos($table,'(')===false) { - if(preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/',$table,$matches)) // with alias + if(preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/',$table,$matches)) // with alias $table=$this->_connection->quoteTableName($matches[1]).' '.$this->_connection->quoteTableName($matches[2]); else $table=$this->_connection->quoteTableName($table); diff --git a/framework/db/ar/CActiveFinder.php b/framework/db/ar/CActiveFinder.php index 58a39369dde..36eb86c857f 100644 --- a/framework/db/ar/CActiveFinder.php +++ b/framework/db/ar/CActiveFinder.php @@ -747,7 +747,7 @@ public function count($criteria=null) else { $select=is_array($criteria->select) ? implode(',',$criteria->select) : $criteria->select; - if($select!=='*' && preg_match('/^count\s*\(/',trim($select))) + if($select!=='*' && preg_match('/^count\s*\(/i',trim($select))) $query->selects=array($select); elseif(is_string($this->_table->primaryKey)) { diff --git a/framework/db/ar/CActiveRecord.php b/framework/db/ar/CActiveRecord.php index fe9f125da0e..0619ee23ca3 100644 --- a/framework/db/ar/CActiveRecord.php +++ b/framework/db/ar/CActiveRecord.php @@ -2022,8 +2022,10 @@ public function mergeWith($criteria,$fromScope=false) $criteria=$criteria->toArray(); if(isset($criteria['select']) && $this->select!==$criteria['select']) { - if($this->select==='*') + if($this->select==='*'||$this->select===false) $this->select=$criteria['select']; + elseif($criteria['select']===false) + $this->select=false; elseif($criteria['select']!=='*') { $select1=is_string($this->select)?preg_split('/\s*,\s*/',trim($this->select),-1,PREG_SPLIT_NO_EMPTY):$this->select; diff --git a/framework/db/schema/CDbCommandBuilder.php b/framework/db/schema/CDbCommandBuilder.php index 7f5afabf060..7c5079948e5 100644 --- a/framework/db/schema/CDbCommandBuilder.php +++ b/framework/db/schema/CDbCommandBuilder.php @@ -137,11 +137,11 @@ public function createCountCommand($table,$criteria,$alias='t') { $pk=array(); foreach($table->primaryKey as $key) - $pk[]=$alias.'.'.$key; + $pk[]=$alias.'.'.$this->_schema->quoteColumnName($key); $pk=implode(', ',$pk); } else - $pk=$alias.'.'.$table->primaryKey; + $pk=$alias.'.'.$this->_schema->quoteColumnName($table->primaryKey); $sql="SELECT COUNT(DISTINCT $pk)"; } else diff --git a/framework/db/schema/CDbCriteria.php b/framework/db/schema/CDbCriteria.php index 86fb1b6085d..c8e1eaa96b2 100644 --- a/framework/db/schema/CDbCriteria.php +++ b/framework/db/schema/CDbCriteria.php @@ -124,19 +124,19 @@ class CDbCriteria extends CComponent */ public $index; /** - * @var mixed scopes to apply + * @var mixed scopes to apply * - * This property is effective only when passing criteria to + * This property is effective only when passing criteria to * the one of the following methods: - *
      - *
    • {@link CActiveRecord::find()}
    • - *
    • {@link CActiveRecord::findAll()}
    • - *
    • {@link CActiveRecord::findByPk()}
    • - *
    • {@link CActiveRecord::findAllByPk()}
    • - *
    • {@link CActiveRecord::findByAttributes()}
    • - *
    • {@link CActiveRecord::findAllByAttributes()}
    • - *
    • {@link CActiveRecord::count()}
    • - *
    + *
      + *
    • {@link CActiveRecord::find()}
    • + *
    • {@link CActiveRecord::findAll()}
    • + *
    • {@link CActiveRecord::findByPk()}
    • + *
    • {@link CActiveRecord::findAllByPk()}
    • + *
    • {@link CActiveRecord::findByAttributes()}
    • + *
    • {@link CActiveRecord::findAllByAttributes()}
    • + *
    • {@link CActiveRecord::count()}
    • + *
    * * Can be set to one of the following: *
      @@ -499,8 +499,10 @@ public function mergeWith($criteria,$operator='AND') $criteria=new self($criteria); if($this->select!==$criteria->select) { - if($this->select==='*') + if($this->select==='*'||$this->select===false) $this->select=$criteria->select; + elseif($criteria->select===false) + $this->select=false; elseif($criteria->select!=='*') { $select1=is_string($this->select)?preg_split('/\s*,\s*/',trim($this->select),-1,PREG_SPLIT_NO_EMPTY):$this->select; diff --git a/framework/db/schema/mssql/CMssqlCommandBuilder.php b/framework/db/schema/mssql/CMssqlCommandBuilder.php index 00bbb48d443..896a9a6c406 100644 --- a/framework/db/schema/mssql/CMssqlCommandBuilder.php +++ b/framework/db/schema/mssql/CMssqlCommandBuilder.php @@ -134,20 +134,20 @@ public function createUpdateCounterCommand($table,$counters,$criteria) } /** - * Alters the SQL to apply JOIN clause. - * Overrides parent implementation to comply with the DELETE command syntax required when multiple tables are referenced. - * @param string $sql the SQL statement to be altered - * @param string $join the JOIN clause (starting with join type, such as INNER JOIN) - * @return string the altered SQL statement - */ - public function applyJoin($sql,$join) - { - if(trim($join)!=='') - $sql=preg_replace('/^\s*DELETE\s+FROM\s+((\[.+\])|([^\s]+))\s*/i',"DELETE \\1 FROM \\1",$sql); - return parent::applyJoin($sql,$join); - } + * Alters the SQL to apply JOIN clause. + * Overrides parent implementation to comply with the DELETE command syntax required when multiple tables are referenced. + * @param string $sql the SQL statement to be altered + * @param string $join the JOIN clause (starting with join type, such as INNER JOIN) + * @return string the altered SQL statement + */ + public function applyJoin($sql,$join) + { + if(trim($join)!=='') + $sql=preg_replace('/^\s*DELETE\s+FROM\s+((\[.+\])|([^\s]+))\s*/i',"DELETE \\1 FROM \\1",$sql); + return parent::applyJoin($sql,$join); + } - /** + /** * This is a port from Prado Framework. * * Overrides parent implementation. Alters the sql to apply $limit and $offset. diff --git a/framework/db/schema/mysql/CMysqlColumnSchema.php b/framework/db/schema/mysql/CMysqlColumnSchema.php index d0728a2bdc6..2a6557a5849 100644 --- a/framework/db/schema/mysql/CMysqlColumnSchema.php +++ b/framework/db/schema/mysql/CMysqlColumnSchema.php @@ -44,7 +44,7 @@ protected function extractDefault($defaultValue) { if(strncmp($this->dbType,'bit',3)===0) $this->defaultValue=bindec(trim($defaultValue,'b\'')); - elseif($this->dbType==='timestamp' && $defaultValue==='CURRENT_TIMESTAMP') + elseif(($this->dbType==='timestamp' || $this->dbType==='datetime') && $defaultValue==='CURRENT_TIMESTAMP') $this->defaultValue=null; else parent::extractDefault($defaultValue); diff --git a/framework/db/schema/oci/COciSchema.php b/framework/db/schema/oci/COciSchema.php index 7f3bb5cbe8c..5c898218e6f 100644 --- a/framework/db/schema/oci/COciSchema.php +++ b/framework/db/schema/oci/COciSchema.php @@ -95,7 +95,7 @@ public function getDefaultSchema() } return $this->_defaultSchema; - } + } /** * @param string $table table name with optional schema name prefix, uses default schema name prefix is not provided. diff --git a/framework/db/schema/pgsql/CPgsqlSchema.php b/framework/db/schema/pgsql/CPgsqlSchema.php index 0c874e61249..a5a5257a0a1 100644 --- a/framework/db/schema/pgsql/CPgsqlSchema.php +++ b/framework/db/schema/pgsql/CPgsqlSchema.php @@ -415,42 +415,6 @@ public function alterColumn($table, $column, $type) return $sql; } - /** - * Builds a SQL statement for creating a new index. - * @param string $name the name of the index. The name will be properly quoted by the method. - * @param string $table the table that the new index will be created for. The table name will be properly quoted by the method. - * @param string $columns the column(s) that should be included in the index. If there are multiple columns, please separate them - * by commas. Each column name will be properly quoted by the method, unless a parenthesis is found in the name. - * @param boolean $unique whether to add UNIQUE constraint on the created index. - * @return string the SQL statement for creating a new index. - * @since 1.1.6 - */ - public function createIndex($name, $table, $columns, $unique=false) - { - $cols=array(); - if (is_string($columns)) - $columns=preg_split('/\s*,\s*/',$columns,-1,PREG_SPLIT_NO_EMPTY); - foreach($columns as $col) - { - if(strpos($col,'(')!==false) - $cols[]=$col; - else - $cols[]=$this->quoteColumnName($col); - } - if ($unique) - { - return 'ALTER TABLE ONLY ' - . $this->quoteTableName($table).' ADD CONSTRAINT ' - . $this->quoteTableName($name).' UNIQUE ('.implode(', ',$cols).')'; - } - else - { - return 'CREATE INDEX ' - . $this->quoteTableName($name).' ON ' - . $this->quoteTableName($table).' ('.implode(', ',$cols).')'; - } - } - /** * Builds a SQL statement for dropping an index. * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. diff --git a/framework/db/schema/sqlite/CSqliteSchema.php b/framework/db/schema/sqlite/CSqliteSchema.php index 0308a6fd6ca..bdf31be6158 100644 --- a/framework/db/schema/sqlite/CSqliteSchema.php +++ b/framework/db/schema/sqlite/CSqliteSchema.php @@ -21,21 +21,21 @@ class CSqliteSchema extends CDbSchema * @var array the abstract column types mapped to physical column types. * @since 1.1.6 */ - public $columnTypes=array( - 'pk' => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', - 'bigpk' => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', - 'string' => 'varchar(255)', - 'text' => 'text', - 'integer' => 'integer', - 'bigint' => 'integer', - 'float' => 'float', - 'decimal' => 'decimal', - 'datetime' => 'datetime', - 'timestamp' => 'timestamp', - 'time' => 'time', - 'date' => 'date', - 'binary' => 'blob', - 'boolean' => 'tinyint(1)', + public $columnTypes=array( + 'pk' => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', + 'bigpk' => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', + 'string' => 'varchar(255)', + 'text' => 'text', + 'integer' => 'integer', + 'bigint' => 'integer', + 'float' => 'float', + 'decimal' => 'decimal', + 'datetime' => 'datetime', + 'timestamp' => 'timestamp', + 'time' => 'time', + 'date' => 'date', + 'binary' => 'blob', + 'boolean' => 'tinyint(1)', 'money' => 'decimal(19,4)', ); diff --git a/framework/gii/components/Pear/Text/Diff/Engine/shell.php b/framework/gii/components/Pear/Text/Diff/Engine/shell.php index 00ead96f411..f1aaa98f0a5 100644 --- a/framework/gii/components/Pear/Text/Diff/Engine/shell.php +++ b/framework/gii/components/Pear/Text/Diff/Engine/shell.php @@ -1,162 +1,162 @@ - - * @package Text_Diff - * @since 0.3.0 - */ -class Text_Diff_Engine_shell { - - /** - * Path to the diff executable - * - * @var string - */ - var $_diffCommand = 'diff'; - - /** - * Returns the array of differences. - * - * @param array $from_lines lines of text from old file - * @param array $to_lines lines of text from new file - * - * @return array all changes made (array with Text_Diff_Op_* objects) - */ - function diff($from_lines, $to_lines) - { - array_walk($from_lines, array('Text_Diff', 'trimNewlines')); - array_walk($to_lines, array('Text_Diff', 'trimNewlines')); - - $temp_dir = Text_Diff::_getTempDir(); - - // Execute gnu diff or similar to get a standard diff file. - $from_file = tempnam($temp_dir, 'Text_Diff'); - $to_file = tempnam($temp_dir, 'Text_Diff'); - $fp = fopen($from_file, 'w'); - fwrite($fp, implode("\n", $from_lines)); - fclose($fp); - $fp = fopen($to_file, 'w'); - fwrite($fp, implode("\n", $to_lines)); - fclose($fp); - $diff = shell_exec($this->_diffCommand . ' ' . $from_file . ' ' . $to_file); - unlink($from_file); - unlink($to_file); - - if (is_null($diff)) { - // No changes were made - return array(new Text_Diff_Op_copy($from_lines)); - } - - $from_line_no = 1; - $to_line_no = 1; - $edits = array(); - - // Get changed lines by parsing something like: - // 0a1,2 - // 1,2c4,6 - // 1,5d6 - preg_match_all('#^(\d+)(?:,(\d+))?([adc])(\d+)(?:,(\d+))?$#m', $diff, - $matches, PREG_SET_ORDER); - - foreach ($matches as $match) { - if (!isset($match[5])) { - // This paren is not set every time (see regex). - $match[5] = false; - } - - if ($match[3] == 'a') { - $from_line_no--; - } - - if ($match[3] == 'd') { - $to_line_no--; - } - - if ($from_line_no < $match[1] || $to_line_no < $match[4]) { - // copied lines - assert('$match[1] - $from_line_no == $match[4] - $to_line_no'); - array_push($edits, - new Text_Diff_Op_copy( - $this->_getLines($from_lines, $from_line_no, $match[1] - 1), - $this->_getLines($to_lines, $to_line_no, $match[4] - 1))); - } - - switch ($match[3]) { - case 'd': - // deleted lines - array_push($edits, - new Text_Diff_Op_delete( - $this->_getLines($from_lines, $from_line_no, $match[2]))); - $to_line_no++; - break; - - case 'c': - // changed lines - array_push($edits, - new Text_Diff_Op_change( - $this->_getLines($from_lines, $from_line_no, $match[2]), - $this->_getLines($to_lines, $to_line_no, $match[5]))); - break; - - case 'a': - // added lines - array_push($edits, - new Text_Diff_Op_add( - $this->_getLines($to_lines, $to_line_no, $match[5]))); - $from_line_no++; - break; - } - } - - if (!empty($from_lines)) { - // Some lines might still be pending. Add them as copied - array_push($edits, - new Text_Diff_Op_copy( - $this->_getLines($from_lines, $from_line_no, - $from_line_no + count($from_lines) - 1), - $this->_getLines($to_lines, $to_line_no, - $to_line_no + count($to_lines) - 1))); - } - - return $edits; - } - - /** - * Get lines from either the old or new text - * - * @access private - * - * @param array &$text_lines Either $from_lines or $to_lines - * @param integer &$line_no Current line number - * @param integer $end Optional end line, when we want to chop more than one line. - * @return array The chopped lines - */ - function _getLines(&$text_lines, &$line_no, $end = false) - { - if (!empty($end)) { - $lines = array(); - // We can shift even more - while ($line_no <= $end) { - array_push($lines, array_shift($text_lines)); - $line_no++; - } - } else { - $lines = array(array_shift($text_lines)); - $line_no++; - } - - return $lines; - } - -} + + * @package Text_Diff + * @since 0.3.0 + */ +class Text_Diff_Engine_shell { + + /** + * Path to the diff executable + * + * @var string + */ + var $_diffCommand = 'diff'; + + /** + * Returns the array of differences. + * + * @param array $from_lines lines of text from old file + * @param array $to_lines lines of text from new file + * + * @return array all changes made (array with Text_Diff_Op_* objects) + */ + function diff($from_lines, $to_lines) + { + array_walk($from_lines, array('Text_Diff', 'trimNewlines')); + array_walk($to_lines, array('Text_Diff', 'trimNewlines')); + + $temp_dir = Text_Diff::_getTempDir(); + + // Execute gnu diff or similar to get a standard diff file. + $from_file = tempnam($temp_dir, 'Text_Diff'); + $to_file = tempnam($temp_dir, 'Text_Diff'); + $fp = fopen($from_file, 'w'); + fwrite($fp, implode("\n", $from_lines)); + fclose($fp); + $fp = fopen($to_file, 'w'); + fwrite($fp, implode("\n", $to_lines)); + fclose($fp); + $diff = shell_exec($this->_diffCommand . ' ' . $from_file . ' ' . $to_file); + unlink($from_file); + unlink($to_file); + + if (is_null($diff)) { + // No changes were made + return array(new Text_Diff_Op_copy($from_lines)); + } + + $from_line_no = 1; + $to_line_no = 1; + $edits = array(); + + // Get changed lines by parsing something like: + // 0a1,2 + // 1,2c4,6 + // 1,5d6 + preg_match_all('#^(\d+)(?:,(\d+))?([adc])(\d+)(?:,(\d+))?$#m', $diff, + $matches, PREG_SET_ORDER); + + foreach ($matches as $match) { + if (!isset($match[5])) { + // This paren is not set every time (see regex). + $match[5] = false; + } + + if ($match[3] == 'a') { + $from_line_no--; + } + + if ($match[3] == 'd') { + $to_line_no--; + } + + if ($from_line_no < $match[1] || $to_line_no < $match[4]) { + // copied lines + assert('$match[1] - $from_line_no == $match[4] - $to_line_no'); + array_push($edits, + new Text_Diff_Op_copy( + $this->_getLines($from_lines, $from_line_no, $match[1] - 1), + $this->_getLines($to_lines, $to_line_no, $match[4] - 1))); + } + + switch ($match[3]) { + case 'd': + // deleted lines + array_push($edits, + new Text_Diff_Op_delete( + $this->_getLines($from_lines, $from_line_no, $match[2]))); + $to_line_no++; + break; + + case 'c': + // changed lines + array_push($edits, + new Text_Diff_Op_change( + $this->_getLines($from_lines, $from_line_no, $match[2]), + $this->_getLines($to_lines, $to_line_no, $match[5]))); + break; + + case 'a': + // added lines + array_push($edits, + new Text_Diff_Op_add( + $this->_getLines($to_lines, $to_line_no, $match[5]))); + $from_line_no++; + break; + } + } + + if (!empty($from_lines)) { + // Some lines might still be pending. Add them as copied + array_push($edits, + new Text_Diff_Op_copy( + $this->_getLines($from_lines, $from_line_no, + $from_line_no + count($from_lines) - 1), + $this->_getLines($to_lines, $to_line_no, + $to_line_no + count($to_lines) - 1))); + } + + return $edits; + } + + /** + * Get lines from either the old or new text + * + * @access private + * + * @param array &$text_lines Either $from_lines or $to_lines + * @param integer &$line_no Current line number + * @param integer $end Optional end line, when we want to chop more than one line. + * @return array The chopped lines + */ + function _getLines(&$text_lines, &$line_no, $end = false) + { + if (!empty($end)) { + $lines = array(); + // We can shift even more + while ($line_no <= $end) { + array_push($lines, array_shift($text_lines)); + $line_no++; + } + } else { + $lines = array(array_shift($text_lines)); + $line_no++; + } + + return $lines; + } + +} diff --git a/framework/gii/components/Pear/Text/Diff/Engine/string.php b/framework/gii/components/Pear/Text/Diff/Engine/string.php index d314afa6f8c..4b29daafe9f 100644 --- a/framework/gii/components/Pear/Text/Diff/Engine/string.php +++ b/framework/gii/components/Pear/Text/Diff/Engine/string.php @@ -1,237 +1,237 @@ - - * $patch = file_get_contents('example.patch'); - * $diff = new Text_Diff('string', array($patch)); - * $renderer = new Text_Diff_Renderer_inline(); - * echo $renderer->render($diff); - * - * - * $Horde: framework/Text_Diff/Diff/Engine/string.php,v 1.5.2.5 2008/09/10 08:31:58 jan Exp $ - * - * Copyright 2005 Örjan Persson - * Copyright 2005-2008 The Horde Project (http://www.horde.org/) - * - * See the enclosed file COPYING for license information (LGPL). If you did - * not receive this file, see http://opensource.org/licenses/lgpl-license.php. - * - * @author Örjan Persson - * @package Text_Diff - * @since 0.2.0 - */ -class Text_Diff_Engine_string { - - /** - * Parses a unified or context diff. - * - * First param contains the whole diff and the second can be used to force - * a specific diff type. If the second parameter is 'autodetect', the - * diff will be examined to find out which type of diff this is. - * - * @param string $diff The diff content. - * @param string $mode The diff mode of the content in $diff. One of - * 'context', 'unified', or 'autodetect'. - * - * @return array List of all diff operations. - */ - function diff($diff, $mode = 'autodetect') - { - if ($mode != 'autodetect' && $mode != 'context' && $mode != 'unified') { - return PEAR::raiseError('Type of diff is unsupported'); - } - - if ($mode == 'autodetect') { - $context = strpos($diff, '***'); - $unified = strpos($diff, '---'); - if ($context === $unified) { - return PEAR::raiseError('Type of diff could not be detected'); - } elseif ($context === false || $unified === false) { - $mode = $context !== false ? 'context' : 'unified'; - } else { - $mode = $context < $unified ? 'context' : 'unified'; - } - } - - // Split by new line and remove the diff header, if there is one. - $diff = explode("\n", $diff); - if (($mode == 'context' && strpos($diff[0], '***') === 0) || - ($mode == 'unified' && strpos($diff[0], '---') === 0)) { - array_shift($diff); - array_shift($diff); - } - - if ($mode == 'context') { - return $this->parseContextDiff($diff); - } else { - return $this->parseUnifiedDiff($diff); - } - } - - /** - * Parses an array containing the unified diff. - * - * @param array $diff Array of lines. - * - * @return array List of all diff operations. - */ - function parseUnifiedDiff($diff) - { - $edits = array(); - $end = count($diff) - 1; - for ($i = 0; $i < $end;) { - $diff1 = array(); - switch (substr($diff[$i], 0, 1)) { - case ' ': - do { - $diff1[] = substr($diff[$i], 1); - } while (++$i < $end && substr($diff[$i], 0, 1) == ' '); - $edits[] = new Text_Diff_Op_copy($diff1); - break; - - case '+': - // get all new lines - do { - $diff1[] = substr($diff[$i], 1); - } while (++$i < $end && substr($diff[$i], 0, 1) == '+'); - $edits[] = new Text_Diff_Op_add($diff1); - break; - - case '-': - // get changed or removed lines - $diff2 = array(); - do { - $diff1[] = substr($diff[$i], 1); - } while (++$i < $end && substr($diff[$i], 0, 1) == '-'); - - while ($i < $end && substr($diff[$i], 0, 1) == '+') { - $diff2[] = substr($diff[$i++], 1); - } - if (count($diff2) == 0) { - $edits[] = new Text_Diff_Op_delete($diff1); - } else { - $edits[] = new Text_Diff_Op_change($diff1, $diff2); - } - break; - - default: - $i++; - break; - } - } - - return $edits; - } - - /** - * Parses an array containing the context diff. - * - * @param array $diff Array of lines. - * - * @return array List of all diff operations. - */ - function parseContextDiff(&$diff) - { - $edits = array(); - $i = $max_i = $j = $max_j = 0; - $end = count($diff) - 1; - while ($i < $end && $j < $end) { - while ($i >= $max_i && $j >= $max_j) { - // Find the boundaries of the diff output of the two files - for ($i = $j; - $i < $end && substr($diff[$i], 0, 3) == '***'; - $i++); - for ($max_i = $i; - $max_i < $end && substr($diff[$max_i], 0, 3) != '---'; - $max_i++); - for ($j = $max_i; - $j < $end && substr($diff[$j], 0, 3) == '---'; - $j++); - for ($max_j = $j; - $max_j < $end && substr($diff[$max_j], 0, 3) != '***'; - $max_j++); - } - - // find what hasn't been changed - $array = array(); - while ($i < $max_i && - $j < $max_j && - strcmp($diff[$i], $diff[$j]) == 0) { - $array[] = substr($diff[$i], 2); - $i++; - $j++; - } - - while ($i < $max_i && ($max_j-$j) <= 1) { - if ($diff[$i] != '' && substr($diff[$i], 0, 1) != ' ') { - break; - } - $array[] = substr($diff[$i++], 2); - } - - while ($j < $max_j && ($max_i-$i) <= 1) { - if ($diff[$j] != '' && substr($diff[$j], 0, 1) != ' ') { - break; - } - $array[] = substr($diff[$j++], 2); - } - if (count($array) > 0) { - $edits[] = new Text_Diff_Op_copy($array); - } - - if ($i < $max_i) { - $diff1 = array(); - switch (substr($diff[$i], 0, 1)) { - case '!': - $diff2 = array(); - do { - $diff1[] = substr($diff[$i], 2); - if ($j < $max_j && substr($diff[$j], 0, 1) == '!') { - $diff2[] = substr($diff[$j++], 2); - } - } while (++$i < $max_i && substr($diff[$i], 0, 1) == '!'); - $edits[] = new Text_Diff_Op_change($diff1, $diff2); - break; - - case '+': - do { - $diff1[] = substr($diff[$i], 2); - } while (++$i < $max_i && substr($diff[$i], 0, 1) == '+'); - $edits[] = new Text_Diff_Op_add($diff1); - break; - - case '-': - do { - $diff1[] = substr($diff[$i], 2); - } while (++$i < $max_i && substr($diff[$i], 0, 1) == '-'); - $edits[] = new Text_Diff_Op_delete($diff1); - break; - } - } - - if ($j < $max_j) { - $diff2 = array(); - switch (substr($diff[$j], 0, 1)) { - case '+': - do { - $diff2[] = substr($diff[$j++], 2); - } while ($j < $max_j && substr($diff[$j], 0, 1) == '+'); - $edits[] = new Text_Diff_Op_add($diff2); - break; - - case '-': - do { - $diff2[] = substr($diff[$j++], 2); - } while ($j < $max_j && substr($diff[$j], 0, 1) == '-'); - $edits[] = new Text_Diff_Op_delete($diff2); - break; - } - } - } - - return $edits; - } - -} + + * $patch = file_get_contents('example.patch'); + * $diff = new Text_Diff('string', array($patch)); + * $renderer = new Text_Diff_Renderer_inline(); + * echo $renderer->render($diff); + * + * + * $Horde: framework/Text_Diff/Diff/Engine/string.php,v 1.5.2.5 2008/09/10 08:31:58 jan Exp $ + * + * Copyright 2005 Örjan Persson + * Copyright 2005-2008 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://opensource.org/licenses/lgpl-license.php. + * + * @author Örjan Persson + * @package Text_Diff + * @since 0.2.0 + */ +class Text_Diff_Engine_string { + + /** + * Parses a unified or context diff. + * + * First param contains the whole diff and the second can be used to force + * a specific diff type. If the second parameter is 'autodetect', the + * diff will be examined to find out which type of diff this is. + * + * @param string $diff The diff content. + * @param string $mode The diff mode of the content in $diff. One of + * 'context', 'unified', or 'autodetect'. + * + * @return array List of all diff operations. + */ + function diff($diff, $mode = 'autodetect') + { + if ($mode != 'autodetect' && $mode != 'context' && $mode != 'unified') { + return PEAR::raiseError('Type of diff is unsupported'); + } + + if ($mode == 'autodetect') { + $context = strpos($diff, '***'); + $unified = strpos($diff, '---'); + if ($context === $unified) { + return PEAR::raiseError('Type of diff could not be detected'); + } elseif ($context === false || $unified === false) { + $mode = $context !== false ? 'context' : 'unified'; + } else { + $mode = $context < $unified ? 'context' : 'unified'; + } + } + + // Split by new line and remove the diff header, if there is one. + $diff = explode("\n", $diff); + if (($mode == 'context' && strpos($diff[0], '***') === 0) || + ($mode == 'unified' && strpos($diff[0], '---') === 0)) { + array_shift($diff); + array_shift($diff); + } + + if ($mode == 'context') { + return $this->parseContextDiff($diff); + } else { + return $this->parseUnifiedDiff($diff); + } + } + + /** + * Parses an array containing the unified diff. + * + * @param array $diff Array of lines. + * + * @return array List of all diff operations. + */ + function parseUnifiedDiff($diff) + { + $edits = array(); + $end = count($diff) - 1; + for ($i = 0; $i < $end;) { + $diff1 = array(); + switch (substr($diff[$i], 0, 1)) { + case ' ': + do { + $diff1[] = substr($diff[$i], 1); + } while (++$i < $end && substr($diff[$i], 0, 1) == ' '); + $edits[] = new Text_Diff_Op_copy($diff1); + break; + + case '+': + // get all new lines + do { + $diff1[] = substr($diff[$i], 1); + } while (++$i < $end && substr($diff[$i], 0, 1) == '+'); + $edits[] = new Text_Diff_Op_add($diff1); + break; + + case '-': + // get changed or removed lines + $diff2 = array(); + do { + $diff1[] = substr($diff[$i], 1); + } while (++$i < $end && substr($diff[$i], 0, 1) == '-'); + + while ($i < $end && substr($diff[$i], 0, 1) == '+') { + $diff2[] = substr($diff[$i++], 1); + } + if (count($diff2) == 0) { + $edits[] = new Text_Diff_Op_delete($diff1); + } else { + $edits[] = new Text_Diff_Op_change($diff1, $diff2); + } + break; + + default: + $i++; + break; + } + } + + return $edits; + } + + /** + * Parses an array containing the context diff. + * + * @param array $diff Array of lines. + * + * @return array List of all diff operations. + */ + function parseContextDiff(&$diff) + { + $edits = array(); + $i = $max_i = $j = $max_j = 0; + $end = count($diff) - 1; + while ($i < $end && $j < $end) { + while ($i >= $max_i && $j >= $max_j) { + // Find the boundaries of the diff output of the two files + for ($i = $j; + $i < $end && substr($diff[$i], 0, 3) == '***'; + $i++); + for ($max_i = $i; + $max_i < $end && substr($diff[$max_i], 0, 3) != '---'; + $max_i++); + for ($j = $max_i; + $j < $end && substr($diff[$j], 0, 3) == '---'; + $j++); + for ($max_j = $j; + $max_j < $end && substr($diff[$max_j], 0, 3) != '***'; + $max_j++); + } + + // find what hasn't been changed + $array = array(); + while ($i < $max_i && + $j < $max_j && + strcmp($diff[$i], $diff[$j]) == 0) { + $array[] = substr($diff[$i], 2); + $i++; + $j++; + } + + while ($i < $max_i && ($max_j-$j) <= 1) { + if ($diff[$i] != '' && substr($diff[$i], 0, 1) != ' ') { + break; + } + $array[] = substr($diff[$i++], 2); + } + + while ($j < $max_j && ($max_i-$i) <= 1) { + if ($diff[$j] != '' && substr($diff[$j], 0, 1) != ' ') { + break; + } + $array[] = substr($diff[$j++], 2); + } + if (count($array) > 0) { + $edits[] = new Text_Diff_Op_copy($array); + } + + if ($i < $max_i) { + $diff1 = array(); + switch (substr($diff[$i], 0, 1)) { + case '!': + $diff2 = array(); + do { + $diff1[] = substr($diff[$i], 2); + if ($j < $max_j && substr($diff[$j], 0, 1) == '!') { + $diff2[] = substr($diff[$j++], 2); + } + } while (++$i < $max_i && substr($diff[$i], 0, 1) == '!'); + $edits[] = new Text_Diff_Op_change($diff1, $diff2); + break; + + case '+': + do { + $diff1[] = substr($diff[$i], 2); + } while (++$i < $max_i && substr($diff[$i], 0, 1) == '+'); + $edits[] = new Text_Diff_Op_add($diff1); + break; + + case '-': + do { + $diff1[] = substr($diff[$i], 2); + } while (++$i < $max_i && substr($diff[$i], 0, 1) == '-'); + $edits[] = new Text_Diff_Op_delete($diff1); + break; + } + } + + if ($j < $max_j) { + $diff2 = array(); + switch (substr($diff[$j], 0, 1)) { + case '+': + do { + $diff2[] = substr($diff[$j++], 2); + } while ($j < $max_j && substr($diff[$j], 0, 1) == '+'); + $edits[] = new Text_Diff_Op_add($diff2); + break; + + case '-': + do { + $diff2[] = substr($diff[$j++], 2); + } while ($j < $max_j && substr($diff[$j], 0, 1) == '-'); + $edits[] = new Text_Diff_Op_delete($diff2); + break; + } + } + } + + return $edits; + } + +} diff --git a/framework/gii/components/Pear/Text/Diff/Mapped.php b/framework/gii/components/Pear/Text/Diff/Mapped.php index 908e4de61c8..7f671844e75 100644 --- a/framework/gii/components/Pear/Text/Diff/Mapped.php +++ b/framework/gii/components/Pear/Text/Diff/Mapped.php @@ -1,55 +1,55 @@ - - */ -class Text_Diff_Mapped extends Text_Diff { - - /** - * Computes a diff between sequences of strings. - * - * This can be used to compute things like case-insensitive diffs, or diffs - * which ignore changes in white-space. - * - * @param array $from_lines An array of strings. - * @param array $to_lines An array of strings. - * @param array $mapped_from_lines This array should have the same size - * number of elements as $from_lines. The - * elements in $mapped_from_lines and - * $mapped_to_lines are what is actually - * compared when computing the diff. - * @param array $mapped_to_lines This array should have the same number - * of elements as $to_lines. - */ - function Text_Diff_Mapped($from_lines, $to_lines, - $mapped_from_lines, $mapped_to_lines) - { - assert(count($from_lines) == count($mapped_from_lines)); - assert(count($to_lines) == count($mapped_to_lines)); - - parent::Text_Diff($mapped_from_lines, $mapped_to_lines); - - $xi = $yi = 0; - for ($i = 0; $i < count($this->_edits); $i++) { - $orig = &$this->_edits[$i]->orig; - if (is_array($orig)) { - $orig = array_slice($from_lines, $xi, count($orig)); - $xi += count($orig); - } - - $final = &$this->_edits[$i]->final; - if (is_array($final)) { - $final = array_slice($to_lines, $yi, count($final)); - $yi += count($final); - } - } - } - -} + + */ +class Text_Diff_Mapped extends Text_Diff { + + /** + * Computes a diff between sequences of strings. + * + * This can be used to compute things like case-insensitive diffs, or diffs + * which ignore changes in white-space. + * + * @param array $from_lines An array of strings. + * @param array $to_lines An array of strings. + * @param array $mapped_from_lines This array should have the same size + * number of elements as $from_lines. The + * elements in $mapped_from_lines and + * $mapped_to_lines are what is actually + * compared when computing the diff. + * @param array $mapped_to_lines This array should have the same number + * of elements as $to_lines. + */ + function Text_Diff_Mapped($from_lines, $to_lines, + $mapped_from_lines, $mapped_to_lines) + { + assert(count($from_lines) == count($mapped_from_lines)); + assert(count($to_lines) == count($mapped_to_lines)); + + parent::Text_Diff($mapped_from_lines, $mapped_to_lines); + + $xi = $yi = 0; + for ($i = 0; $i < count($this->_edits); $i++) { + $orig = &$this->_edits[$i]->orig; + if (is_array($orig)) { + $orig = array_slice($from_lines, $xi, count($orig)); + $xi += count($orig); + } + + $final = &$this->_edits[$i]->final; + if (is_array($final)) { + $final = array_slice($to_lines, $yi, count($final)); + $yi += count($final); + } + } + } + +} diff --git a/framework/gii/components/Pear/Text/Diff/Renderer/context.php b/framework/gii/components/Pear/Text/Diff/Renderer/context.php index 262ecefcb56..79775006a4e 100644 --- a/framework/gii/components/Pear/Text/Diff/Renderer/context.php +++ b/framework/gii/components/Pear/Text/Diff/Renderer/context.php @@ -1,77 +1,77 @@ -_second_block = "--- $ybeg ----\n"; - return "***************\n*** $xbeg ****"; - } - - function _endBlock() - { - return $this->_second_block; - } - - function _context($lines) - { - $this->_second_block .= $this->_lines($lines, ' '); - return $this->_lines($lines, ' '); - } - - function _added($lines) - { - $this->_second_block .= $this->_lines($lines, '+ '); - return ''; - } - - function _deleted($lines) - { - return $this->_lines($lines, '- '); - } - - function _changed($orig, $final) - { - $this->_second_block .= $this->_lines($final, '! '); - return $this->_lines($orig, '! '); - } - -} +_second_block = "--- $ybeg ----\n"; + return "***************\n*** $xbeg ****"; + } + + function _endBlock() + { + return $this->_second_block; + } + + function _context($lines) + { + $this->_second_block .= $this->_lines($lines, ' '); + return $this->_lines($lines, ' '); + } + + function _added($lines) + { + $this->_second_block .= $this->_lines($lines, '+ '); + return ''; + } + + function _deleted($lines) + { + return $this->_lines($lines, '- '); + } + + function _changed($orig, $final) + { + $this->_second_block .= $this->_lines($final, '! '); + return $this->_lines($orig, '! '); + } + +} diff --git a/framework/gii/components/Pear/Text/Diff/Renderer/inline.php b/framework/gii/components/Pear/Text/Diff/Renderer/inline.php index ae4bf9883c9..7f4e5ef2ca5 100644 --- a/framework/gii/components/Pear/Text/Diff/Renderer/inline.php +++ b/framework/gii/components/Pear/Text/Diff/Renderer/inline.php @@ -1,170 +1,170 @@ -'; - - /** - * Suffix for inserted text. - */ - var $_ins_suffix = ''; - - /** - * Prefix for deleted text. - */ - var $_del_prefix = ''; - - /** - * Suffix for deleted text. - */ - var $_del_suffix = ''; - - /** - * Header for each change block. - */ - var $_block_header = ''; - - /** - * What are we currently splitting on? Used to recurse to show word-level - * changes. - */ - var $_split_level = 'lines'; - - function _blockHeader($xbeg, $xlen, $ybeg, $ylen) - { - return $this->_block_header; - } - - function _startBlock($header) - { - return $header; - } - - function _lines($lines, $prefix = ' ', $encode = true) - { - if ($encode) { - array_walk($lines, array(&$this, '_encode')); - } - - if ($this->_split_level == 'words') { - return implode('', $lines); - } else { - return implode("\n", $lines) . "\n"; - } - } - - function _added($lines) - { - array_walk($lines, array(&$this, '_encode')); - $lines[0] = $this->_ins_prefix . $lines[0]; - $lines[count($lines) - 1] .= $this->_ins_suffix; - return $this->_lines($lines, ' ', false); - } - - function _deleted($lines, $words = false) - { - array_walk($lines, array(&$this, '_encode')); - $lines[0] = $this->_del_prefix . $lines[0]; - $lines[count($lines) - 1] .= $this->_del_suffix; - return $this->_lines($lines, ' ', false); - } - - function _changed($orig, $final) - { - /* If we've already split on words, don't try to do so again - just - * display. */ - if ($this->_split_level == 'words') { - $prefix = ''; - while ($orig[0] !== false && $final[0] !== false && - substr($orig[0], 0, 1) == ' ' && - substr($final[0], 0, 1) == ' ') { - $prefix .= substr($orig[0], 0, 1); - $orig[0] = substr($orig[0], 1); - $final[0] = substr($final[0], 1); - } - return $prefix . $this->_deleted($orig) . $this->_added($final); - } - - $text1 = implode("\n", $orig); - $text2 = implode("\n", $final); - - /* Non-printing newline marker. */ - $nl = "\0"; - - /* We want to split on word boundaries, but we need to - * preserve whitespace as well. Therefore we split on words, - * but include all blocks of whitespace in the wordlist. */ - $diff = new Text_Diff($this->_splitOnWords($text1, $nl), - $this->_splitOnWords($text2, $nl)); - - /* Get the diff in inline format. */ - $renderer = new Text_Diff_Renderer_inline(array_merge($this->getParams(), - array('split_level' => 'words'))); - - /* Run the diff and get the output. */ - return str_replace($nl, "\n", $renderer->render($diff)) . "\n"; - } - - function _splitOnWords($string, $newlineEscape = "\n") - { - // Ignore \0; otherwise the while loop will never finish. - $string = str_replace("\0", '', $string); - - $words = array(); - $length = strlen($string); - $pos = 0; - - while ($pos < $length) { - // Eat a word with any preceding whitespace. - $spaces = strspn(substr($string, $pos), " \n"); - $nextpos = strcspn(substr($string, $pos + $spaces), " \n"); - $words[] = str_replace("\n", $newlineEscape, substr($string, $pos, $spaces + $nextpos)); - $pos += $spaces + $nextpos; - } - - return $words; - } - - function _encode(&$string) - { - $string = htmlspecialchars($string); - } - -} +'; + + /** + * Suffix for inserted text. + */ + var $_ins_suffix = ''; + + /** + * Prefix for deleted text. + */ + var $_del_prefix = ''; + + /** + * Suffix for deleted text. + */ + var $_del_suffix = ''; + + /** + * Header for each change block. + */ + var $_block_header = ''; + + /** + * What are we currently splitting on? Used to recurse to show word-level + * changes. + */ + var $_split_level = 'lines'; + + function _blockHeader($xbeg, $xlen, $ybeg, $ylen) + { + return $this->_block_header; + } + + function _startBlock($header) + { + return $header; + } + + function _lines($lines, $prefix = ' ', $encode = true) + { + if ($encode) { + array_walk($lines, array(&$this, '_encode')); + } + + if ($this->_split_level == 'words') { + return implode('', $lines); + } else { + return implode("\n", $lines) . "\n"; + } + } + + function _added($lines) + { + array_walk($lines, array(&$this, '_encode')); + $lines[0] = $this->_ins_prefix . $lines[0]; + $lines[count($lines) - 1] .= $this->_ins_suffix; + return $this->_lines($lines, ' ', false); + } + + function _deleted($lines, $words = false) + { + array_walk($lines, array(&$this, '_encode')); + $lines[0] = $this->_del_prefix . $lines[0]; + $lines[count($lines) - 1] .= $this->_del_suffix; + return $this->_lines($lines, ' ', false); + } + + function _changed($orig, $final) + { + /* If we've already split on words, don't try to do so again - just + * display. */ + if ($this->_split_level == 'words') { + $prefix = ''; + while ($orig[0] !== false && $final[0] !== false && + substr($orig[0], 0, 1) == ' ' && + substr($final[0], 0, 1) == ' ') { + $prefix .= substr($orig[0], 0, 1); + $orig[0] = substr($orig[0], 1); + $final[0] = substr($final[0], 1); + } + return $prefix . $this->_deleted($orig) . $this->_added($final); + } + + $text1 = implode("\n", $orig); + $text2 = implode("\n", $final); + + /* Non-printing newline marker. */ + $nl = "\0"; + + /* We want to split on word boundaries, but we need to + * preserve whitespace as well. Therefore we split on words, + * but include all blocks of whitespace in the wordlist. */ + $diff = new Text_Diff($this->_splitOnWords($text1, $nl), + $this->_splitOnWords($text2, $nl)); + + /* Get the diff in inline format. */ + $renderer = new Text_Diff_Renderer_inline(array_merge($this->getParams(), + array('split_level' => 'words'))); + + /* Run the diff and get the output. */ + return str_replace($nl, "\n", $renderer->render($diff)) . "\n"; + } + + function _splitOnWords($string, $newlineEscape = "\n") + { + // Ignore \0; otherwise the while loop will never finish. + $string = str_replace("\0", '', $string); + + $words = array(); + $length = strlen($string); + $pos = 0; + + while ($pos < $length) { + // Eat a word with any preceding whitespace. + $spaces = strspn(substr($string, $pos), " \n"); + $nextpos = strcspn(substr($string, $pos + $spaces), " \n"); + $words[] = str_replace("\n", $newlineEscape, substr($string, $pos, $spaces + $nextpos)); + $pos += $spaces + $nextpos; + } + + return $words; + } + + function _encode(&$string) + { + $string = htmlspecialchars($string); + } + +} diff --git a/framework/gii/components/Pear/Text/Diff/Renderer/unified.php b/framework/gii/components/Pear/Text/Diff/Renderer/unified.php index 90f05387920..943d519acdf 100644 --- a/framework/gii/components/Pear/Text/Diff/Renderer/unified.php +++ b/framework/gii/components/Pear/Text/Diff/Renderer/unified.php @@ -1,67 +1,67 @@ -_lines($lines, ' '); - } - - function _added($lines) - { - return $this->_lines($lines, '+'); - } - - function _deleted($lines) - { - return $this->_lines($lines, '-'); - } - - function _changed($orig, $final) - { - return $this->_deleted($orig) . $this->_added($final); - } - -} +_lines($lines, ' '); + } + + function _added($lines) + { + return $this->_lines($lines, '+'); + } + + function _deleted($lines) + { + return $this->_lines($lines, '-'); + } + + function _changed($orig, $final) + { + return $this->_deleted($orig) . $this->_added($final); + } + +} diff --git a/framework/gii/components/Pear/Text/Diff/ThreeWay.php b/framework/gii/components/Pear/Text/Diff/ThreeWay.php index 0ed96fdf26d..4e4b939d00c 100644 --- a/framework/gii/components/Pear/Text/Diff/ThreeWay.php +++ b/framework/gii/components/Pear/Text/Diff/ThreeWay.php @@ -1,276 +1,276 @@ - - */ -class Text_Diff_ThreeWay extends Text_Diff { - - /** - * Conflict counter. - * - * @var integer - */ - var $_conflictingBlocks = 0; - - /** - * Computes diff between 3 sequences of strings. - * - * @param array $orig The original lines to use. - * @param array $final1 The first version to compare to. - * @param array $final2 The second version to compare to. - */ - function Text_Diff_ThreeWay($orig, $final1, $final2) - { - if (extension_loaded('xdiff')) { - $engine = new Text_Diff_Engine_xdiff(); - } else { - $engine = new Text_Diff_Engine_native(); - } - - $this->_edits = $this->_diff3($engine->diff($orig, $final1), - $engine->diff($orig, $final2)); - } - - /** - */ - function mergedOutput($label1 = false, $label2 = false) - { - $lines = array(); - foreach ($this->_edits as $edit) { - if ($edit->isConflict()) { - /* FIXME: this should probably be moved somewhere else. */ - $lines = array_merge($lines, - array('<<<<<<<' . ($label1 ? ' ' . $label1 : '')), - $edit->final1, - array("======="), - $edit->final2, - array('>>>>>>>' . ($label2 ? ' ' . $label2 : ''))); - $this->_conflictingBlocks++; - } else { - $lines = array_merge($lines, $edit->merged()); - } - } - - return $lines; - } - - /** - * @access private - */ - function _diff3($edits1, $edits2) - { - $edits = array(); - $bb = new Text_Diff_ThreeWay_BlockBuilder(); - - $e1 = current($edits1); - $e2 = current($edits2); - while ($e1 || $e2) { - if ($e1 && $e2 && is_a($e1, 'Text_Diff_Op_copy') && is_a($e2, 'Text_Diff_Op_copy')) { - /* We have copy blocks from both diffs. This is the (only) - * time we want to emit a diff3 copy block. Flush current - * diff3 diff block, if any. */ - if ($edit = $bb->finish()) { - $edits[] = $edit; - } - - $ncopy = min($e1->norig(), $e2->norig()); - assert($ncopy > 0); - $edits[] = new Text_Diff_ThreeWay_Op_copy(array_slice($e1->orig, 0, $ncopy)); - - if ($e1->norig() > $ncopy) { - array_splice($e1->orig, 0, $ncopy); - array_splice($e1->final, 0, $ncopy); - } else { - $e1 = next($edits1); - } - - if ($e2->norig() > $ncopy) { - array_splice($e2->orig, 0, $ncopy); - array_splice($e2->final, 0, $ncopy); - } else { - $e2 = next($edits2); - } - } else { - if ($e1 && $e2) { - if ($e1->orig && $e2->orig) { - $norig = min($e1->norig(), $e2->norig()); - $orig = array_splice($e1->orig, 0, $norig); - array_splice($e2->orig, 0, $norig); - $bb->input($orig); - } - - if (is_a($e1, 'Text_Diff_Op_copy')) { - $bb->out1(array_splice($e1->final, 0, $norig)); - } - - if (is_a($e2, 'Text_Diff_Op_copy')) { - $bb->out2(array_splice($e2->final, 0, $norig)); - } - } - - if ($e1 && ! $e1->orig) { - $bb->out1($e1->final); - $e1 = next($edits1); - } - if ($e2 && ! $e2->orig) { - $bb->out2($e2->final); - $e2 = next($edits2); - } - } - } - - if ($edit = $bb->finish()) { - $edits[] = $edit; - } - - return $edits; - } - -} - -/** - * @package Text_Diff - * @author Geoffrey T. Dairiki - * - * @access private - */ -class Text_Diff_ThreeWay_Op { - - function Text_Diff_ThreeWay_Op($orig = false, $final1 = false, $final2 = false) - { - $this->orig = $orig ? $orig : array(); - $this->final1 = $final1 ? $final1 : array(); - $this->final2 = $final2 ? $final2 : array(); - } - - function merged() - { - if (!isset($this->_merged)) { - if ($this->final1 === $this->final2) { - $this->_merged = &$this->final1; - } elseif ($this->final1 === $this->orig) { - $this->_merged = &$this->final2; - } elseif ($this->final2 === $this->orig) { - $this->_merged = &$this->final1; - } else { - $this->_merged = false; - } - } - - return $this->_merged; - } - - function isConflict() - { - return $this->merged() === false; - } - -} - -/** - * @package Text_Diff - * @author Geoffrey T. Dairiki - * - * @access private - */ -class Text_Diff_ThreeWay_Op_copy extends Text_Diff_ThreeWay_Op { - - function Text_Diff_ThreeWay_Op_Copy($lines = false) - { - $this->orig = $lines ? $lines : array(); - $this->final1 = &$this->orig; - $this->final2 = &$this->orig; - } - - function merged() - { - return $this->orig; - } - - function isConflict() - { - return false; - } - -} - -/** - * @package Text_Diff - * @author Geoffrey T. Dairiki - * - * @access private - */ -class Text_Diff_ThreeWay_BlockBuilder { - - function Text_Diff_ThreeWay_BlockBuilder() - { - $this->_init(); - } - - function input($lines) - { - if ($lines) { - $this->_append($this->orig, $lines); - } - } - - function out1($lines) - { - if ($lines) { - $this->_append($this->final1, $lines); - } - } - - function out2($lines) - { - if ($lines) { - $this->_append($this->final2, $lines); - } - } - - function isEmpty() - { - return !$this->orig && !$this->final1 && !$this->final2; - } - - function finish() - { - if ($this->isEmpty()) { - return false; - } else { - $edit = new Text_Diff_ThreeWay_Op($this->orig, $this->final1, $this->final2); - $this->_init(); - return $edit; - } - } - - function _init() - { - $this->orig = $this->final1 = $this->final2 = array(); - } - - function _append(&$array, $lines) - { - array_splice($array, sizeof($array), 0, $lines); - } - -} + + */ +class Text_Diff_ThreeWay extends Text_Diff { + + /** + * Conflict counter. + * + * @var integer + */ + var $_conflictingBlocks = 0; + + /** + * Computes diff between 3 sequences of strings. + * + * @param array $orig The original lines to use. + * @param array $final1 The first version to compare to. + * @param array $final2 The second version to compare to. + */ + function Text_Diff_ThreeWay($orig, $final1, $final2) + { + if (extension_loaded('xdiff')) { + $engine = new Text_Diff_Engine_xdiff(); + } else { + $engine = new Text_Diff_Engine_native(); + } + + $this->_edits = $this->_diff3($engine->diff($orig, $final1), + $engine->diff($orig, $final2)); + } + + /** + */ + function mergedOutput($label1 = false, $label2 = false) + { + $lines = array(); + foreach ($this->_edits as $edit) { + if ($edit->isConflict()) { + /* FIXME: this should probably be moved somewhere else. */ + $lines = array_merge($lines, + array('<<<<<<<' . ($label1 ? ' ' . $label1 : '')), + $edit->final1, + array("======="), + $edit->final2, + array('>>>>>>>' . ($label2 ? ' ' . $label2 : ''))); + $this->_conflictingBlocks++; + } else { + $lines = array_merge($lines, $edit->merged()); + } + } + + return $lines; + } + + /** + * @access private + */ + function _diff3($edits1, $edits2) + { + $edits = array(); + $bb = new Text_Diff_ThreeWay_BlockBuilder(); + + $e1 = current($edits1); + $e2 = current($edits2); + while ($e1 || $e2) { + if ($e1 && $e2 && is_a($e1, 'Text_Diff_Op_copy') && is_a($e2, 'Text_Diff_Op_copy')) { + /* We have copy blocks from both diffs. This is the (only) + * time we want to emit a diff3 copy block. Flush current + * diff3 diff block, if any. */ + if ($edit = $bb->finish()) { + $edits[] = $edit; + } + + $ncopy = min($e1->norig(), $e2->norig()); + assert($ncopy > 0); + $edits[] = new Text_Diff_ThreeWay_Op_copy(array_slice($e1->orig, 0, $ncopy)); + + if ($e1->norig() > $ncopy) { + array_splice($e1->orig, 0, $ncopy); + array_splice($e1->final, 0, $ncopy); + } else { + $e1 = next($edits1); + } + + if ($e2->norig() > $ncopy) { + array_splice($e2->orig, 0, $ncopy); + array_splice($e2->final, 0, $ncopy); + } else { + $e2 = next($edits2); + } + } else { + if ($e1 && $e2) { + if ($e1->orig && $e2->orig) { + $norig = min($e1->norig(), $e2->norig()); + $orig = array_splice($e1->orig, 0, $norig); + array_splice($e2->orig, 0, $norig); + $bb->input($orig); + } + + if (is_a($e1, 'Text_Diff_Op_copy')) { + $bb->out1(array_splice($e1->final, 0, $norig)); + } + + if (is_a($e2, 'Text_Diff_Op_copy')) { + $bb->out2(array_splice($e2->final, 0, $norig)); + } + } + + if ($e1 && ! $e1->orig) { + $bb->out1($e1->final); + $e1 = next($edits1); + } + if ($e2 && ! $e2->orig) { + $bb->out2($e2->final); + $e2 = next($edits2); + } + } + } + + if ($edit = $bb->finish()) { + $edits[] = $edit; + } + + return $edits; + } + +} + +/** + * @package Text_Diff + * @author Geoffrey T. Dairiki + * + * @access private + */ +class Text_Diff_ThreeWay_Op { + + function Text_Diff_ThreeWay_Op($orig = false, $final1 = false, $final2 = false) + { + $this->orig = $orig ? $orig : array(); + $this->final1 = $final1 ? $final1 : array(); + $this->final2 = $final2 ? $final2 : array(); + } + + function merged() + { + if (!isset($this->_merged)) { + if ($this->final1 === $this->final2) { + $this->_merged = &$this->final1; + } elseif ($this->final1 === $this->orig) { + $this->_merged = &$this->final2; + } elseif ($this->final2 === $this->orig) { + $this->_merged = &$this->final1; + } else { + $this->_merged = false; + } + } + + return $this->_merged; + } + + function isConflict() + { + return $this->merged() === false; + } + +} + +/** + * @package Text_Diff + * @author Geoffrey T. Dairiki + * + * @access private + */ +class Text_Diff_ThreeWay_Op_copy extends Text_Diff_ThreeWay_Op { + + function Text_Diff_ThreeWay_Op_Copy($lines = false) + { + $this->orig = $lines ? $lines : array(); + $this->final1 = &$this->orig; + $this->final2 = &$this->orig; + } + + function merged() + { + return $this->orig; + } + + function isConflict() + { + return false; + } + +} + +/** + * @package Text_Diff + * @author Geoffrey T. Dairiki + * + * @access private + */ +class Text_Diff_ThreeWay_BlockBuilder { + + function Text_Diff_ThreeWay_BlockBuilder() + { + $this->_init(); + } + + function input($lines) + { + if ($lines) { + $this->_append($this->orig, $lines); + } + } + + function out1($lines) + { + if ($lines) { + $this->_append($this->final1, $lines); + } + } + + function out2($lines) + { + if ($lines) { + $this->_append($this->final2, $lines); + } + } + + function isEmpty() + { + return !$this->orig && !$this->final1 && !$this->final2; + } + + function finish() + { + if ($this->isEmpty()) { + return false; + } else { + $edit = new Text_Diff_ThreeWay_Op($this->orig, $this->final1, $this->final2); + $this->_init(); + return $edit; + } + } + + function _init() + { + $this->orig = $this->final1 = $this->final2 = array(); + } + + function _append(&$array, $lines) + { + array_splice($array, sizeof($array), 0, $lines); + } + +} diff --git a/framework/gii/components/Pear/Text/Diff3.php b/framework/gii/components/Pear/Text/Diff3.php index 525c2b8a8a0..2c283707053 100644 --- a/framework/gii/components/Pear/Text/Diff3.php +++ b/framework/gii/components/Pear/Text/Diff3.php @@ -1,276 +1,276 @@ - - */ -class Text_Diff3 extends Text_Diff { - - /** - * Conflict counter. - * - * @var integer - */ - var $_conflictingBlocks = 0; - - /** - * Computes diff between 3 sequences of strings. - * - * @param array $orig The original lines to use. - * @param array $final1 The first version to compare to. - * @param array $final2 The second version to compare to. - */ - function Text_Diff3($orig, $final1, $final2) - { - if (extension_loaded('xdiff')) { - $engine = new Text_Diff_Engine_xdiff(); - } else { - $engine = new Text_Diff_Engine_native(); - } - - $this->_edits = $this->_diff3($engine->diff($orig, $final1), - $engine->diff($orig, $final2)); - } - - /** - */ - function mergedOutput($label1 = false, $label2 = false) - { - $lines = array(); - foreach ($this->_edits as $edit) { - if ($edit->isConflict()) { - /* FIXME: this should probably be moved somewhere else. */ - $lines = array_merge($lines, - array('<<<<<<<' . ($label1 ? ' ' . $label1 : '')), - $edit->final1, - array("======="), - $edit->final2, - array('>>>>>>>' . ($label2 ? ' ' . $label2 : ''))); - $this->_conflictingBlocks++; - } else { - $lines = array_merge($lines, $edit->merged()); - } - } - - return $lines; - } - - /** - * @access private - */ - function _diff3($edits1, $edits2) - { - $edits = array(); - $bb = new Text_Diff3_BlockBuilder(); - - $e1 = current($edits1); - $e2 = current($edits2); - while ($e1 || $e2) { - if ($e1 && $e2 && is_a($e1, 'Text_Diff_Op_copy') && is_a($e2, 'Text_Diff_Op_copy')) { - /* We have copy blocks from both diffs. This is the (only) - * time we want to emit a diff3 copy block. Flush current - * diff3 diff block, if any. */ - if ($edit = $bb->finish()) { - $edits[] = $edit; - } - - $ncopy = min($e1->norig(), $e2->norig()); - assert($ncopy > 0); - $edits[] = new Text_Diff3_Op_copy(array_slice($e1->orig, 0, $ncopy)); - - if ($e1->norig() > $ncopy) { - array_splice($e1->orig, 0, $ncopy); - array_splice($e1->final, 0, $ncopy); - } else { - $e1 = next($edits1); - } - - if ($e2->norig() > $ncopy) { - array_splice($e2->orig, 0, $ncopy); - array_splice($e2->final, 0, $ncopy); - } else { - $e2 = next($edits2); - } - } else { - if ($e1 && $e2) { - if ($e1->orig && $e2->orig) { - $norig = min($e1->norig(), $e2->norig()); - $orig = array_splice($e1->orig, 0, $norig); - array_splice($e2->orig, 0, $norig); - $bb->input($orig); - } - - if (is_a($e1, 'Text_Diff_Op_copy')) { - $bb->out1(array_splice($e1->final, 0, $norig)); - } - - if (is_a($e2, 'Text_Diff_Op_copy')) { - $bb->out2(array_splice($e2->final, 0, $norig)); - } - } - - if ($e1 && ! $e1->orig) { - $bb->out1($e1->final); - $e1 = next($edits1); - } - if ($e2 && ! $e2->orig) { - $bb->out2($e2->final); - $e2 = next($edits2); - } - } - } - - if ($edit = $bb->finish()) { - $edits[] = $edit; - } - - return $edits; - } - -} - -/** - * @package Text_Diff - * @author Geoffrey T. Dairiki - * - * @access private - */ -class Text_Diff3_Op { - - function Text_Diff3_Op($orig = false, $final1 = false, $final2 = false) - { - $this->orig = $orig ? $orig : array(); - $this->final1 = $final1 ? $final1 : array(); - $this->final2 = $final2 ? $final2 : array(); - } - - function merged() - { - if (!isset($this->_merged)) { - if ($this->final1 === $this->final2) { - $this->_merged = &$this->final1; - } elseif ($this->final1 === $this->orig) { - $this->_merged = &$this->final2; - } elseif ($this->final2 === $this->orig) { - $this->_merged = &$this->final1; - } else { - $this->_merged = false; - } - } - - return $this->_merged; - } - - function isConflict() - { - return $this->merged() === false; - } - -} - -/** - * @package Text_Diff - * @author Geoffrey T. Dairiki - * - * @access private - */ -class Text_Diff3_Op_copy extends Text_Diff3_Op { - - function Text_Diff3_Op_Copy($lines = false) - { - $this->orig = $lines ? $lines : array(); - $this->final1 = &$this->orig; - $this->final2 = &$this->orig; - } - - function merged() - { - return $this->orig; - } - - function isConflict() - { - return false; - } - -} - -/** - * @package Text_Diff - * @author Geoffrey T. Dairiki - * - * @access private - */ -class Text_Diff3_BlockBuilder { - - function Text_Diff3_BlockBuilder() - { - $this->_init(); - } - - function input($lines) - { - if ($lines) { - $this->_append($this->orig, $lines); - } - } - - function out1($lines) - { - if ($lines) { - $this->_append($this->final1, $lines); - } - } - - function out2($lines) - { - if ($lines) { - $this->_append($this->final2, $lines); - } - } - - function isEmpty() - { - return !$this->orig && !$this->final1 && !$this->final2; - } - - function finish() - { - if ($this->isEmpty()) { - return false; - } else { - $edit = new Text_Diff3_Op($this->orig, $this->final1, $this->final2); - $this->_init(); - return $edit; - } - } - - function _init() - { - $this->orig = $this->final1 = $this->final2 = array(); - } - - function _append(&$array, $lines) - { - array_splice($array, sizeof($array), 0, $lines); - } - -} + + */ +class Text_Diff3 extends Text_Diff { + + /** + * Conflict counter. + * + * @var integer + */ + var $_conflictingBlocks = 0; + + /** + * Computes diff between 3 sequences of strings. + * + * @param array $orig The original lines to use. + * @param array $final1 The first version to compare to. + * @param array $final2 The second version to compare to. + */ + function Text_Diff3($orig, $final1, $final2) + { + if (extension_loaded('xdiff')) { + $engine = new Text_Diff_Engine_xdiff(); + } else { + $engine = new Text_Diff_Engine_native(); + } + + $this->_edits = $this->_diff3($engine->diff($orig, $final1), + $engine->diff($orig, $final2)); + } + + /** + */ + function mergedOutput($label1 = false, $label2 = false) + { + $lines = array(); + foreach ($this->_edits as $edit) { + if ($edit->isConflict()) { + /* FIXME: this should probably be moved somewhere else. */ + $lines = array_merge($lines, + array('<<<<<<<' . ($label1 ? ' ' . $label1 : '')), + $edit->final1, + array("======="), + $edit->final2, + array('>>>>>>>' . ($label2 ? ' ' . $label2 : ''))); + $this->_conflictingBlocks++; + } else { + $lines = array_merge($lines, $edit->merged()); + } + } + + return $lines; + } + + /** + * @access private + */ + function _diff3($edits1, $edits2) + { + $edits = array(); + $bb = new Text_Diff3_BlockBuilder(); + + $e1 = current($edits1); + $e2 = current($edits2); + while ($e1 || $e2) { + if ($e1 && $e2 && is_a($e1, 'Text_Diff_Op_copy') && is_a($e2, 'Text_Diff_Op_copy')) { + /* We have copy blocks from both diffs. This is the (only) + * time we want to emit a diff3 copy block. Flush current + * diff3 diff block, if any. */ + if ($edit = $bb->finish()) { + $edits[] = $edit; + } + + $ncopy = min($e1->norig(), $e2->norig()); + assert($ncopy > 0); + $edits[] = new Text_Diff3_Op_copy(array_slice($e1->orig, 0, $ncopy)); + + if ($e1->norig() > $ncopy) { + array_splice($e1->orig, 0, $ncopy); + array_splice($e1->final, 0, $ncopy); + } else { + $e1 = next($edits1); + } + + if ($e2->norig() > $ncopy) { + array_splice($e2->orig, 0, $ncopy); + array_splice($e2->final, 0, $ncopy); + } else { + $e2 = next($edits2); + } + } else { + if ($e1 && $e2) { + if ($e1->orig && $e2->orig) { + $norig = min($e1->norig(), $e2->norig()); + $orig = array_splice($e1->orig, 0, $norig); + array_splice($e2->orig, 0, $norig); + $bb->input($orig); + } + + if (is_a($e1, 'Text_Diff_Op_copy')) { + $bb->out1(array_splice($e1->final, 0, $norig)); + } + + if (is_a($e2, 'Text_Diff_Op_copy')) { + $bb->out2(array_splice($e2->final, 0, $norig)); + } + } + + if ($e1 && ! $e1->orig) { + $bb->out1($e1->final); + $e1 = next($edits1); + } + if ($e2 && ! $e2->orig) { + $bb->out2($e2->final); + $e2 = next($edits2); + } + } + } + + if ($edit = $bb->finish()) { + $edits[] = $edit; + } + + return $edits; + } + +} + +/** + * @package Text_Diff + * @author Geoffrey T. Dairiki + * + * @access private + */ +class Text_Diff3_Op { + + function Text_Diff3_Op($orig = false, $final1 = false, $final2 = false) + { + $this->orig = $orig ? $orig : array(); + $this->final1 = $final1 ? $final1 : array(); + $this->final2 = $final2 ? $final2 : array(); + } + + function merged() + { + if (!isset($this->_merged)) { + if ($this->final1 === $this->final2) { + $this->_merged = &$this->final1; + } elseif ($this->final1 === $this->orig) { + $this->_merged = &$this->final2; + } elseif ($this->final2 === $this->orig) { + $this->_merged = &$this->final1; + } else { + $this->_merged = false; + } + } + + return $this->_merged; + } + + function isConflict() + { + return $this->merged() === false; + } + +} + +/** + * @package Text_Diff + * @author Geoffrey T. Dairiki + * + * @access private + */ +class Text_Diff3_Op_copy extends Text_Diff3_Op { + + function Text_Diff3_Op_Copy($lines = false) + { + $this->orig = $lines ? $lines : array(); + $this->final1 = &$this->orig; + $this->final2 = &$this->orig; + } + + function merged() + { + return $this->orig; + } + + function isConflict() + { + return false; + } + +} + +/** + * @package Text_Diff + * @author Geoffrey T. Dairiki + * + * @access private + */ +class Text_Diff3_BlockBuilder { + + function Text_Diff3_BlockBuilder() + { + $this->_init(); + } + + function input($lines) + { + if ($lines) { + $this->_append($this->orig, $lines); + } + } + + function out1($lines) + { + if ($lines) { + $this->_append($this->final1, $lines); + } + } + + function out2($lines) + { + if ($lines) { + $this->_append($this->final2, $lines); + } + } + + function isEmpty() + { + return !$this->orig && !$this->final1 && !$this->final2; + } + + function finish() + { + if ($this->isEmpty()) { + return false; + } else { + $edit = new Text_Diff3_Op($this->orig, $this->final1, $this->final2); + $this->_init(); + return $edit; + } + } + + function _init() + { + $this->orig = $this->final1 = $this->final2 = array(); + } + + function _append(&$array, $lines) + { + array_splice($array, sizeof($array), 0, $lines); + } + +} diff --git a/framework/i18n/CChoiceFormat.php b/framework/i18n/CChoiceFormat.php index d6f7e823c60..e7edfd7e34f 100644 --- a/framework/i18n/CChoiceFormat.php +++ b/framework/i18n/CChoiceFormat.php @@ -71,6 +71,13 @@ public static function format($messages, $number) */ protected static function evaluate($expression,$n) { - return @eval("return $expression;"); + try + { + return @eval("return $expression;"); + } + catch (ParseError $e) + { + return false; + } } } \ No newline at end of file diff --git a/framework/i18n/gettext/CGettextMoFile.php b/framework/i18n/gettext/CGettextMoFile.php index b9c0c18268d..221af6d034d 100644 --- a/framework/i18n/gettext/CGettextMoFile.php +++ b/framework/i18n/gettext/CGettextMoFile.php @@ -73,7 +73,8 @@ public function load($file,$context) throw new CException(Yii::t('yii','Unable to lock file "{file}" for reading.', array('{file}'=>$file))); - $magic=current($array=unpack('c',$this->readByte($fr,4))); + $array=unpack('c',$this->readByte($fr,4)); + $magic=current($array); if($magic==-34) $this->useBigEndian=false; elseif($magic==-107) @@ -231,7 +232,8 @@ protected function writeByte($fw,$data) */ protected function readInteger($fr) { - return current($array=unpack($this->useBigEndian ? 'N' : 'V', $this->readByte($fr,4))); + $array=unpack($this->useBigEndian ? 'N' : 'V', $this->readByte($fr,4)); + return current($array); } /** diff --git a/framework/messages/config.php b/framework/messages/config.php index e7fa9f326fd..08aed9ab639 100644 --- a/framework/messages/config.php +++ b/framework/messages/config.php @@ -6,7 +6,7 @@ return array( 'sourcePath'=>dirname(__FILE__).DIRECTORY_SEPARATOR.'..', 'messagePath'=>dirname(__FILE__).DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'messages', - 'languages'=>array('fi','zh_cn','zh_tw','ca','de','el','es','sv','he','nl','pt','pt_br','ru','it','fr','ja','pl','hu','ro','id','vi','bg','lv','sk','uk','ko_kr','kk','cs','da'), + 'languages'=>array('ar','bg','bs','ca','cs','da','de','el','es','fa_ir','fi','fr','he','hu','id','it','ja','kk','ko_kr','lt','lv','nl','no','pl','pt','pt_br','ro','ru','sk','sr_sr','sr_yu','sv','ta_in','th','tr','uk','vi','zh_cn','zh_tw'), 'fileTypes'=>array('php'), 'overwrite'=>true, 'exclude'=>array( diff --git a/framework/messages/de/yii.php b/framework/messages/de/yii.php index 7928a640090..e245716cd1b 100644 --- a/framework/messages/de/yii.php +++ b/framework/messages/de/yii.php @@ -17,7 +17,16 @@ * NOTE, this file must be saved in UTF-8 encoding. */ return array ( - 'Can not generate multiple insert command with empty data set.' => 'Ein multi-INSERT-command kann nicht mit leerem Datensatz erstellt werden.', + '\'{db}\' component doesn\'t exist.' => 'Die Applikations-Komponente \'{db}\' existiert nicht.', + '\'{db}\' component is not a valid CDbConnection instance.' => 'Die Applikations-Komponente \'{db}\' ist keine Instanz der CDbConnection Klasse.', + 'CApcCache requires PHP {extension} extension to be loaded.' => 'CApcCache erfordert, dass die PHP {extension} Erweiterung geladen wurde.', + 'Can\'t create state persister table. Check CREATE privilege for \'{db}\' connection user or create table manually with SQL: {sql}.' => 'Die Tabelle fĂĽr den CDbStatePersister konnte nicht angelegt werden. PrĂĽfen Sie die CREATE-Rechte fĂĽr den Benutzer der Verbindung \'{db}\', oder erstellen Sie die Tabelle manuell mit dem folgenden SQL-Befehl: {sql}.', + 'Encryption key length can be {keyLengths}.' => 'Die SchlĂĽssellänge kann folgende Werte annehmen: {keyLengths}.', + 'Encryption key length must be between {minLength} and {maxLength}.' => 'Die SchlĂĽssellänge muss zwischen {minLength} und {maxLength} liegen.', + 'Encryption key should be a string.' => 'Der SchlĂĽssel muss als String gegeben werden.', + 'Failed to validate key. Supported key lengths of cipher not known.' => 'Der SchlĂĽssel konnte nicht validiert werden. Die VerfĂĽgbaren SchlĂĽssellängen sind unbekannt.', + 'No encryption key specified.' => 'Es wurde kein SchlĂĽssel angegeben.', + 'stateTableName param cannot be null.' => 'Der "stateTableName" Parameter darf nicht null sein.', '"{path}" is not a valid directory.' => '"{path}" ist kein gĂĽltiges Verzeichnis.', '< Previous' => '< Vorherige', '<< First' => '<< Erste', @@ -36,7 +45,6 @@ 'Application runtime path "{path}" is not valid. Please make sure it is a directory writable by the Web server process.' => 'Der Laufzeit-Pfad "{path}" der Applikation ist ungĂĽltig. Bitte stellen Sie sicher, dass der Webserver-Prozess Schreibrechte dafĂĽr besitzt.', 'Authorization item "{item}" has already been assigned to user "{user}".' => 'Autorisierungs-Element "{item}" wurde bereits dem Benutzer "{user}" zugewiesen.', 'Base path "{path}" is not a valid directory.' => 'Basispfad "{path}" ist kein gĂĽltiges Verzeichnis.', - 'CApcCache requires PHP apc extension to be loaded.' => 'CApcCache erfordert, dass die PHP APC Erweiterung geladen wurde.', 'CAssetManager.basePath "{path}" is invalid. Please make sure the directory exists and is writable by the Web server process.' => 'CAssetManager.basePath "{path}" ist ungĂĽltig. Bitte stellen Sie sicher, dass das Verzeichnis existiert und der Webserver-Prozess Schreibrechte dafĂĽr besitzt.', 'CCacheHttpSession.cacheID is invalid. Please make sure "{id}" refers to a valid cache application component.' => 'CCacheHttpSession.cacheID ist ungĂĽltig. Stellen Sie sicher, dass "{id}" auf eine gĂĽltige Cache-Komponente verweist.', 'CCaptchaValidator.action "{id}" is invalid. Unable to find such an action in the current controller.' => 'CCaptchaValidator.action "{id}" ist ungĂĽltig. Konnte im aktuellen Controller keine solche Aktion finden.', @@ -79,7 +87,6 @@ 'CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.' => 'CProfileLogRoute fand einen unzugehörigen Code-Block "{token}". Stellen Sie sicher dass Aufrufe von Yii::beginProfile() und Yii::endProfile() richtig verschachtelt sind.', 'CProfileLogRoute.report "{report}" is invalid. Valid values include "summary" and "callstack".' => 'CProfileLogRoute.report "{report}" ist ungĂĽltig. GĂĽltige Werte enthalten "summary" und "callstack".', 'CSecurityManager requires PHP mcrypt extension to be loaded in order to use data encryption feature.' => 'CSecurityManager erfordert, dass die PHP mcrypt Erweiterung geladen wurde, um das DatenverschlĂĽsselungs-Feature nutzen zu können.', - 'CSecurityManager.encryptionKey cannot be empty.' => 'CSecurityManager.encryptionKey darf nicht leer sein.', 'CSecurityManager.validationKey cannot be empty.' => 'CSecurityManager.validationKey darf nicht leer sein.', 'CSecurityManager::generateRandomString() cannot generate random string in the current environment.' => 'CSecurityManager::generateRandomString() ist auf diesem System nicht in der Lage eine zufällige Zeichenkette zu erzeugen.', 'CTypedList<{type}> can only hold objects of {type} class.' => 'CTypedList<{type}> kann nur Objekte der {type}-Klasse beinhalten.', @@ -89,6 +96,7 @@ 'CWinCache user cache is disabled. Please set wincache.ucenabled to On in your php.ini.' => 'Anwendercache fĂĽr CWinCache ist deaktiviert. Bitte setzen Sie in Ihrer php.ini wincache.ucenabled auf On.', 'CXCache requires PHP XCache extension to be loaded.' => 'CXCache erfordert, dass die PHP XCache Erweiterung geladen wurde.', 'CZendDataCache requires PHP Zend Data Cache extension to be loaded.' => 'CZendDataCache efordert, dass die PHP Zend Data Cache Erweiterung geladen wurde.', + 'Can not generate multiple insert command with empty data set.' => 'Ein multi-INSERT-command kann nicht mit leerem Datensatz erstellt werden.', 'Cannot add "{child}" as a child of "{name}". A loop has been detected.' => 'Kann "{child}" nicht als Kind von "{name}" hinzufĂĽgen. Es wurde eine Schleife entdeckt.', 'Cannot add "{child}" as a child of "{parent}". A loop has been detected.' => 'Kann "{child}" nicht als Kind von "{parent}" hinzufĂĽgen. Es wurde eine Schleife entdeckt.', 'Cannot add "{name}" as a child of itself.' => 'Kann "{name}" nicht als Kind von sich selbst hinzufĂĽgen.', diff --git a/framework/messages/fi/yii.php b/framework/messages/fi/yii.php index d20fd61af57..e8344d7e9bd 100644 --- a/framework/messages/fi/yii.php +++ b/framework/messages/fi/yii.php @@ -17,6 +17,13 @@ * NOTE, this file must be saved in UTF-8 encoding. */ return array ( + 'Can not generate multiple insert command with empty data set.' => 'Tyhjällä tietojoukolla ei voida luoda useita insert-komentoja.', + 'Encryption key length can be {keyLengths}.' => 'Salausavaimen pituus voi olla {keyLengths}.', + 'Encryption key length must be between {minLength} and {maxLength}.' => 'Salausavaimen pituus täytyy olla {minLength} ja {maxLength} väliltä.', + 'Encryption key should be a string.' => 'Salausavaimen tulee olla merkkijono.', + 'Failed to validate key. Supported key lengths of cipher not known.' => 'Avain kelpoisuustarkistus epäonnistui. Salakirjoitusjärjestelmän tuettuja avainpituuksia ei tiedetä.', + 'No encryption key specified.' => 'Salausavainta ei ole määritetty.', + 'Unable to upload the file "{file}" because of an unrecognized error.' => 'Ei voi ladata tiedostoa "{file}" tuntemattoman virheen vuoksi.', '"{path}" is not a valid directory.' => '"{path}" ei ole kelvollinen hakemisto.', '< Previous' => '< Edellinen', '<< First' => '<< Ensimmäinen', @@ -78,7 +85,6 @@ 'CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.' => 'CProfileLogRoute löysi yhteensopimattoman koodilohkon "{token}". Varmista, että Yii::beginProfile() ja Yii::endProfile() -kutsut on sisennetty oikein.', 'CProfileLogRoute.report "{report}" is invalid. Valid values include "summary" and "callstack".' => 'CProfileLogRoute.report "{report}" on virheellinen. Arvoiksi kelpaavat "summary" ja "callstack".', 'CSecurityManager requires PHP mcrypt extension to be loaded in order to use data encryption feature.' => 'CSecurityManager vaatii, että PHP:n mcrypt-laajennus on ladattu, jotta tiedonsalaustoiminnallisuutta voi käyttää.', - 'CSecurityManager.encryptionKey cannot be empty.' => 'CSecurityManager.encryptionKey ei voi olla tyhjä.', 'CSecurityManager.validationKey cannot be empty.' => 'CSecurityManager.validationKey ei voi olla tyhjä.', 'CSecurityManager::generateRandomString() cannot generate random string in the current environment.' => 'CSecurityManager::generateRandomString() ei pysty generoimaan satunnaista merkkijonoa nykyisessä ympäristössä.', 'CTypedList<{type}> can only hold objects of {type} class.' => 'CTypedList<{type}> hyväksyy vain {type}-luokan olioita.', @@ -149,7 +155,6 @@ 'The "range" property must be specified with a list of values.' => '"range"-ominaisuudelle pitää määrittää arvoluettelo.', 'The $converter argument must be a valid callback or null.' => '$converter argumentin täytyy olla kelvollinen callback tai null.', 'The CSRF token could not be verified.' => 'CSRF-tokenia ei voitu todentaa.', - 'The DB query must contain the "from" portion.' => 'Tietokantakyselyssä pitää olla "from"-osuus.', 'The STAT relation "{name}" cannot have child relations.' => 'STAT-relaatiolla "{name}" ei voi olla lapsirelaatioita.', 'The URL pattern "{pattern}" for route "{route}" is not a valid regular expression.' => 'URL-ratkaisumalli "{pattern}" reitille "{route}" ei ole kelvollinen säännöllinen lauseke.', 'The active record cannot be deleted because it is new.' => 'Active recordia ei voi poistaa, koska se on uusi.', diff --git a/framework/messages/fr/yii.php b/framework/messages/fr/yii.php index 4c131c15fbc..e6fdb682f4f 100644 --- a/framework/messages/fr/yii.php +++ b/framework/messages/fr/yii.php @@ -160,6 +160,7 @@ 'The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.' => 'Le fichier « {file} » est trop petit. Sa taille ne peut ĂŞtre infĂ©rieure Ă  {limit} octets.', 'The file "{file}" was only partially uploaded.' => 'Le fichier « {file} » a Ă©tĂ© tĂ©lĂ©chargĂ© partiellement.', 'The first element in a filter configuration must be the filter class.' => 'Le premier Ă©lĂ©ment de la configuration d\'un filtre doit ĂŞtre la classe filtre.', + 'The format of {attribute} is invalid.' => 'Le format de {attribute} est invalide.', 'The item "{name}" does not exist.' => 'L\'Ă©lĂ©ment « {name} » est inexistant.', 'The item "{parent}" already has a child "{child}".' => 'L\'Ă©lĂ©ment « {parent} » a dĂ©jĂ  un enfant « {child} ».', 'The layout path "{path}" is not a valid directory.' => 'Le chemin d\'accès « {path} » au gabarit n\'est pas un dossier valide.', diff --git a/framework/messages/it/zii.php b/framework/messages/it/zii.php index 7fe96bc6276..3eab7b5343a 100644 --- a/framework/messages/it/zii.php +++ b/framework/messages/it/zii.php @@ -1,36 +1,36 @@ - 'Home', - 'The button type "{type}" is not supported.' => 'Il pulsante "{type}" non è supportato.', - 'Are you sure you want to delete this item?' => 'Sei sicuro di voler cancellare questo oggetto?', - 'Delete' => 'Cancella', - 'Displaying {start}-{end} of 1 result.|Displaying {start}-{end} of {count} results.' => 'Visualizzazione {start}-{end} di 1 risultato.|Visualizzazione {start}-{end} di {count} risultati.', - 'Either "name" or "value" must be specified for CDataColumn.' => 'Per CDataColumn deve essere specificato o il "name" o il "value".', - 'No results found.' => 'Nessun risultato trovato.', - 'Not set' => 'Non impostato', - 'Please specify the "attributes" property.' => 'Occorre specificare la proprietĂ  "attributes".', - 'Please specify the "data" property.' => 'Occorre specificare la proprietĂ  "data".', - 'Sort by: ' => 'Ordina per:', - 'The "dataProvider" property cannot be empty.' => 'La proprietĂ  "dataProvider" non può essere vuota.', - 'The attribute must be specified in the format of "Name:Type:Label", where "Type" and "Label" are optional.' => 'L\'attributo deve essere specificato nel formato "Name:Type:Label", dove "Type" e "Label" sono opzionali.', - 'The column must be specified in the format of "Name:Type:Label", where "Type" and "Label" are optional.' => 'La colonna deve essere specificata nel formato "Name:Type:Label", dove "Type" e "Label" sono opzionali.', - 'The property "itemView" cannot be empty.' => 'La proprietĂ  "itemView" non può essere vuota.', - 'Total 1 result.|Total {count} results.' => 'Totale di 1 risultato.|Totale di {count} risultati.', - 'Update' => 'Aggiorna', - 'View' => 'Vedi', - '{class} must specify "model" and "attribute" or "name" property values.' => '{class} deve specificare il/i valore/i delle proprietĂ  "model" e "attribute" o "name".', + 'Home', + 'The button type "{type}" is not supported.' => 'Il pulsante "{type}" non è supportato.', + 'Are you sure you want to delete this item?' => 'Sei sicuro di voler cancellare questo oggetto?', + 'Delete' => 'Cancella', + 'Displaying {start}-{end} of 1 result.|Displaying {start}-{end} of {count} results.' => 'Visualizzazione {start}-{end} di 1 risultato.|Visualizzazione {start}-{end} di {count} risultati.', + 'Either "name" or "value" must be specified for CDataColumn.' => 'Per CDataColumn deve essere specificato o il "name" o il "value".', + 'No results found.' => 'Nessun risultato trovato.', + 'Not set' => 'Non impostato', + 'Please specify the "attributes" property.' => 'Occorre specificare la proprietĂ  "attributes".', + 'Please specify the "data" property.' => 'Occorre specificare la proprietĂ  "data".', + 'Sort by: ' => 'Ordina per:', + 'The "dataProvider" property cannot be empty.' => 'La proprietĂ  "dataProvider" non può essere vuota.', + 'The attribute must be specified in the format of "Name:Type:Label", where "Type" and "Label" are optional.' => 'L\'attributo deve essere specificato nel formato "Name:Type:Label", dove "Type" e "Label" sono opzionali.', + 'The column must be specified in the format of "Name:Type:Label", where "Type" and "Label" are optional.' => 'La colonna deve essere specificata nel formato "Name:Type:Label", dove "Type" e "Label" sono opzionali.', + 'The property "itemView" cannot be empty.' => 'La proprietĂ  "itemView" non può essere vuota.', + 'Total 1 result.|Total {count} results.' => 'Totale di 1 risultato.|Totale di {count} risultati.', + 'Update' => 'Aggiorna', + 'View' => 'Vedi', + '{class} must specify "model" and "attribute" or "name" property values.' => '{class} deve specificare il/i valore/i delle proprietĂ  "model" e "attribute" o "name".', ); \ No newline at end of file diff --git a/framework/messages/lv/zii.php b/framework/messages/lv/zii.php index a074ccb6fc3..ecfe5a747e8 100644 --- a/framework/messages/lv/zii.php +++ b/framework/messages/lv/zii.php @@ -1,34 +1,34 @@ - 'Vai patiešÄm vÄ“laties dzÄ“st šo vienÄ«bu?', - 'Delete' => 'DzÄ“st', - 'Displaying {start}-{end} of 1 result.|Displaying {start}-{end} of {count} results.' => 'AttÄ“lo no {start}. lÄ«dz {end}. no pavisam {count} ieraksta(-iem).', - 'Either "name" or "value" must be specified for CDataColumn.' => 'CDataColumn nepieciešams norÄdÄ«t "name" vai "value".', - 'No results found.' => 'Dati netika atrasti.', - 'Not set' => 'Nav uzstÄdÄ«ts', - 'Please specify the "attributes" property.' => 'NorÄdiet mainÄ«go "attributes".', - 'Please specify the "data" property.' => 'NorÄdiet mainÄ«go "data".', - 'Sort by: ' => 'KÄrtot pÄ“c: ', - 'The "dataProvider" property cannot be empty.' => 'MainÄ«gais "dataProvider" nedrÄ«kst bĹ«t tukšs.', - 'The attribute must be specified in the format of "Name:Type:Label", where "Type" and "Label" are optional.' => 'AtribĹ«tam ir jÄbĹ«t norÄdÄ«tam formÄtÄ "Nosaukums:Veids:Apraksts", kur "Veids" un "Apraksts" ir obligÄti.', - 'The column must be specified in the format of "Name:Type:Label", where "Type" and "Label" are optional.' => 'Kolonnai ir jÄbĹ«t iformÄtÄ "Nosaukums:Veids:Apraksts", kur "Veids" un "Apraksts" ir obligÄti.', - 'The property "itemView" cannot be empty.' => 'MainÄ«gais "itemView" nedrÄ«kst bĹ«t tukšs.', - 'Total 1 result.|Total {count} results.' => 'KopÄ {count} ieraksts(-i).', - 'Update' => 'Labot', - 'View' => 'SkatÄ«ties', - '{class} must specify "model" and "attribute" or "name" property values.' => 'Klasei {class} ir jÄbĹ«t norÄdÄ«tiem vienam no mainÄ«gajiem "attribute" vai "name".', -); + 'Vai patiešÄm vÄ“laties dzÄ“st šo vienÄ«bu?', + 'Delete' => 'DzÄ“st', + 'Displaying {start}-{end} of 1 result.|Displaying {start}-{end} of {count} results.' => 'AttÄ“lo no {start}. lÄ«dz {end}. no pavisam {count} ieraksta(-iem).', + 'Either "name" or "value" must be specified for CDataColumn.' => 'CDataColumn nepieciešams norÄdÄ«t "name" vai "value".', + 'No results found.' => 'Dati netika atrasti.', + 'Not set' => 'Nav uzstÄdÄ«ts', + 'Please specify the "attributes" property.' => 'NorÄdiet mainÄ«go "attributes".', + 'Please specify the "data" property.' => 'NorÄdiet mainÄ«go "data".', + 'Sort by: ' => 'KÄrtot pÄ“c: ', + 'The "dataProvider" property cannot be empty.' => 'MainÄ«gais "dataProvider" nedrÄ«kst bĹ«t tukšs.', + 'The attribute must be specified in the format of "Name:Type:Label", where "Type" and "Label" are optional.' => 'AtribĹ«tam ir jÄbĹ«t norÄdÄ«tam formÄtÄ "Nosaukums:Veids:Apraksts", kur "Veids" un "Apraksts" ir obligÄti.', + 'The column must be specified in the format of "Name:Type:Label", where "Type" and "Label" are optional.' => 'Kolonnai ir jÄbĹ«t iformÄtÄ "Nosaukums:Veids:Apraksts", kur "Veids" un "Apraksts" ir obligÄti.', + 'The property "itemView" cannot be empty.' => 'MainÄ«gais "itemView" nedrÄ«kst bĹ«t tukšs.', + 'Total 1 result.|Total {count} results.' => 'KopÄ {count} ieraksts(-i).', + 'Update' => 'Labot', + 'View' => 'SkatÄ«ties', + '{class} must specify "model" and "attribute" or "name" property values.' => 'Klasei {class} ir jÄbĹ«t norÄdÄ«tiem vienam no mainÄ«gajiem "attribute" vai "name".', +); diff --git a/framework/messages/nl/yii.php b/framework/messages/nl/yii.php index 2c58e99a62b..953553d1bd6 100644 --- a/framework/messages/nl/yii.php +++ b/framework/messages/nl/yii.php @@ -32,7 +32,7 @@ 'Altering a DB column is not supported by SQLite.' => 'Een DB kolom aanpassen wordt niet ondersteund door SQLite.', 'Application Log' => 'Applicatielogboek', 'Application base path "{path}" is not a valid directory.' => 'Het base path voor de applicatie "{path}" is ongeldig.', - 'Application runtime path "{path}" is not valid. Please make sure it is a directory writable by the Web server process.' => 'Het runtime path voor de applicatie "{path}" is ongeldig. Dit moet een map zijn die door de web server kan aangepast worden.', + 'Application runtime path "{path}" is not valid. Please make sure it is a directory writable by the Web server process.' => 'Het runtime path voor de applicatie "{path}" is ongeldig. Dit moet een map zijn die door de web server aangepast kan worden.', 'Authorization item "{item}" has already been assigned to user "{user}".' => 'Authorisatie-item "{item}" was al aan gebruiker "{user}" toegewezen.', 'Base path "{path}" is not a valid directory.' => 'Het base path "{path}" is ongeldig.', 'CApcCache requires PHP apc extension to be loaded.' => 'Voor het gebruik van CApcCache moet de apc extensie geladen zijn.', @@ -232,7 +232,7 @@ '{attribute} is in the list.' => '{attribute} maakt deel uit van de lijst.', '{attribute} is invalid.' => '{attribute} is ongeldig.', '{attribute} is not a valid URL.' => '{attribute} is geen geldige URL.', - '{attribute} is not a valid email address.' => '{attribute} is geen geldig emailadres.', + '{attribute} is not a valid email address.' => '{attribute} is geen geldig e-mailadres.', '{attribute} is not in the list.' => '{attribute} zit niet in de lijst.', '{attribute} is of the wrong length (should be {length} characters).' => '{attribute} heeft de verkeerde lengte (het zou {length} karakters lang moeten zijn)', '{attribute} is too big (maximum is {max}).' => '{attribute} is te groot (het maximum is {max})', diff --git a/framework/messages/ru/yii.php b/framework/messages/ru/yii.php index 156cd038ff9..2ae0117248b 100644 --- a/framework/messages/ru/yii.php +++ b/framework/messages/ru/yii.php @@ -41,7 +41,7 @@ 'CApcCache requires PHP apc extension to be loaded.' => 'CApcCache требŃет загрŃженного раŃŃирения PHP APC.', 'CAssetManager.basePath "{path}" is invalid. Please make sure the directory exists and is writable by the Web server process.' => 'ĐźŃŃ‚ŃŚ CAssetManager.basePath "{path}" Đ·Đ°Đ´Đ°Đ˝ неверно. ĐŁĐ´ĐľŃтоверьтеŃŃŚ, что директория ŃŃщеŃтвŃет и Đ´ĐľŃŃ‚Ńпна для запиŃи пользователю, под которым Đ·Đ°ĐżŃщен веб-Ńервер.', 'CCacheHttpSession.cacheID is invalid. Please make sure "{id}" refers to a valid cache application component.' => 'Đдентификатор CCacheHttpSession.cacheID Đ·Đ°Đ´Đ°Đ˝ неверно. ĐŁĐ´ĐľŃтоверьтеŃŃŚ, что "{id}" ŃоответŃтвŃет ŃŃщеŃтвŃŃŽŃ‰ĐµĐĽŃ ĐşĐľĐĽĐżĐľĐ˝ĐµĐ˝Ń‚Ń ĐşŃŤŃĐ° приложения.', - 'CCaptchaValidator.action "{id}" is invalid. Unable to find such an action in the current controller.' => 'ДейŃтвие CCaptchaValidator.action "{id}" задано неверно: дейŃтвия не найдено в текŃщем контроллере.', + 'CCaptchaValidator.action "{id}" is invalid. Unable to find such an action in the current controller.' => 'ДейŃтвие CCaptchaValidator.action "{id}" задано неверно: дейŃтвие не найдено в текŃщем контроллере.', 'CDbAuthManager.connectionID "{id}" is invalid. Please make sure it refers to the ID of a CDbConnection application component.' => 'Đдентификатор CDbAuthManager.connectionID "{id}" Đ·Đ°Đ´Đ°Đ˝ неверно. ĐŁĐ´ĐľŃтоверьтеŃŃŚ, что он ŃоответŃтвŃет Đ¸Đ´ĐµĐ˝Ń‚Đ¸Ń„Đ¸ĐşĐ°Ń‚ĐľŃ€Ń ĐşĐľĐĽĐżĐľĐ˝ĐµĐ˝Ń‚Đ° CDbConnection ваŃего приложения.', 'CDbCache.connectionID "{id}" is invalid. Please make sure it refers to the ID of a CDbConnection application component.' => 'Đдентификатор CDbCache.connectionID "{id}" Đ·Đ°Đ´Đ°Đ˝ неверно. ĐŁĐ´ĐľŃтоверьтеŃŃŚ, что он ŃоответŃтвŃет Đ¸Đ´ĐµĐ˝Ń‚Đ¸Ń„Đ¸ĐşĐ°Ń‚ĐľŃ€Ń ĐşĐľĐĽĐżĐľĐ˝ĐµĐ˝Ń‚Đ° CDbConnection ваŃего приложения.', 'CDbCacheDependency.connectionID "{id}" is invalid. Please make sure it refers to the ID of a CDbConnection application component.' => 'Неправильный CDbCacheDependency.connectionID "{id}". УбедитеŃŃŚ, что ID Ńказывает на ID компонента приложения CDbConnection.', @@ -67,7 +67,7 @@ 'CFlexWidget.baseUrl cannot be empty.' => 'Параметр CFlexWidget.baseUrl должен быть заполнен.', 'CFlexWidget.name cannot be empty.' => 'Параметр CFlexWidget.name должен быть заполнен.', 'CGlobalStateCacheDependency.stateName cannot be empty.' => 'Параметр CGlobalStateCacheDependency.stateName должен быть заполнен.', - 'CHttpCacheFilter.lastModified contained a value that could not be understood by strtotime()' => 'CHttpCacheFilter.lastModified Ńодержит знаение, которое не ŃдалоŃŃŚ разобрать при помощи strtotime()', + 'CHttpCacheFilter.lastModified contained a value that could not be understood by strtotime()' => 'CHttpCacheFilter.lastModified Ńодержит значение, которое не ŃдалоŃŃŚ разобрать при помощи strtotime()', 'CHttpCookieCollection can only hold CHttpCookie objects.' => 'CHttpCookieCollection может Ńодержать только объекты типа CHttpCookie.', 'CHttpRequest is unable to determine the entry script URL.' => 'ĐšĐľĐĽĐżĐľĐ˝ĐµĐ˝Ń‚Ń CHttpRequest не ŃдалоŃŃŚ определить URL входного Ńкрипта.', 'CHttpRequest is unable to determine the path info of the request.' => 'ĐšĐľĐĽĐżĐľĐ˝ĐµĐ˝Ń‚Ń CHttpRequest не ŃдалоŃŃŚ определить информацию Đľ ĐżŃти, ŃодержащŃŃŽŃŃŹ в запроŃе.', @@ -262,7 +262,7 @@ '{class} has an invalid validation rule. The rule must specify attributes to be validated and the validator name.' => '{class} Ńодержит неверное правило проверки. Правило должно иметь имя и включать элементы для проверки.', '{class} must specify "model" and "attribute" or "name" property values.' => 'Đ’ клаŃŃе {class} должны быть определены значения ŃвойŃтв "model" и "attribute", либо "name".', '{class} requires the Blowfish option of the PHP crypt() function. This system does not have it.' => '{class} требŃет наличия отŃŃŃ‚ŃтвŃющего в ваŃей ŃиŃтеме Blowfish для Ń„Ńнкции PHP crypt().', - '{class} requires the PHP crypt() function. This system does not have it.' => '{class} требŃет наличия отŃŃŃ‚ŃтвŃюей в ваŃей ŃиŃтеме Ń„Ńнкции PHP crypt().', + '{class} requires the PHP crypt() function. This system does not have it.' => '{class} требŃет наличия отŃŃŃ‚ŃтвŃющей в ваŃей ŃиŃтеме Ń„Ńнкции PHP crypt().', '{class}.allowAutoLogin must be set true in order to use cookie-based authentication.' => 'Для иŃпользования авторизации, поŃтроенной на cookie, ŃвойŃтво {class}.allowAutoLogin должно принять значение "true".', '{class}::$cost must be a number.' => '{class}::$cost должен быть чиŃлом.', '{class}::$cost must be between 4 and 31.' => '{class}::$cost должен быть от 4 Đ´Đľ 31.', diff --git a/framework/messages/th/yii.php b/framework/messages/th/yii.php index 0c15e590835..ada7e062532 100644 --- a/framework/messages/th/yii.php +++ b/framework/messages/th/yii.php @@ -1,211 +1,211 @@ - '< หน้าทีŕąŕąŕ¸Ąŕą‰ŕ¸§', - '<< First' => '<< หน้าŕąŕ¸Łŕ¸', - 'Go to page: ' => 'ไปทีŕąŕ¸«ŕ¸™ŕą‰ŕ¸˛: ', - 'Last >>' => 'หน้าสุดท้าย >>', - 'Next >' => 'หน้าถัดไป >', - 'The asset "{asset}" to be published does not exist.' => 'ไมŕąŕ¸žŕ¸šŕ¸‚้อมูล "{asset}" ทีŕąŕ¸–ูŕ¸ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™', - '"{path}" is not a valid directory.' => 'ไมŕąŕ¸žŕ¸šŕą„ดเรŕ¸ŕ¸—อรี๠"{path}"', - 'Active Record requires a "db" CDbConnection application component.' => 'ต้องŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕ¸‚้อมูล "db" ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Ł CDbConnection ตามโครงสร้างของระบบ', - 'Active record "{class}" has an invalid configuration for relation "{relation}". It must specify the relation type, the related active record class and the foreign key.' => 'ข้อมูลทีŕąŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ "{class}" ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ŕ¸ŕ¸łŕ¸«ŕ¸™ŕ¸”ความสัมพันŕ¸ŕąŚ "{relation}" ได้ โดยŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸—ำŕ¸ŕ¸˛ŕ¸Łŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕ¸„วามสัมพันŕ¸ŕąŚ ซึŕąŕ¸‡ŕ¸‚้อมูลทีŕąŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸„ีย์เชืŕąŕ¸­ŕ¸ˇŕ¸„วามสัมพันŕ¸ŕąŚŕąŕ¸™ŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡ŕ¸‚้อมูล', - 'Active record "{class}" is trying to select an invalid column "{column}". Note, the column must exist in the table or be an expression with alias.' => 'ข้อมูลทีŕąŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ "{class}" ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เลือŕ¸ŕ¸źŕ¸´ŕ¸Ąŕ¸”์ "{column}" ได้ คำŕąŕ¸™ŕ¸°ŕ¸™ŕ¸ł, ชืŕąŕ¸­ŕ¸źŕ¸´ŕ¸Ąŕ¸”์ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ˇŕ¸µŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡ŕ¸«ŕ¸Łŕ¸·ŕ¸­ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸Šŕ¸·ŕąŕ¸­ŕą€ŕ¸Şŕ¸ˇŕ¸·ŕ¸­ŕ¸™ŕ¸‚องฟิลด์อืŕąŕ¸™ŕą†', - 'Alias "{alias}" is invalid. Make sure it points to an existing directory or file.' => 'ชืŕąŕ¸­ŕą€ŕ¸Şŕ¸ˇŕ¸·ŕ¸­ŕ¸™ "{alias}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸šŕ¸„วามถูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸‚องไดเรŕ¸ŕ¸—อรีŕąŕ¸«ŕ¸Łŕ¸·ŕ¸­ŕą„ฟล์', - 'Application base path "{path}" is not a valid directory.' => 'ทีŕąŕ¸­ŕ¸˘ŕ¸ąŕąŕą‚ปรŕąŕ¸ŕ¸Łŕ¸ˇŕ¸«ŕ¸Ąŕ¸±ŕ¸ "{path}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', - 'Application runtime path "{path}" is not valid. Please make sure it is a directory writable by the Web server process.' => 'ทีŕąŕ¸­ŕ¸˘ŕ¸ąŕąŕą‚ปรŕąŕ¸ŕ¸Łŕ¸ˇ "{path}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸šŕ¸§ŕąŕ¸˛ŕą„ดเรŕ¸ŕ¸—อรีŕąŕ¸™ŕ¸±ŕą‰ŕ¸™ŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕąŕ¸Ąŕ¸°ŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เขียนได้โดยคำสัŕąŕ¸‡ŕ¸‚องเซิฟเวอร์', - 'Authorization item "{item}" has already been assigned to user "{user}".' => 'รายŕ¸ŕ¸˛ŕ¸Ł "{item}" ได้ถูŕ¸ŕ¸ŕ¸łŕ¸«ŕ¸™ŕ¸”ŕąŕ¸«ŕą‰ŕ¸ŕ¸±ŕ¸šŕ¸śŕ¸ąŕą‰ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ "{user}" เรียบร้อยŕąŕ¸Ąŕą‰ŕ¸§', - 'CApcCache requires PHP apc extension to be loaded.' => 'CApcCache ต้องŕ¸ŕ¸˛ŕ¸Ł PHP apc ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™', - 'CAssetManager.basePath "{path}" is invalid. Please make sure the directory exists and is writable by the Web server process.' => 'CAssetManager.basePath "{path}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸šŕ¸§ŕąŕ¸˛ŕą„ดเรŕ¸ŕ¸—อรีŕąŕ¸™ŕ¸±ŕą‰ŕ¸™ŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕąŕ¸Ąŕ¸°ŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เขียนได้โดยคำสัŕąŕ¸‡ŕ¸‚องเซิฟเวอร์', - 'CCacheHttpSession.cacheID is invalid. Please make sure "{id}" refers to a valid cache application component.' => 'CCacheHttpSession.cacheID ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸š "{id}" วŕąŕ¸˛ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸„ŕąŕ¸˛ŕ¸•ŕ¸˛ŕ¸ˇŕą‚ครงสร้างของหนŕąŕ¸§ŕ¸˘ŕ¸„วามŕ¸ŕ¸łŕ¸Šŕ¸±ŕąŕ¸§ŕ¸„ราว (Cache) หรือไมŕą', - 'CCaptchaValidator.action "{id}" is invalid. Unable to find such an action in the current controller.' => 'CCaptchaValidator.action "{id}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ค้นหาŕ¸ŕ¸˛ŕ¸Łŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕąŕ¸™ŕ¸Şŕąŕ¸§ŕ¸™ŕ¸„วบคุมปัŕ¸ŕ¸ŕ¸¸ŕ¸šŕ¸±ŕ¸™ŕą„ด้', - 'CDbAuthManager.connectionID "{id}" is invalid. Please make sure it refers to the ID of a CDbConnection application component.' => 'CDbAuthManager.connectionID "{id}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸š ID วŕąŕ¸˛ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸„ŕąŕ¸˛ŕ¸ŕ¸˛ŕ¸ CDbConnection ตามโครงสร้างของระบบหรือไมŕą', - 'CDbCache.connectionID "{id}" is invalid. Please make sure it refers to the ID of a CDbConnection application component.' => 'CDbCache.connectionID "{id}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸š ID วŕąŕ¸˛ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸„ŕąŕ¸˛ŕ¸ŕ¸˛ŕ¸ CDbConnection ตามโครงสร้างของระบบหรือไมŕą', - 'CDbCacheDependency.sql cannot be empty.' => 'CDbCacheDependency.sql ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เป็นคŕąŕ¸˛ŕ¸§ŕąŕ¸˛ŕ¸‡ŕą„ด้', - 'CDbCommand failed to execute the SQL statement: {error}' => 'CDbCommand ล้มเหลวŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ŕ¸Ąŕ¸±ŕ¸ŕ¸©ŕ¸“ะของ SQL: {error}', - 'CDbCommand failed to prepare the SQL statement: {error}' => 'CDbCommand ล้มเหลวŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸ŕ¸ŕąŕ¸ŕ¸‡ŕ¸Ąŕ¸±ŕ¸ŕ¸©ŕ¸“ะของ SQL: {error}', - 'CDbConnection does not support reading schema for {driver} database.' => 'CDbConnection ไมŕąŕ¸Łŕ¸­ŕ¸‡ŕ¸Łŕ¸±ŕ¸šŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕ¸­ŕąŕ¸˛ŕ¸™ŕ¸Łŕ¸˛ŕ¸˘ŕ¸Ąŕ¸°ŕą€ŕ¸­ŕ¸µŕ¸˘ŕ¸”โครงสร้างสำหรับ {driver} ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸™ŕ¸‚้อมูลได้', - 'CDbConnection failed to open the DB connection: {error}' => 'CDbConnection ล้มเหลวŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Šŕ¸·ŕąŕ¸­ŕ¸ˇŕ¸•ŕąŕ¸­ŕ¸ŕ¸˛ŕ¸™ŕ¸‚้อมูล: {error}', - 'CDbConnection is inactive and cannot perform any DB operations.' => 'CDbConnection ไมŕąŕą„ด้ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕąŕ¸Ąŕ¸°ŕą„มŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ดำเนินŕ¸ŕ¸˛ŕ¸Łŕ¸ŕ¸±ŕ¸šŕ¸ŕ¸˛ŕ¸™ŕ¸‚้อมูลŕąŕ¸”ๆได้', - 'CDbConnection.connectionString cannot be empty.' => 'CDbConnection.connectionString ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เป็นคŕąŕ¸˛ŕ¸§ŕąŕ¸˛ŕ¸‡ŕą„ด้', - 'CDbDataReader cannot rewind. It is a forward-only reader.' => 'CDbDataReader ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ย้อนŕ¸ŕ¸Ąŕ¸±ŕ¸šŕą„ด้, ถูŕ¸ŕ¸ŕ¸łŕ¸«ŕ¸™ŕ¸”ŕąŕ¸«ŕą‰ŕ¸­ŕąŕ¸˛ŕ¸™ŕą„ด้เพียงอยŕąŕ¸˛ŕ¸‡ŕą€ŕ¸”ียว', - 'CDbHttpSession.connectionID "{id}" is invalid. Please make sure it refers to the ID of a CDbConnection application component.' => 'CDbHttpSession.connectionID "{id}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸š ID วŕąŕ¸˛ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸„ŕąŕ¸˛ŕ¸ŕ¸˛ŕ¸ CDbConnection ตามโครงสร้างของระบบหรือไมŕą', - 'CDbLogRoute requires database table "{table}" to store log messages.' => 'CDbLogRoute ต้องŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡ "{table}" เพืŕąŕ¸­ŕą€ŕ¸ŕą‡ŕ¸šŕ¸šŕ¸±ŕ¸™ŕ¸—ึŕ¸ŕ¸‚้อความ', - 'CDbLogRoute.connectionID "{id}" does not point to a valid CDbConnection application component.' => 'CDbLogRoute.connectionID "{id}" ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ระบุคŕąŕ¸˛ŕ¸—ีŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸‚อง CDbConnection ตามโครงสร้างของระบบได้', - 'CDbMessageSource.connectionID is invalid. Please make sure "{id}" refers to a valid database application component.' => 'CDbMessageSource.connectionID ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸šŕ¸§ŕąŕ¸˛ "{id}" เป็นข้อมูลทีŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸•ŕ¸˛ŕ¸ˇŕą‚ครงสร้างของŕ¸ŕ¸˛ŕ¸™ŕ¸‚้อมูลหรือไมŕą', - 'CDbTransaction is inactive and cannot perform commit or roll back operations.' => 'CDbTransaction ไมŕąŕą„ด้ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕąŕ¸Ąŕ¸°ŕą„มŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ดำเนินŕ¸ŕ¸˛ŕ¸Łŕ¸ŕ¸Łŕ¸°ŕ¸—ำตŕąŕ¸­ŕ¸«ŕ¸Łŕ¸·ŕ¸­ŕ¸˘ŕą‰ŕ¸­ŕ¸™ŕ¸ŕ¸Ąŕ¸±ŕ¸šŕ¸ŕ¸˛ŕ¸Łŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕą„ด้', - 'CDirectoryCacheDependency.directory cannot be empty.' => 'CDirectoryCacheDependency.directory ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เป็นคŕąŕ¸˛ŕ¸§ŕąŕ¸˛ŕ¸‡ŕą„ด้', - 'CFileCacheDependency.fileName cannot be empty.' => 'CFileCacheDependency.fileName ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เป็นคŕąŕ¸˛ŕ¸§ŕąŕ¸˛ŕ¸‡ŕą„ด้', - 'CFileLogRoute.logPath "{path}" does not point to a valid directory. Make sure the directory exists and is writable by the Web server process.' => 'CFileLogRoute.logPath "{path}" ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ชี้ไดเรŕ¸ŕ¸—อรีŕąŕ¸—ีŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕą„ด้ ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸šŕ¸§ŕąŕ¸˛ŕą„ดเรŕ¸ŕ¸—อรีŕąŕ¸™ŕ¸±ŕą‰ŕ¸™ŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕąŕ¸Ąŕ¸°ŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เขียนได้โดยคำสัŕąŕ¸‡ŕ¸‚องเซิฟเวอร์', - 'CFilterChain can only take objects implementing the IFilter interface.' => 'CFilterChain สามารถเรียŕ¸ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕ¸ŕ¸±ŕ¸šŕ¸‚้อมูลทีŕąŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕ¸Łŕ¸ąŕ¸›ŕąŕ¸šŕ¸šŕ¸‚อง IFilter ได้', - 'CFlexWidget.baseUrl cannot be empty.' => 'CFlexWidget.baseUrl ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เป็นคŕąŕ¸˛ŕ¸§ŕąŕ¸˛ŕ¸‡ŕą„ด้', - 'CFlexWidget.name cannot be empty.' => 'CFlexWidget.name ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เป็นคŕąŕ¸˛ŕ¸§ŕąŕ¸˛ŕ¸‡ŕą„ด้', - 'CGlobalStateCacheDependency.stateName cannot be empty.' => 'CGlobalStateCacheDependency.stateName ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เป็นคŕąŕ¸˛ŕ¸§ŕąŕ¸˛ŕ¸‡ŕą„ด้', - 'CHttpCookieCollection can only hold CHttpCookie objects.' => 'CHttpCookieCollection สามารถŕąŕ¸Šŕą‰ŕ¸ŕ¸±ŕ¸š CHttpCookie ได้เทŕąŕ¸˛ŕ¸™ŕ¸±ŕą‰ŕ¸™', - 'CHttpRequest is unable to determine the entry script URL.' => 'CHttpRequest ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ตรวŕ¸ŕ¸Şŕ¸­ŕ¸šŕ¸„ำสัŕąŕ¸‡ŕ¸—ีŕąŕ¸­ŕ¸˘ŕ¸ąŕąŕ¸‚องสคริปต์ได้', - 'CHttpRequest is unable to determine the path info of the request.' => 'CHttpRequest ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ตรวŕ¸ŕ¸Şŕ¸­ŕ¸šŕ¸‚้อมูลทีŕąŕ¸­ŕ¸˘ŕ¸ąŕąŕ¸•ŕ¸˛ŕ¸ˇŕ¸„ำขอได้', - 'CHttpRequest is unable to determine the request URI.' => 'CHttpRequest ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ตรวŕ¸ŕ¸Şŕ¸­ŕ¸š URI ตามคำขอได้', - 'CHttpSession.cookieMode can only be "none", "allow" or "only".' => 'CHttpSession.cookieMode ระบุคŕąŕ¸˛ŕą€ŕ¸›ŕą‡ŕ¸™ "none", "allow" หรือ "only" เทŕąŕ¸˛ŕ¸™ŕ¸±ŕą‰ŕ¸™', - 'CHttpSession.gcProbability "{value}" is invalid. It must be an integer between 0 and 100.' => 'CHttpSession.gcProbability "{value}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ คŕąŕ¸˛ŕ¸—ีŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸ŕ¸łŕ¸™ŕ¸§ŕ¸™ŕą€ŕ¸•ŕą‡ŕ¸ˇŕ¸Łŕ¸°ŕ¸«ŕ¸§ŕąŕ¸˛ŕ¸‡ 0 ถึง 100 เทŕąŕ¸˛ŕ¸™ŕ¸±ŕą‰ŕ¸™', - 'CHttpSession.savePath "{path}" is not a valid directory.' => 'CHttpSession.savePath "{path}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', - 'CMemCache requires PHP memcache extension to be loaded.' => 'CMemCache ต้องŕ¸ŕ¸˛ŕ¸Ł PHP memcache ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™', - 'CMemCache server configuration must be an array.' => 'CMemCache ของคŕąŕ¸˛ŕą€ŕ¸‹ŕ¸´ŕ¸źŕą€ŕ¸§ŕ¸­ŕ¸ŁŕąŚŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕąŕ¸šŕ¸šŕ¸­ŕ¸˛ŕ¸ŁŕąŚŕą€ŕ¸Łŕ¸˘ŕąŚ', - 'CMemCache server configuration must have "host" value.' => 'CMemCache ของคŕąŕ¸˛ŕą€ŕ¸‹ŕ¸´ŕ¸źŕą€ŕ¸§ŕ¸­ŕ¸ŁŕąŚŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸„ŕąŕ¸˛ŕ¸‚อง "host"', - 'CMultiFileUpload.name is required.' => 'CMultiFileUpload.name ต้องŕ¸ŕ¸˛ŕ¸Łŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™', - 'CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.' => 'CProfileLogRoute พบคำสัŕąŕ¸‡ŕ¸—ีŕąŕ¸«ŕą‰ŕ¸˛ŕ¸ˇŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ "{token}". ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸šŕ¸§ŕąŕ¸˛ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ Yii::beginProfile() ŕąŕ¸Ąŕ¸° Yii::endProfile() ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸–ูŕ¸ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕ¸„ุณสมบัติตŕąŕ¸˛ŕ¸‡ŕą†ŕ¸—ีŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', - 'CProfileLogRoute.report "{report}" is invalid. Valid values include "summary" and "callstack".' => 'CProfileLogRoute.report "{report}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ คŕąŕ¸˛ŕ¸—ีŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ˇŕ¸µŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ "summary" หรือ "callstack".', - 'CSecurityManager requires PHP mcrypt extension to be loaded in order to use data encryption feature.' => 'CSecurityManager ต้องŕ¸ŕ¸˛ŕ¸Ł PHP mcrypt ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕą€ŕ¸žŕ¸·ŕąŕ¸­ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕ¸„ุณสมบัติŕ¸ŕ¸˛ŕ¸Łŕ¸šŕ¸µŕ¸šŕ¸­ŕ¸±ŕ¸”ข้อมูล', - 'CSecurityManager.encryptionKey cannot be empty.' => 'CSecurityManager.encryptionKey ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เป็นคŕąŕ¸˛ŕ¸§ŕąŕ¸˛ŕ¸‡ŕą„ด้', - 'CSecurityManager.validation must be either "MD5" or "SHA1".' => 'CSecurityManager.validation ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕ¸Łŕ¸ąŕ¸›ŕąŕ¸šŕ¸š "MD5" หรือ "SHA1"', - 'CSecurityManager.validationKey cannot be empty.' => 'CSecurityManager.validationKey ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เป็นคŕąŕ¸˛ŕ¸§ŕąŕ¸˛ŕ¸‡ŕą„ด้', - 'CTypedList<{type}> can only hold objects of {type} class.' => 'CTypedList<{type}> สามารถŕąŕ¸Šŕą‰ŕ¸ŕ¸±ŕ¸šŕ¸Łŕ¸ąŕ¸›ŕąŕ¸šŕ¸š {type} ได้เทŕąŕ¸˛ŕ¸™ŕ¸±ŕą‰ŕ¸™', - 'CUrlManager.UrlFormat must be either "path" or "get".' => 'CUrlManager.UrlFormat ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ˇŕ¸µ "path" หรือ "get"', - 'CXCache requires PHP XCache extension to be loaded.' => 'CXCache ต้องŕ¸ŕ¸˛ŕ¸Ł PHP XCache ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™', - 'Cache table "{tableName}" does not exist.' => 'ไมŕąŕ¸žŕ¸š Cache ของตาราง "{tableName}"', - 'Cannot add "{child}" as a child of "{name}". A loop has been detected.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เพิŕąŕ¸ˇ "{child}" เป็นรายŕ¸ŕ¸˛ŕ¸Łŕ¸˘ŕąŕ¸­ŕ¸˘ŕ¸‚อง "{name}" ได้ ตรวŕ¸ŕ¸žŕ¸šŕ¸ŕ¸˛ŕ¸Łŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕ¸‹ŕą‰ŕ¸ł', - 'Cannot add "{child}" as a child of "{parent}". A loop has been detected.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เพิŕąŕ¸ˇ "{child}" เป็นรายŕ¸ŕ¸˛ŕ¸Łŕ¸˘ŕąŕ¸­ŕ¸˘ŕ¸‚อง "{parent}" ได้ ตรวŕ¸ŕ¸žŕ¸šŕ¸ŕ¸˛ŕ¸Łŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕ¸‹ŕą‰ŕ¸ł', - 'Cannot add "{name}" as a child of itself.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เพิŕąŕ¸ˇ "{name}" เป็นรายŕ¸ŕ¸˛ŕ¸Łŕ¸˘ŕąŕ¸­ŕ¸˘ŕą„ด้', - 'Cannot add an item of type "{child}" to an item of type "{parent}".' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เพิŕąŕ¸ˇŕ¸Łŕ¸˛ŕ¸˘ŕ¸ŕ¸˛ŕ¸Ł "{child}" ลงŕąŕ¸™ŕ¸Łŕ¸˛ŕ¸˘ŕ¸ŕ¸˛ŕ¸Ł "{parent}" ได้', - 'Either "{parent}" or "{child}" does not exist.' => '"{parent}" หรือ "{child}" ไมŕąŕ¸ˇŕ¸µ', - 'Error: Table "{table}" does not have a primary key.' => 'ข้อผิดพลาด: ตาราง "{table}" ยังไมŕąŕ¸ˇŕ¸µŕ¸„ีย์หลัŕ¸', - 'Error: Table "{table}" has a composite primary key which is not supported by crud command.' => 'ข้อผิดพลาด: ตาราง "{table}" คีย์หลัŕ¸ŕ¸ˇŕ¸µŕ¸Łŕ¸ąŕ¸›ŕąŕ¸šŕ¸šŕ¸—ีŕąŕą„มŕąŕ¸Şŕ¸™ŕ¸±ŕ¸šŕ¸Şŕ¸™ŕ¸¸ŕ¸™ŕ¸«ŕ¸Łŕ¸·ŕ¸­ŕ¸Łŕ¸­ŕ¸‡ŕ¸Łŕ¸±ŕ¸šŕ¸„ำสัŕąŕ¸‡ crud', - 'Event "{class}.{event}" is attached with an invalid handler "{handler}".' => 'เหตุŕ¸ŕ¸˛ŕ¸Łŕ¸“์ "{class}.{event}" มีŕ¸ŕ¸˛ŕ¸Łŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕą„มŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ "{handler}".', - 'Event "{class}.{event}" is not defined.' => 'เหตุŕ¸ŕ¸˛ŕ¸Łŕ¸“์ "{class}.{event}" ไมŕąŕą„ด้ถูŕ¸ŕ¸ŕ¸łŕ¸«ŕ¸™ŕ¸”', - 'Failed to write the uploaded file "{file}" to disk.' => 'ล้มเหลวŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕ¸­ŕ¸±ŕ¸žŕą‚หลดไฟล์ "{file}" ลงŕąŕ¸™ŕ¸”ิส', - 'File upload was stopped by extension.' => 'ไฟล์ทีŕąŕ¸­ŕ¸±ŕ¸žŕą‚หลดถูŕ¸ŕ¸Łŕ¸°ŕ¸‡ŕ¸±ŕ¸šŕą€ŕ¸™ŕ¸·ŕąŕ¸­ŕ¸‡ŕ¸”้วยรูปŕąŕ¸šŕ¸šŕ¸‚องไฟล์', - 'Filter "{filter}" is invalid. Controller "{class}" does have the filter method "filter{filter}".' => 'ตัวŕ¸ŕ¸Łŕ¸­ŕ¸‡ "{filter}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ สŕąŕ¸§ŕ¸™ŕ¸„วบคุม "{class}" ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕ¸•ŕ¸±ŕ¸§ŕ¸ŕ¸Łŕ¸­ŕ¸‡ "filter{filter}"', - 'Get a new code' => 'สร้างรหัสŕąŕ¸«ŕ¸ˇŕą', - 'Invalid MO file revision: {revision}.' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸ŕą‰ŕą„ขไฟล์ MO ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡: {revision}.', - 'Invalid MO file: {file} (magic: {magic}).' => 'ไฟล์ MO ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡: {file} (magic: {magic}).', - 'Invalid enumerable value "{value}". Please make sure it is among ({enum}).' => 'คŕąŕ¸˛ŕ¸‚อง enumerable "{value}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸šŕ¸§ŕąŕ¸˛ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸„ŕąŕ¸˛ŕąŕ¸™ ({enum}) นี้', - 'List data must be an array or an object implementing Traversable.' => 'รายŕ¸ŕ¸˛ŕ¸Łŕ¸‚้อมูลŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕ¸Łŕ¸ąŕ¸›ŕ¸‚องอาร์เรย์หรือรูปŕąŕ¸šŕ¸š Traversable', - 'List index "{index}" is out of bound.' => 'ไมŕąŕ¸žŕ¸šŕ¸Łŕ¸˛ŕ¸˘ŕ¸ŕ¸˛ŕ¸Ł "{index}" ŕąŕ¸™ŕ¸«ŕ¸™ŕą‰ŕ¸˛ŕąŕ¸Łŕ¸', - 'Login Required' => 'ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕą€ŕ¸‚้าสูŕąŕ¸Łŕ¸°ŕ¸šŕ¸š', - 'Map data must be an array or an object implementing Traversable.' => 'ŕąŕ¸śŕ¸™ŕ¸śŕ¸±ŕ¸‡ŕ¸‚้อมูลŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕ¸Łŕ¸ąŕ¸›ŕ¸‚องอาร์เรย์หรือรูปŕąŕ¸šŕ¸š Traversable', - 'Missing the temporary folder to store the uploaded file "{file}".' => 'ไมŕąŕ¸žŕ¸šŕą„ดเรŕ¸ŕ¸—อรีŕąŕą„ฟล์ชัŕąŕ¸§ŕ¸„ราวŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸ŕą‡ŕ¸šŕą„ฟล์ "{file}" ทีŕąŕ¸­ŕ¸±ŕ¸žŕą‚หลดเข้ามา', - 'No columns are being updated for table "{table}".' => 'ไมŕąŕ¸ˇŕ¸µŕ¸źŕ¸´ŕ¸Ąŕ¸”์ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕ¸›ŕ¸Łŕ¸±ŕ¸šŕ¸›ŕ¸Łŕ¸¸ŕ¸‡ŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡ "{table}"', - 'No counter columns are being updated for table "{table}".' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–นับฟิลด์ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕ¸›ŕ¸Łŕ¸±ŕ¸šŕ¸›ŕ¸Łŕ¸¸ŕ¸‡ŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡ "{table}" ได้', - 'Object configuration must be an array containing a "class" element.' => 'ŕ¸ŕ¸˛ŕ¸Łŕ¸ŕ¸łŕ¸«ŕ¸™ŕ¸”โครงสร้างข้อมูลŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕ¸Łŕ¸ąŕ¸›ŕąŕ¸šŕ¸šŕ¸­ŕ¸˛ŕ¸ŁŕąŚŕą€ŕ¸Łŕ¸˘ŕąŚŕ¸‚อง "class" เทŕąŕ¸˛ŕ¸™ŕ¸±ŕą‰ŕ¸™', - 'Please fix the following input errors:' => 'โปรดŕąŕ¸ŕą‰ŕą„ขข้อผิดพลาดทางด้านลŕąŕ¸˛ŕ¸‡:', - 'Property "{class}.{property}" is not defined.' => 'คุณสมบัติ "{class}.{property}" ยังไมŕąŕ¸–ูŕ¸ŕ¸ŕ¸łŕ¸«ŕ¸™ŕ¸”', - 'Property "{class}.{property}" is read only.' => 'คุณสมบัติ "{class}.{property}" สามารถอŕąŕ¸˛ŕ¸™ŕą„ด้เทŕąŕ¸˛ŕ¸™ŕ¸±ŕą‰ŕ¸™', - 'Queue data must be an array or an object implementing Traversable.' => 'ข้อมูลŕ¸ŕ¸˛ŕ¸ŕ¸ŕ¸˛ŕ¸Łŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕ¸ŕ¸°ŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕ¸Łŕ¸ąŕ¸›ŕ¸‚องอาร์เรย์หรือรูปŕąŕ¸šŕ¸š Traversable', - 'Relation "{name}" is not defined in active record class "{class}".' => 'ความสัมพันŕ¸ŕąŚŕ¸‚อง "{name}" ไมŕąŕą„ด้ถูŕ¸ŕ¸ŕ¸łŕ¸«ŕ¸™ŕ¸”เอาไว้ŕąŕ¸™ "{class}"', - 'Stack data must be an array or an object implementing Traversable.' => 'ข้อมูลสŕąŕ¸•ŕ¸ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕ¸Łŕ¸ąŕ¸›ŕ¸‚องอาร์เรย์หรือรูปŕąŕ¸šŕ¸š Traversable', - 'Table "{table}" does not have a column named "{column}".' => 'ตาราง "{table}" ไมŕąŕ¸ˇŕ¸µŕ¸źŕ¸´ŕ¸Ąŕ¸”์ชืŕąŕ¸­ "{column}" อยูŕąŕąŕ¸™ŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡', - 'Table "{table}" does not have a primary key defined.' => 'ตาราง "{table}" ยังไมŕąŕą„ด้ŕ¸ŕ¸łŕ¸«ŕ¸™ŕ¸”คีย์หลัŕ¸', - 'The "filter" property must be specified with a valid callback.' => 'คุณสมบัติ "filter" ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸–ูŕ¸ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕąŕ¸«ŕą‰ŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ŕ¸•ŕ¸Łŕ¸§ŕ¸ŕ¸Şŕ¸­ŕ¸š', - 'The "pattern" property must be specified with a valid regular expression.' => 'คุณสมบัติ "pattern" ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕąŕ¸«ŕą‰ŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸•ŕ¸˛ŕ¸ˇŕ¸Łŕ¸ąŕ¸›ŕąŕ¸šŕ¸šŕ¸‚อง Regular Expression', - 'The "view" property is required.' => 'ต้องŕ¸ŕ¸˛ŕ¸Łŕ¸„ุณสมบัติ "view" ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™', - 'The CSRF token could not be verified.' => 'CSRF ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ตรวŕ¸ŕ¸Şŕ¸­ŕ¸šŕą„ด้', - 'The URL pattern "{pattern}" for route "{route}" is not a valid regular expression.' => 'รูปŕąŕ¸šŕ¸šŕ¸—ีŕąŕ¸­ŕ¸˘ŕ¸ąŕą "{pattern}" สำหรับ "{route}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸•ŕ¸˛ŕ¸ˇŕ¸Łŕ¸ąŕ¸›ŕąŕ¸šŕ¸šŕ¸‚อง Regular Expression', - 'The active record cannot be deleted because it is new.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ลบข้อมูลทีŕąŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ŕ¸˛ŕ¸Łŕą„ด้ เนืŕąŕ¸­ŕ¸‡ŕ¸ŕ¸˛ŕ¸ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸‚้อมูลŕąŕ¸«ŕ¸ˇŕą', - 'The active record cannot be inserted to database because it is not new.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เพิŕąŕ¸ˇŕ¸‚้อมูลทีŕąŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ŕ¸˛ŕ¸Łŕą„ด้ เนืŕąŕ¸­ŕ¸‡ŕ¸ŕ¸˛ŕ¸ŕą„มŕąŕąŕ¸Šŕąŕ¸‚้อมูลŕąŕ¸«ŕ¸ˇŕą', - 'The active record cannot be updated because it is new.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ปรับปรุงข้อมูลทีŕąŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ŕ¸˛ŕ¸Łŕą„ด้ เนืŕąŕ¸­ŕ¸‡ŕ¸ŕ¸˛ŕ¸ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸‚้อมูลŕąŕ¸«ŕ¸ˇŕą', - 'The asset "{asset}" to be pulished does not exist.' => 'ไมŕąŕ¸žŕ¸šŕ¸‚้อมูล "{asset}" ทีŕąŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ŕ¸˛ŕ¸Ł', - 'The column "{column}" is not a foreign key in table "{table}".' => 'ฟิลด์ "{column}" ไมŕąŕąŕ¸Šŕąŕ¸„ีย์เชืŕąŕ¸­ŕ¸ˇŕ¸•ŕąŕ¸­ŕąŕ¸™ŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡ "{table}"', - 'The command path "{path}" is not a valid directory.' => 'ทีŕąŕ¸­ŕ¸˘ŕ¸ąŕąŕ¸„ำสัŕąŕ¸‡ "{path}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', - 'The controller path "{path}" is not a valid directory.' => 'ทีŕąŕ¸­ŕ¸˘ŕ¸ąŕąŕ¸Şŕąŕ¸§ŕ¸™ŕ¸„วบคุม "{path}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', - 'The file "{file}" cannot be uploaded. Only files with these extensions are allowed: {extensions}.' => 'ไฟล์ "{file}" ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–อัพโหลดได้ ไฟล์ทีŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–อัพโหลดได้ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕą€ŕ¸›ŕą‡ŕ¸™: {extensions}.', - 'The file "{file}" is too large. Its size cannot exceed {limit} bytes.' => 'ไฟล์ "{file}" มีขนาดŕąŕ¸«ŕ¸Ťŕąŕą„ป ไฟล์ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ˇŕ¸µŕ¸‚นาดไมŕąŕą€ŕ¸ŕ¸´ŕ¸™ {limit} ไบต์', - 'The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.' => 'ไฟล์ "{file}" มีขนาดเล็ŕ¸ŕą€ŕ¸ŕ¸´ŕ¸™ŕą„ป ไฟล์ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ˇŕ¸µŕ¸‚นาดมาŕ¸ŕ¸ŕ¸§ŕąŕ¸˛ {limit} ไบต์', - 'The file "{file}" was only partially uploaded.' => 'ไฟล์ "{file}" ถูŕ¸ŕ¸­ŕ¸±ŕ¸žŕą‚หลดไมŕąŕ¸Şŕ¸ˇŕ¸šŕ¸ąŕ¸Łŕ¸“์', - 'The first element in a filter configuration must be the filter class.' => 'ŕ¸ŕ¸˛ŕ¸Łŕ¸ŕ¸łŕ¸«ŕ¸™ŕ¸”คŕąŕ¸˛ŕ¸•ŕ¸±ŕ¸§ŕ¸ŕ¸Łŕ¸­ŕ¸‡ ตัวŕąŕ¸Łŕ¸ŕ¸ŕ¸°ŕąŕ¸—นคŕąŕ¸˛ŕ¸›ŕ¸Łŕ¸°ŕą€ŕ¸ ŕ¸—ของŕ¸ŕ¸˛ŕ¸Łŕ¸ŕ¸Łŕ¸­ŕ¸‡', - 'The item "{name}" does not exist.' => 'ไมŕąŕ¸žŕ¸šŕ¸Łŕ¸˛ŕ¸˘ŕ¸ŕ¸˛ŕ¸Ł "{name}"', - 'The item "{parent}" already has a child "{child}".' => 'รายŕ¸ŕ¸˛ŕ¸Ł "{parent}" มีรายŕ¸ŕ¸˛ŕ¸Łŕ¸˘ŕąŕ¸­ŕ¸˘ "{child}"', - 'The layout path "{path}" is not a valid directory.' => 'รูปŕąŕ¸šŕ¸šŕ¸—ีŕąŕ¸­ŕ¸˘ŕ¸ąŕą "{path}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', - 'The list is read only.' => 'รายŕ¸ŕ¸˛ŕ¸Łŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–อŕąŕ¸˛ŕ¸™ŕą„ด้เทŕąŕ¸˛ŕ¸™ŕ¸±ŕą‰ŕ¸™', - 'The map is read only.' => 'ŕąŕ¸śŕ¸™ŕ¸—ีŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–อŕąŕ¸˛ŕ¸™ŕą„ด้อยŕąŕ¸˛ŕ¸‡ŕą€ŕ¸”ียว', - 'The pattern for 12 hour format must be "h" or "hh".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งรูปŕąŕ¸šŕ¸š 12 ชัŕąŕ¸§ŕą‚มง ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "h" หรือ "hh"', - 'The pattern for 24 hour format must be "H" or "HH".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งรูปŕąŕ¸šŕ¸š 24 ชัŕąŕ¸§ŕą‚มง ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "H" หรือ "HH"', - 'The pattern for AM/PM marker must be "a".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งŕąŕ¸šŕ¸š AM/PM ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "a"', - 'The pattern for day in month must be "F".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งชืŕąŕ¸­ŕ¸‚องเดือนŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "F"', - 'The pattern for day in year must be "D", "DD" or "DDD".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งวันของปีŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "D", "DD" หรือ "DDD"', - 'The pattern for day of the month must be "d" or "dd".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งวันของเดือนŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "d" หรือ "dd"', - 'The pattern for day of the week must be "E", "EE", "EEE", "EEEE" or "EEEEE".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งวันของสัปดาห์ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "E", "EE", "EEE", "EEEE" หรือ "EEEEE"', - 'The pattern for era must be "G", "GG", "GGG", "GGGG" or "GGGGG".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งยุคŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "G", "GG", "GGG", "GGGG" หรือ "GGGGG"', - 'The pattern for hour in AM/PM must be "K" or "KK".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งชัŕąŕ¸§ŕą‚มง (ŕąŕ¸šŕ¸š AM/PM) ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "K" หรือ "KK"', - 'The pattern for hour in day must be "k" or "kk".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งชัŕąŕ¸§ŕą‚มงของวันŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "k" หรือ "kk"', - 'The pattern for minutes must be "m" or "mm".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งนาทีŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "m" หรือ "mm"', - 'The pattern for month must be "M", "MM", "MMM", or "MMMM".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งเดือนŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "M", "MM", "MMM", หรือ "MMMM"', - 'The pattern for seconds must be "s" or "ss".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งวินาทีŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "s" หรือ "ss"', - 'The pattern for time zone must be "z" or "v".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งโซนเวลาŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "z" หรือ "v"', - 'The pattern for week in month must be "W".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งสัปดาห์ของเดือนŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "W"', - 'The pattern for week in year must be "w".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งสัปดาห์ของปีŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "w"', - 'The queue is empty.' => 'สถานะŕ¸ŕ¸˛ŕ¸Łŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕ¸§ŕąŕ¸˛ŕ¸‡', - 'The relation "{relation}" in active record class "{class}" is not specified correctly: the join table "{joinTable}" given in the foreign key cannot be found in the database.' => 'ความสัมพันŕ¸ŕąŚŕ¸Łŕ¸°ŕ¸«ŕ¸§ŕąŕ¸˛ŕ¸‡ "{relation}" ŕ¸ŕ¸±ŕ¸š "{class}" ถูŕ¸ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą„ว้ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡: ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Šŕ¸·ŕąŕ¸­ŕ¸ˇŕ¸•ŕąŕ¸­ŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡ "{joinTable}" ไมŕąŕ¸žŕ¸šŕ¸„ีย์เชืŕąŕ¸­ŕ¸ˇŕ¸„วามสัมพันŕ¸ŕąŚŕąŕ¸™ŕ¸ŕ¸˛ŕ¸™ŕ¸‚้อมูล', - 'The relation "{relation}" in active record class "{class}" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.' => 'ความสัมพันŕ¸ŕąŚŕ¸Łŕ¸°ŕ¸«ŕ¸§ŕąŕ¸˛ŕ¸‡ "{relation}" ŕ¸ŕ¸±ŕ¸š "{class}" ถูŕ¸ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕ¸„ีย์เชืŕąŕ¸­ŕ¸ˇŕ¸•ŕąŕ¸­ŕą„มŕąŕ¸Şŕ¸ˇŕ¸šŕ¸ąŕ¸Łŕ¸“์ คีย์เชืŕąŕ¸­ŕ¸ˇŕ¸•ŕąŕ¸­ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ˇŕ¸µŕ¸„ŕąŕ¸˛ŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡ŕ¸—ีŕąŕą€ŕ¸Šŕ¸·ŕąŕ¸­ŕ¸ˇŕ¸•ŕąŕ¸­ŕ¸ŕ¸±ŕ¸™ŕ¸—ั้งสองตาราง', - 'The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key "{key}". The foreign key does not point to either joining table.' => 'ความสัมพันŕ¸ŕąŚŕ¸Łŕ¸°ŕ¸«ŕ¸§ŕąŕ¸˛ŕ¸‡ "{relation}" ŕ¸ŕ¸±ŕ¸š "{class}" ถูŕ¸ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕ¸”้วยคีย์เชืŕąŕ¸­ŕ¸ˇŕ¸•ŕąŕ¸­ŕ¸—ีŕąŕą„มŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸„ือ "{key}" คีย์เชืŕąŕ¸­ŕ¸ˇŕ¸•ŕąŕ¸­ŕ¸™ŕ¸µŕą‰ŕą„มŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ระบุความสัมพันŕ¸ŕąŚŕ¸Łŕ¸°ŕ¸«ŕ¸§ŕąŕ¸˛ŕ¸‡ŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡ŕą„ด้', - 'The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key. The format of the foreign key must be "joinTable(fk1,fk2,...)".' => 'ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕ¸„ีย์ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Šŕ¸·ŕąŕ¸­ŕ¸ˇŕ¸•ŕąŕ¸­ŕ¸„วามสัมพันŕ¸ŕąŚŕ¸Łŕ¸°ŕ¸«ŕ¸§ŕąŕ¸˛ŕ¸‡ "{class}" ŕ¸ŕ¸±ŕ¸š "{relation}" รูปŕąŕ¸šŕ¸šŕ¸‚องคีย์เชืŕąŕ¸­ŕ¸ˇŕ¸•ŕąŕ¸­ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕ¸Łŕ¸ąŕ¸›ŕąŕ¸šŕ¸š "joinTable(fk1,fk2,...)"', - 'The requested controller "{controller}" does not exist.' => 'ไมŕąŕ¸žŕ¸šŕ¸Şŕąŕ¸§ŕ¸™ŕ¸„วบคุม "{controller}" ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ŕąŕ¸Šŕą‰', - 'The requested view "{name}" was not found.' => 'ไมŕąŕ¸žŕ¸š "{name}" ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ŕąŕ¸Šŕą‰', - 'The stack is empty.' => 'สŕąŕ¸•ŕ¸ŕ¸‚้อมูลเป็นคŕąŕ¸˛ŕ¸§ŕąŕ¸˛ŕ¸‡', - 'The system is unable to find the requested action "{action}".' => 'ระบบไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ดำเนินŕ¸ŕ¸˛ŕ¸Ł "{action}" ตามคำสัŕąŕ¸‡ŕą„ด้', - 'The system view path "{path}" is not a valid directory.' => 'ระบบตรวŕ¸ŕ¸žŕ¸šŕ¸—ีŕąŕ¸­ŕ¸˘ŕ¸ąŕą "{path}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', - 'The table "{table}" for active record class "{class}" cannot be found in the database.' => 'ตาราง "{table}" สำหรับบันทึŕ¸ŕ¸‚้อมูลของ "{class}" ไมŕąŕ¸žŕ¸šŕąŕ¸™ŕ¸ŕ¸˛ŕ¸™ŕ¸‚้อมูล', - 'The value for the primary key "{key}" is not supplied when querying the table "{table}".' => 'คŕąŕ¸˛ŕ¸‚องคีย์หลัภ"{key}" ไมŕąŕ¸Łŕ¸­ŕ¸‡ŕ¸Łŕ¸±ŕ¸šŕą€ŕ¸ˇŕ¸·ŕąŕ¸­ŕ¸ˇŕ¸µŕ¸ŕ¸˛ŕ¸Łŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡ "{table}"', - 'The verification code is incorrect.' => 'รหัสป้องŕ¸ŕ¸±ŕ¸™ŕą„มŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', - 'The view path "{path}" is not a valid directory.' => 'ทีŕąŕ¸­ŕ¸˘ŕ¸ąŕą "{path}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', - 'Theme directory "{directory}" does not exist.' => 'ไมŕąŕ¸žŕ¸šŕą„ดเรŕ¸ŕ¸—อรีŕąŕ¸Łŕ¸ąŕ¸›ŕąŕ¸šŕ¸š "{directory}" ', - 'This content requires the Adobe Flash Player.' => 'ต้องŕ¸ŕ¸˛ŕ¸Ł Adobe Flash Player ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งข้อมูล', - 'Unable to add an item whose name is the same as an existing item.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เพิŕąŕ¸ˇŕ¸Łŕ¸˛ŕ¸˘ŕ¸ŕ¸˛ŕ¸Łŕ¸—ีŕąŕ¸ˇŕ¸µŕ¸Šŕ¸·ŕąŕ¸­ŕą€ŕ¸”ียวŕ¸ŕ¸±ŕ¸™ŕą„ด้', - 'Unable to change the item name. The name "{name}" is already used by another item.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เปลีŕąŕ¸˘ŕ¸™ŕ¸Šŕ¸·ŕąŕ¸­ŕ¸Łŕ¸˛ŕ¸˘ŕ¸ŕ¸˛ŕ¸Łŕą„ด้ "{name}" ŕ¸ŕ¸łŕ¸Ąŕ¸±ŕ¸‡ŕ¸–ูŕ¸ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕ¸ŕ¸˛ŕ¸ŕ¸Łŕ¸˛ŕ¸˘ŕ¸ŕ¸˛ŕ¸Łŕ¸­ŕ¸·ŕąŕ¸™', - 'Unable to create application state file "{file}". Make sure the directory containing the file exists and is writable by the Web server process.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–สร้างไฟล์ระบบ "{file}" ได้ ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸šŕ¸§ŕąŕ¸˛ŕą„ดเรŕ¸ŕ¸—อรีŕąŕą€ŕ¸ŕą‡ŕ¸šŕą„ฟล์ สามารถเขียนได้โดยคำสัŕąŕ¸‡ŕ¸‚องเซิฟเวอร์', - 'Unable to find the decorator view "{view}".' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–หาสŕąŕ¸§ŕ¸™ŕ¸„วบคุมของ "{view}" ได้', - 'Unable to find the list item.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–หารายŕ¸ŕ¸˛ŕ¸Łŕą„ด้', - 'Unable to lock file "{file}" for reading.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ล็อคไฟล์ "{file}" สำหรับอŕąŕ¸˛ŕ¸™ŕą„ด้', - 'Unable to lock file "{file}" for writing.' => ' ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ล็อคไฟล์ "{file}" สำหรับเขียนได้', - 'Unable to read file "{file}".' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–อŕąŕ¸˛ŕ¸™ŕą„ฟล์ "{file}" ได้', - 'Unable to replay the action "{object}.{method}". The method does not exist.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ดำเนินŕ¸ŕ¸˛ŕ¸Ł "{object}.{method}" ซ้ำได้ ŕ¸ŕ¸˛ŕ¸Łŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕ¸–ูŕ¸ŕ¸Łŕ¸°ŕ¸‡ŕ¸±ŕ¸š', - 'Unable to write file "{file}".' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เขียนทับไฟล์ "{file}" ได้', - 'Unknown authorization item "{name}".' => 'ไมŕąŕ¸žŕ¸šŕ¸Łŕ¸˛ŕ¸˘ŕ¸ŕ¸˛ŕ¸Ł "{name}"', - 'Unrecognized locale "{locale}".' => 'ไมŕąŕ¸žŕ¸šŕ¸›ŕ¸Ąŕ¸˛ŕ¸˘ŕ¸—าง "{locale}"', - 'View file "{file}" does not exist.' => 'ไมŕąŕ¸žŕ¸šŕą„ฟล์ "{file}" ทีŕąŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ŕ¸˛ŕ¸Łŕ¸”ู', - 'Yii application can only be created once.' => 'โปรŕąŕ¸ŕ¸Łŕ¸ˇ Yii สามารถสร้างได้เพียงครั้งเดียวเทŕąŕ¸˛ŕ¸™ŕ¸±ŕą‰ŕ¸™', - 'You are not authorized to perform this action.' => 'คุณไมŕąŕą„ด้รับอนุญาตŕąŕ¸«ŕą‰ŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕ¸”ังŕ¸ŕ¸Ąŕąŕ¸˛ŕ¸§', - 'Your request is not valid.' => 'ŕ¸ŕ¸˛ŕ¸Łŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕą„มŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', - '{attribute} "{value}" has already been taken.' => '{attribute} "{value}" ได้รับŕ¸ŕ¸˛ŕ¸Łŕ¸šŕ¸±ŕ¸™ŕ¸—ึŕ¸', - '{attribute} cannot be blank.' => '{attribute} ไมŕąŕ¸„วรเป็นคŕąŕ¸˛ŕ¸§ŕąŕ¸˛ŕ¸‡', - '{attribute} is invalid.' => '{attribute} ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', - '{attribute} is not a valid URL.' => '{attribute} URL ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', - '{attribute} is not a valid email address.' => '{attribute} รูปŕąŕ¸šŕ¸šŕ¸­ŕ¸µŕą€ŕ¸ˇŕ¸ĄŕąŚŕą„มŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', - '{attribute} is not in the list.' => '{attribute} ไมŕąŕ¸ˇŕ¸µŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕ¸Łŕ¸˛ŕ¸˘ŕ¸ŕ¸˛ŕ¸Ł', - '{attribute} is of the wrong length (should be {length} characters).' => '{attribute} ขนาดความยาวไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ (ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ˇŕ¸µŕ¸‚นาด {length} ตัวอัŕ¸ŕ¸©ŕ¸Ł)', - '{attribute} is too big (maximum is {max}).' => '{attribute} มีขนาดŕąŕ¸«ŕ¸Ťŕąŕą€ŕ¸ŕ¸´ŕ¸™ŕą„ป (ขนาดสูงสุด {max})', - '{attribute} is too long (maximum is {max} characters).' => '{attribute} ยาวเŕ¸ŕ¸´ŕ¸™ŕą„ป (ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕą„มŕąŕą€ŕ¸ŕ¸´ŕ¸™ {max} ตัวอัŕ¸ŕ¸©ŕ¸Ł)', - '{attribute} is too short (minimum is {min} characters).' => '{attribute} สั้นเŕ¸ŕ¸´ŕ¸™ŕą„ป (ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ˇŕ¸˛ŕ¸ŕ¸ŕ¸§ŕąŕ¸˛ {min} ตัวอัŕ¸ŕ¸©ŕ¸Ł)', - '{attribute} is too small (minimum is {min}).' => '{attribute} มีขนาดเล็ŕ¸ŕą€ŕ¸ŕ¸´ŕ¸™ŕą„ป (ขนาดเล็ŕ¸ŕ¸Şŕ¸¸ŕ¸” {min})', - '{attribute} must be a number.' => '{attribute} ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸•ŕ¸±ŕ¸§ŕą€ŕ¸Ąŕ¸‚เทŕąŕ¸˛ŕ¸™ŕ¸±ŕą‰ŕ¸™', - '{attribute} must be an integer.' => '{attribute} ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸ŕ¸łŕ¸™ŕ¸§ŕ¸™ŕą€ŕ¸•ŕą‡ŕ¸ˇŕą€ŕ¸—ŕąŕ¸˛ŕ¸™ŕ¸±ŕą‰ŕ¸™', - '{attribute} must be repeated exactly.' => '{attribute} ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ˇŕ¸µŕ¸„ŕąŕ¸˛ŕą€ŕ¸«ŕ¸ˇŕ¸·ŕ¸­ŕ¸™ŕ¸ŕ¸±ŕ¸™', - '{attribute} must be {type}.' => '{attribute} ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕą€ŕ¸›ŕą‡ŕ¸™ {type}.', - '{className} does not support add() functionality.' => '{className} ไมŕąŕ¸Łŕ¸­ŕ¸‡ŕ¸Łŕ¸±ŕ¸šŕ¸źŕ¸±ŕ¸‡ŕ¸ŕąŚŕ¸Šŕ¸±ŕąŕ¸™ add()', - '{className} does not support delete() functionality.' => '{className} ไมŕąŕ¸Łŕ¸­ŕ¸‡ŕ¸Łŕ¸±ŕ¸šŕ¸źŕ¸±ŕ¸‡ŕ¸ŕąŚŕ¸Šŕ¸±ŕąŕ¸™ delete()', - '{className} does not support flush() functionality.' => '{className} ไมŕąŕ¸Łŕ¸­ŕ¸‡ŕ¸Łŕ¸±ŕ¸šŕ¸źŕ¸±ŕ¸‡ŕ¸ŕąŚŕ¸Šŕ¸±ŕąŕ¸™ flush()', - '{className} does not support get() functionality.' => '{className} ไมŕąŕ¸Łŕ¸­ŕ¸‡ŕ¸Łŕ¸±ŕ¸šŕ¸źŕ¸±ŕ¸‡ŕ¸ŕąŚŕ¸Šŕ¸±ŕąŕ¸™ get()', - '{className} does not support set() functionality.' => '{className} ไมŕąŕ¸Łŕ¸­ŕ¸‡ŕ¸Łŕ¸±ŕ¸šŕ¸źŕ¸±ŕ¸‡ŕ¸ŕąŚŕ¸Šŕ¸±ŕąŕ¸™ set()', - '{class} does not have attribute "{name}".' => '{class} ไมŕąŕ¸ˇŕ¸µŕ¸‚้อมูลของ "{name}"', - '{class} does not have relation "{name}".' => '{class} ไมŕąŕ¸ˇŕ¸µŕ¸„วามสัมพันŕ¸ŕąŚŕ¸ŕ¸±ŕ¸™ŕ¸ŕ¸±ŕ¸š "{name}"', - '{class} does not support fetching all table names.' => '{class} ไมŕąŕ¸Łŕ¸­ŕ¸‡ŕ¸Łŕ¸±ŕ¸šŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ŕ¸‚้อมูลŕ¸ŕ¸˛ŕ¸ŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡ŕ¸—ั้งหมด', - '{class} has an invalid validation rule. The rule must specify attributes to be validated and the validator name.' => '{class} ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸•ŕ¸˛ŕ¸ˇŕą€ŕ¸‡ŕ¸·ŕąŕ¸­ŕ¸™ŕą„ข. ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸—ำŕ¸ŕ¸˛ŕ¸Łŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕ¸„ŕąŕ¸˛ŕ¸‚องข้อมูลŕąŕ¸Ąŕ¸°ŕ¸Šŕ¸·ŕąŕ¸­ŕ¸‚องข้อมูล', - '{class} must specify "model" and "attribute" or "name" property values.' => '{class} ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸—ำŕ¸ŕ¸˛ŕ¸Łŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕ¸„ŕąŕ¸˛ "model" ŕąŕ¸Ąŕ¸° "attribute" หรือ "name" เหลŕąŕ¸˛ŕ¸™ŕ¸µŕą‰ŕ¸”้วย', - '{class}.allowAutoLogin must be set true in order to use cookie-based authentication.' => '{class}.allowAutoLogin ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕą€ŕ¸›ŕ¸´ŕ¸”ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕą€ŕ¸ˇŕ¸·ŕąŕ¸­ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕ¸„ุ๊ŕ¸ŕ¸ŕ¸µŕą‰', - '{class}::authenticate() must be implemented.' => '{class}::authenticate() ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸–ูŕ¸ŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Ł', - '{controller} cannot find the requested view "{view}".' => '{controller} ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ค้นหา "{view}" ตามคำสัŕąŕ¸‡ŕą„ด้', - '{controller} contains improperly nested widget tags in its view "{view}". A {widget} widget does not have an endWidget() call.' => '{controller} มีชุดคำสัŕąŕ¸‡ŕ¸—ีŕąŕą„มŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸«ŕ¸Ąŕ¸˛ŕ¸˘ŕ¸­ŕ¸˘ŕąŕ¸˛ŕ¸‡ŕąŕ¸™ŕ¸ˇŕ¸¸ŕ¸ˇŕ¸ˇŕ¸­ŕ¸‡ŕ¸‚อง "{view}" ทำŕąŕ¸«ŕą‰ {widget} ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เรียŕ¸ŕąŕ¸Šŕą‰ŕ¸„ำสัŕąŕ¸‡ endWidget() ได้', - '{controller} has an extra endWidget({id}) call in its view.' => '{controller} ŕ¸ŕ¸łŕ¸Ąŕ¸±ŕ¸‡ŕ¸–ูŕ¸ŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ŕąŕ¸Šŕą‰ŕą‚ดย endWidget({id}) ŕąŕ¸™ŕ¸‚ณะนี้', - '{widget} cannot find the view "{view}".' => '{widget} ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ดู "{view}" ได้', -); + '< หน้าทีŕąŕąŕ¸Ąŕą‰ŕ¸§', + '<< First' => '<< หน้าŕąŕ¸Łŕ¸', + 'Go to page: ' => 'ไปทีŕąŕ¸«ŕ¸™ŕą‰ŕ¸˛: ', + 'Last >>' => 'หน้าสุดท้าย >>', + 'Next >' => 'หน้าถัดไป >', + 'The asset "{asset}" to be published does not exist.' => 'ไมŕąŕ¸žŕ¸šŕ¸‚้อมูล "{asset}" ทีŕąŕ¸–ูŕ¸ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™', + '"{path}" is not a valid directory.' => 'ไมŕąŕ¸žŕ¸šŕą„ดเรŕ¸ŕ¸—อรี๠"{path}"', + 'Active Record requires a "db" CDbConnection application component.' => 'ต้องŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕ¸‚้อมูล "db" ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Ł CDbConnection ตามโครงสร้างของระบบ', + 'Active record "{class}" has an invalid configuration for relation "{relation}". It must specify the relation type, the related active record class and the foreign key.' => 'ข้อมูลทีŕąŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ "{class}" ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ŕ¸ŕ¸łŕ¸«ŕ¸™ŕ¸”ความสัมพันŕ¸ŕąŚ "{relation}" ได้ โดยŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸—ำŕ¸ŕ¸˛ŕ¸Łŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕ¸„วามสัมพันŕ¸ŕąŚ ซึŕąŕ¸‡ŕ¸‚้อมูลทีŕąŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸„ีย์เชืŕąŕ¸­ŕ¸ˇŕ¸„วามสัมพันŕ¸ŕąŚŕąŕ¸™ŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡ŕ¸‚้อมูล', + 'Active record "{class}" is trying to select an invalid column "{column}". Note, the column must exist in the table or be an expression with alias.' => 'ข้อมูลทีŕąŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ "{class}" ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เลือŕ¸ŕ¸źŕ¸´ŕ¸Ąŕ¸”์ "{column}" ได้ คำŕąŕ¸™ŕ¸°ŕ¸™ŕ¸ł, ชืŕąŕ¸­ŕ¸źŕ¸´ŕ¸Ąŕ¸”์ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ˇŕ¸µŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡ŕ¸«ŕ¸Łŕ¸·ŕ¸­ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸Šŕ¸·ŕąŕ¸­ŕą€ŕ¸Şŕ¸ˇŕ¸·ŕ¸­ŕ¸™ŕ¸‚องฟิลด์อืŕąŕ¸™ŕą†', + 'Alias "{alias}" is invalid. Make sure it points to an existing directory or file.' => 'ชืŕąŕ¸­ŕą€ŕ¸Şŕ¸ˇŕ¸·ŕ¸­ŕ¸™ "{alias}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸šŕ¸„วามถูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸‚องไดเรŕ¸ŕ¸—อรีŕąŕ¸«ŕ¸Łŕ¸·ŕ¸­ŕą„ฟล์', + 'Application base path "{path}" is not a valid directory.' => 'ทีŕąŕ¸­ŕ¸˘ŕ¸ąŕąŕą‚ปรŕąŕ¸ŕ¸Łŕ¸ˇŕ¸«ŕ¸Ąŕ¸±ŕ¸ "{path}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', + 'Application runtime path "{path}" is not valid. Please make sure it is a directory writable by the Web server process.' => 'ทีŕąŕ¸­ŕ¸˘ŕ¸ąŕąŕą‚ปรŕąŕ¸ŕ¸Łŕ¸ˇ "{path}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸šŕ¸§ŕąŕ¸˛ŕą„ดเรŕ¸ŕ¸—อรีŕąŕ¸™ŕ¸±ŕą‰ŕ¸™ŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕąŕ¸Ąŕ¸°ŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เขียนได้โดยคำสัŕąŕ¸‡ŕ¸‚องเซิฟเวอร์', + 'Authorization item "{item}" has already been assigned to user "{user}".' => 'รายŕ¸ŕ¸˛ŕ¸Ł "{item}" ได้ถูŕ¸ŕ¸ŕ¸łŕ¸«ŕ¸™ŕ¸”ŕąŕ¸«ŕą‰ŕ¸ŕ¸±ŕ¸šŕ¸śŕ¸ąŕą‰ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ "{user}" เรียบร้อยŕąŕ¸Ąŕą‰ŕ¸§', + 'CApcCache requires PHP apc extension to be loaded.' => 'CApcCache ต้องŕ¸ŕ¸˛ŕ¸Ł PHP apc ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™', + 'CAssetManager.basePath "{path}" is invalid. Please make sure the directory exists and is writable by the Web server process.' => 'CAssetManager.basePath "{path}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸šŕ¸§ŕąŕ¸˛ŕą„ดเรŕ¸ŕ¸—อรีŕąŕ¸™ŕ¸±ŕą‰ŕ¸™ŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕąŕ¸Ąŕ¸°ŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เขียนได้โดยคำสัŕąŕ¸‡ŕ¸‚องเซิฟเวอร์', + 'CCacheHttpSession.cacheID is invalid. Please make sure "{id}" refers to a valid cache application component.' => 'CCacheHttpSession.cacheID ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸š "{id}" วŕąŕ¸˛ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸„ŕąŕ¸˛ŕ¸•ŕ¸˛ŕ¸ˇŕą‚ครงสร้างของหนŕąŕ¸§ŕ¸˘ŕ¸„วามŕ¸ŕ¸łŕ¸Šŕ¸±ŕąŕ¸§ŕ¸„ราว (Cache) หรือไมŕą', + 'CCaptchaValidator.action "{id}" is invalid. Unable to find such an action in the current controller.' => 'CCaptchaValidator.action "{id}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ค้นหาŕ¸ŕ¸˛ŕ¸Łŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕąŕ¸™ŕ¸Şŕąŕ¸§ŕ¸™ŕ¸„วบคุมปัŕ¸ŕ¸ŕ¸¸ŕ¸šŕ¸±ŕ¸™ŕą„ด้', + 'CDbAuthManager.connectionID "{id}" is invalid. Please make sure it refers to the ID of a CDbConnection application component.' => 'CDbAuthManager.connectionID "{id}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸š ID วŕąŕ¸˛ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸„ŕąŕ¸˛ŕ¸ŕ¸˛ŕ¸ CDbConnection ตามโครงสร้างของระบบหรือไมŕą', + 'CDbCache.connectionID "{id}" is invalid. Please make sure it refers to the ID of a CDbConnection application component.' => 'CDbCache.connectionID "{id}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸š ID วŕąŕ¸˛ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸„ŕąŕ¸˛ŕ¸ŕ¸˛ŕ¸ CDbConnection ตามโครงสร้างของระบบหรือไมŕą', + 'CDbCacheDependency.sql cannot be empty.' => 'CDbCacheDependency.sql ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เป็นคŕąŕ¸˛ŕ¸§ŕąŕ¸˛ŕ¸‡ŕą„ด้', + 'CDbCommand failed to execute the SQL statement: {error}' => 'CDbCommand ล้มเหลวŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ŕ¸Ąŕ¸±ŕ¸ŕ¸©ŕ¸“ะของ SQL: {error}', + 'CDbCommand failed to prepare the SQL statement: {error}' => 'CDbCommand ล้มเหลวŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸ŕ¸ŕąŕ¸ŕ¸‡ŕ¸Ąŕ¸±ŕ¸ŕ¸©ŕ¸“ะของ SQL: {error}', + 'CDbConnection does not support reading schema for {driver} database.' => 'CDbConnection ไมŕąŕ¸Łŕ¸­ŕ¸‡ŕ¸Łŕ¸±ŕ¸šŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕ¸­ŕąŕ¸˛ŕ¸™ŕ¸Łŕ¸˛ŕ¸˘ŕ¸Ąŕ¸°ŕą€ŕ¸­ŕ¸µŕ¸˘ŕ¸”โครงสร้างสำหรับ {driver} ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸™ŕ¸‚้อมูลได้', + 'CDbConnection failed to open the DB connection: {error}' => 'CDbConnection ล้มเหลวŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Šŕ¸·ŕąŕ¸­ŕ¸ˇŕ¸•ŕąŕ¸­ŕ¸ŕ¸˛ŕ¸™ŕ¸‚้อมูล: {error}', + 'CDbConnection is inactive and cannot perform any DB operations.' => 'CDbConnection ไมŕąŕą„ด้ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕąŕ¸Ąŕ¸°ŕą„มŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ดำเนินŕ¸ŕ¸˛ŕ¸Łŕ¸ŕ¸±ŕ¸šŕ¸ŕ¸˛ŕ¸™ŕ¸‚้อมูลŕąŕ¸”ๆได้', + 'CDbConnection.connectionString cannot be empty.' => 'CDbConnection.connectionString ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เป็นคŕąŕ¸˛ŕ¸§ŕąŕ¸˛ŕ¸‡ŕą„ด้', + 'CDbDataReader cannot rewind. It is a forward-only reader.' => 'CDbDataReader ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ย้อนŕ¸ŕ¸Ąŕ¸±ŕ¸šŕą„ด้, ถูŕ¸ŕ¸ŕ¸łŕ¸«ŕ¸™ŕ¸”ŕąŕ¸«ŕą‰ŕ¸­ŕąŕ¸˛ŕ¸™ŕą„ด้เพียงอยŕąŕ¸˛ŕ¸‡ŕą€ŕ¸”ียว', + 'CDbHttpSession.connectionID "{id}" is invalid. Please make sure it refers to the ID of a CDbConnection application component.' => 'CDbHttpSession.connectionID "{id}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸š ID วŕąŕ¸˛ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸„ŕąŕ¸˛ŕ¸ŕ¸˛ŕ¸ CDbConnection ตามโครงสร้างของระบบหรือไมŕą', + 'CDbLogRoute requires database table "{table}" to store log messages.' => 'CDbLogRoute ต้องŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡ "{table}" เพืŕąŕ¸­ŕą€ŕ¸ŕą‡ŕ¸šŕ¸šŕ¸±ŕ¸™ŕ¸—ึŕ¸ŕ¸‚้อความ', + 'CDbLogRoute.connectionID "{id}" does not point to a valid CDbConnection application component.' => 'CDbLogRoute.connectionID "{id}" ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ระบุคŕąŕ¸˛ŕ¸—ีŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸‚อง CDbConnection ตามโครงสร้างของระบบได้', + 'CDbMessageSource.connectionID is invalid. Please make sure "{id}" refers to a valid database application component.' => 'CDbMessageSource.connectionID ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸šŕ¸§ŕąŕ¸˛ "{id}" เป็นข้อมูลทีŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸•ŕ¸˛ŕ¸ˇŕą‚ครงสร้างของŕ¸ŕ¸˛ŕ¸™ŕ¸‚้อมูลหรือไมŕą', + 'CDbTransaction is inactive and cannot perform commit or roll back operations.' => 'CDbTransaction ไมŕąŕą„ด้ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕąŕ¸Ąŕ¸°ŕą„มŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ดำเนินŕ¸ŕ¸˛ŕ¸Łŕ¸ŕ¸Łŕ¸°ŕ¸—ำตŕąŕ¸­ŕ¸«ŕ¸Łŕ¸·ŕ¸­ŕ¸˘ŕą‰ŕ¸­ŕ¸™ŕ¸ŕ¸Ąŕ¸±ŕ¸šŕ¸ŕ¸˛ŕ¸Łŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕą„ด้', + 'CDirectoryCacheDependency.directory cannot be empty.' => 'CDirectoryCacheDependency.directory ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เป็นคŕąŕ¸˛ŕ¸§ŕąŕ¸˛ŕ¸‡ŕą„ด้', + 'CFileCacheDependency.fileName cannot be empty.' => 'CFileCacheDependency.fileName ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เป็นคŕąŕ¸˛ŕ¸§ŕąŕ¸˛ŕ¸‡ŕą„ด้', + 'CFileLogRoute.logPath "{path}" does not point to a valid directory. Make sure the directory exists and is writable by the Web server process.' => 'CFileLogRoute.logPath "{path}" ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ชี้ไดเรŕ¸ŕ¸—อรีŕąŕ¸—ีŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕą„ด้ ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸šŕ¸§ŕąŕ¸˛ŕą„ดเรŕ¸ŕ¸—อรีŕąŕ¸™ŕ¸±ŕą‰ŕ¸™ŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕąŕ¸Ąŕ¸°ŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เขียนได้โดยคำสัŕąŕ¸‡ŕ¸‚องเซิฟเวอร์', + 'CFilterChain can only take objects implementing the IFilter interface.' => 'CFilterChain สามารถเรียŕ¸ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕ¸ŕ¸±ŕ¸šŕ¸‚้อมูลทีŕąŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕ¸Łŕ¸ąŕ¸›ŕąŕ¸šŕ¸šŕ¸‚อง IFilter ได้', + 'CFlexWidget.baseUrl cannot be empty.' => 'CFlexWidget.baseUrl ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เป็นคŕąŕ¸˛ŕ¸§ŕąŕ¸˛ŕ¸‡ŕą„ด้', + 'CFlexWidget.name cannot be empty.' => 'CFlexWidget.name ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เป็นคŕąŕ¸˛ŕ¸§ŕąŕ¸˛ŕ¸‡ŕą„ด้', + 'CGlobalStateCacheDependency.stateName cannot be empty.' => 'CGlobalStateCacheDependency.stateName ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เป็นคŕąŕ¸˛ŕ¸§ŕąŕ¸˛ŕ¸‡ŕą„ด้', + 'CHttpCookieCollection can only hold CHttpCookie objects.' => 'CHttpCookieCollection สามารถŕąŕ¸Šŕą‰ŕ¸ŕ¸±ŕ¸š CHttpCookie ได้เทŕąŕ¸˛ŕ¸™ŕ¸±ŕą‰ŕ¸™', + 'CHttpRequest is unable to determine the entry script URL.' => 'CHttpRequest ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ตรวŕ¸ŕ¸Şŕ¸­ŕ¸šŕ¸„ำสัŕąŕ¸‡ŕ¸—ีŕąŕ¸­ŕ¸˘ŕ¸ąŕąŕ¸‚องสคริปต์ได้', + 'CHttpRequest is unable to determine the path info of the request.' => 'CHttpRequest ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ตรวŕ¸ŕ¸Şŕ¸­ŕ¸šŕ¸‚้อมูลทีŕąŕ¸­ŕ¸˘ŕ¸ąŕąŕ¸•ŕ¸˛ŕ¸ˇŕ¸„ำขอได้', + 'CHttpRequest is unable to determine the request URI.' => 'CHttpRequest ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ตรวŕ¸ŕ¸Şŕ¸­ŕ¸š URI ตามคำขอได้', + 'CHttpSession.cookieMode can only be "none", "allow" or "only".' => 'CHttpSession.cookieMode ระบุคŕąŕ¸˛ŕą€ŕ¸›ŕą‡ŕ¸™ "none", "allow" หรือ "only" เทŕąŕ¸˛ŕ¸™ŕ¸±ŕą‰ŕ¸™', + 'CHttpSession.gcProbability "{value}" is invalid. It must be an integer between 0 and 100.' => 'CHttpSession.gcProbability "{value}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ คŕąŕ¸˛ŕ¸—ีŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸ŕ¸łŕ¸™ŕ¸§ŕ¸™ŕą€ŕ¸•ŕą‡ŕ¸ˇŕ¸Łŕ¸°ŕ¸«ŕ¸§ŕąŕ¸˛ŕ¸‡ 0 ถึง 100 เทŕąŕ¸˛ŕ¸™ŕ¸±ŕą‰ŕ¸™', + 'CHttpSession.savePath "{path}" is not a valid directory.' => 'CHttpSession.savePath "{path}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', + 'CMemCache requires PHP memcache extension to be loaded.' => 'CMemCache ต้องŕ¸ŕ¸˛ŕ¸Ł PHP memcache ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™', + 'CMemCache server configuration must be an array.' => 'CMemCache ของคŕąŕ¸˛ŕą€ŕ¸‹ŕ¸´ŕ¸źŕą€ŕ¸§ŕ¸­ŕ¸ŁŕąŚŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕąŕ¸šŕ¸šŕ¸­ŕ¸˛ŕ¸ŁŕąŚŕą€ŕ¸Łŕ¸˘ŕąŚ', + 'CMemCache server configuration must have "host" value.' => 'CMemCache ของคŕąŕ¸˛ŕą€ŕ¸‹ŕ¸´ŕ¸źŕą€ŕ¸§ŕ¸­ŕ¸ŁŕąŚŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸„ŕąŕ¸˛ŕ¸‚อง "host"', + 'CMultiFileUpload.name is required.' => 'CMultiFileUpload.name ต้องŕ¸ŕ¸˛ŕ¸Łŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™', + 'CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.' => 'CProfileLogRoute พบคำสัŕąŕ¸‡ŕ¸—ีŕąŕ¸«ŕą‰ŕ¸˛ŕ¸ˇŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ "{token}". ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸šŕ¸§ŕąŕ¸˛ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ Yii::beginProfile() ŕąŕ¸Ąŕ¸° Yii::endProfile() ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸–ูŕ¸ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕ¸„ุณสมบัติตŕąŕ¸˛ŕ¸‡ŕą†ŕ¸—ีŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', + 'CProfileLogRoute.report "{report}" is invalid. Valid values include "summary" and "callstack".' => 'CProfileLogRoute.report "{report}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ คŕąŕ¸˛ŕ¸—ีŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ˇŕ¸µŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ "summary" หรือ "callstack".', + 'CSecurityManager requires PHP mcrypt extension to be loaded in order to use data encryption feature.' => 'CSecurityManager ต้องŕ¸ŕ¸˛ŕ¸Ł PHP mcrypt ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕą€ŕ¸žŕ¸·ŕąŕ¸­ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕ¸„ุณสมบัติŕ¸ŕ¸˛ŕ¸Łŕ¸šŕ¸µŕ¸šŕ¸­ŕ¸±ŕ¸”ข้อมูล', + 'CSecurityManager.encryptionKey cannot be empty.' => 'CSecurityManager.encryptionKey ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เป็นคŕąŕ¸˛ŕ¸§ŕąŕ¸˛ŕ¸‡ŕą„ด้', + 'CSecurityManager.validation must be either "MD5" or "SHA1".' => 'CSecurityManager.validation ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕ¸Łŕ¸ąŕ¸›ŕąŕ¸šŕ¸š "MD5" หรือ "SHA1"', + 'CSecurityManager.validationKey cannot be empty.' => 'CSecurityManager.validationKey ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เป็นคŕąŕ¸˛ŕ¸§ŕąŕ¸˛ŕ¸‡ŕą„ด้', + 'CTypedList<{type}> can only hold objects of {type} class.' => 'CTypedList<{type}> สามารถŕąŕ¸Šŕą‰ŕ¸ŕ¸±ŕ¸šŕ¸Łŕ¸ąŕ¸›ŕąŕ¸šŕ¸š {type} ได้เทŕąŕ¸˛ŕ¸™ŕ¸±ŕą‰ŕ¸™', + 'CUrlManager.UrlFormat must be either "path" or "get".' => 'CUrlManager.UrlFormat ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ˇŕ¸µ "path" หรือ "get"', + 'CXCache requires PHP XCache extension to be loaded.' => 'CXCache ต้องŕ¸ŕ¸˛ŕ¸Ł PHP XCache ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™', + 'Cache table "{tableName}" does not exist.' => 'ไมŕąŕ¸žŕ¸š Cache ของตาราง "{tableName}"', + 'Cannot add "{child}" as a child of "{name}". A loop has been detected.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เพิŕąŕ¸ˇ "{child}" เป็นรายŕ¸ŕ¸˛ŕ¸Łŕ¸˘ŕąŕ¸­ŕ¸˘ŕ¸‚อง "{name}" ได้ ตรวŕ¸ŕ¸žŕ¸šŕ¸ŕ¸˛ŕ¸Łŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕ¸‹ŕą‰ŕ¸ł', + 'Cannot add "{child}" as a child of "{parent}". A loop has been detected.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เพิŕąŕ¸ˇ "{child}" เป็นรายŕ¸ŕ¸˛ŕ¸Łŕ¸˘ŕąŕ¸­ŕ¸˘ŕ¸‚อง "{parent}" ได้ ตรวŕ¸ŕ¸žŕ¸šŕ¸ŕ¸˛ŕ¸Łŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕ¸‹ŕą‰ŕ¸ł', + 'Cannot add "{name}" as a child of itself.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เพิŕąŕ¸ˇ "{name}" เป็นรายŕ¸ŕ¸˛ŕ¸Łŕ¸˘ŕąŕ¸­ŕ¸˘ŕą„ด้', + 'Cannot add an item of type "{child}" to an item of type "{parent}".' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เพิŕąŕ¸ˇŕ¸Łŕ¸˛ŕ¸˘ŕ¸ŕ¸˛ŕ¸Ł "{child}" ลงŕąŕ¸™ŕ¸Łŕ¸˛ŕ¸˘ŕ¸ŕ¸˛ŕ¸Ł "{parent}" ได้', + 'Either "{parent}" or "{child}" does not exist.' => '"{parent}" หรือ "{child}" ไมŕąŕ¸ˇŕ¸µ', + 'Error: Table "{table}" does not have a primary key.' => 'ข้อผิดพลาด: ตาราง "{table}" ยังไมŕąŕ¸ˇŕ¸µŕ¸„ีย์หลัŕ¸', + 'Error: Table "{table}" has a composite primary key which is not supported by crud command.' => 'ข้อผิดพลาด: ตาราง "{table}" คีย์หลัŕ¸ŕ¸ˇŕ¸µŕ¸Łŕ¸ąŕ¸›ŕąŕ¸šŕ¸šŕ¸—ีŕąŕą„มŕąŕ¸Şŕ¸™ŕ¸±ŕ¸šŕ¸Şŕ¸™ŕ¸¸ŕ¸™ŕ¸«ŕ¸Łŕ¸·ŕ¸­ŕ¸Łŕ¸­ŕ¸‡ŕ¸Łŕ¸±ŕ¸šŕ¸„ำสัŕąŕ¸‡ crud', + 'Event "{class}.{event}" is attached with an invalid handler "{handler}".' => 'เหตุŕ¸ŕ¸˛ŕ¸Łŕ¸“์ "{class}.{event}" มีŕ¸ŕ¸˛ŕ¸Łŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕą„มŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ "{handler}".', + 'Event "{class}.{event}" is not defined.' => 'เหตุŕ¸ŕ¸˛ŕ¸Łŕ¸“์ "{class}.{event}" ไมŕąŕą„ด้ถูŕ¸ŕ¸ŕ¸łŕ¸«ŕ¸™ŕ¸”', + 'Failed to write the uploaded file "{file}" to disk.' => 'ล้มเหลวŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕ¸­ŕ¸±ŕ¸žŕą‚หลดไฟล์ "{file}" ลงŕąŕ¸™ŕ¸”ิส', + 'File upload was stopped by extension.' => 'ไฟล์ทีŕąŕ¸­ŕ¸±ŕ¸žŕą‚หลดถูŕ¸ŕ¸Łŕ¸°ŕ¸‡ŕ¸±ŕ¸šŕą€ŕ¸™ŕ¸·ŕąŕ¸­ŕ¸‡ŕ¸”้วยรูปŕąŕ¸šŕ¸šŕ¸‚องไฟล์', + 'Filter "{filter}" is invalid. Controller "{class}" does have the filter method "filter{filter}".' => 'ตัวŕ¸ŕ¸Łŕ¸­ŕ¸‡ "{filter}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ สŕąŕ¸§ŕ¸™ŕ¸„วบคุม "{class}" ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕ¸•ŕ¸±ŕ¸§ŕ¸ŕ¸Łŕ¸­ŕ¸‡ "filter{filter}"', + 'Get a new code' => 'สร้างรหัสŕąŕ¸«ŕ¸ˇŕą', + 'Invalid MO file revision: {revision}.' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸ŕą‰ŕą„ขไฟล์ MO ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡: {revision}.', + 'Invalid MO file: {file} (magic: {magic}).' => 'ไฟล์ MO ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡: {file} (magic: {magic}).', + 'Invalid enumerable value "{value}". Please make sure it is among ({enum}).' => 'คŕąŕ¸˛ŕ¸‚อง enumerable "{value}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸šŕ¸§ŕąŕ¸˛ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸„ŕąŕ¸˛ŕąŕ¸™ ({enum}) นี้', + 'List data must be an array or an object implementing Traversable.' => 'รายŕ¸ŕ¸˛ŕ¸Łŕ¸‚้อมูลŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕ¸Łŕ¸ąŕ¸›ŕ¸‚องอาร์เรย์หรือรูปŕąŕ¸šŕ¸š Traversable', + 'List index "{index}" is out of bound.' => 'ไมŕąŕ¸žŕ¸šŕ¸Łŕ¸˛ŕ¸˘ŕ¸ŕ¸˛ŕ¸Ł "{index}" ŕąŕ¸™ŕ¸«ŕ¸™ŕą‰ŕ¸˛ŕąŕ¸Łŕ¸', + 'Login Required' => 'ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕą€ŕ¸‚้าสูŕąŕ¸Łŕ¸°ŕ¸šŕ¸š', + 'Map data must be an array or an object implementing Traversable.' => 'ŕąŕ¸śŕ¸™ŕ¸śŕ¸±ŕ¸‡ŕ¸‚้อมูลŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕ¸Łŕ¸ąŕ¸›ŕ¸‚องอาร์เรย์หรือรูปŕąŕ¸šŕ¸š Traversable', + 'Missing the temporary folder to store the uploaded file "{file}".' => 'ไมŕąŕ¸žŕ¸šŕą„ดเรŕ¸ŕ¸—อรีŕąŕą„ฟล์ชัŕąŕ¸§ŕ¸„ราวŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸ŕą‡ŕ¸šŕą„ฟล์ "{file}" ทีŕąŕ¸­ŕ¸±ŕ¸žŕą‚หลดเข้ามา', + 'No columns are being updated for table "{table}".' => 'ไมŕąŕ¸ˇŕ¸µŕ¸źŕ¸´ŕ¸Ąŕ¸”์ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕ¸›ŕ¸Łŕ¸±ŕ¸šŕ¸›ŕ¸Łŕ¸¸ŕ¸‡ŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡ "{table}"', + 'No counter columns are being updated for table "{table}".' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–นับฟิลด์ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕ¸›ŕ¸Łŕ¸±ŕ¸šŕ¸›ŕ¸Łŕ¸¸ŕ¸‡ŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡ "{table}" ได้', + 'Object configuration must be an array containing a "class" element.' => 'ŕ¸ŕ¸˛ŕ¸Łŕ¸ŕ¸łŕ¸«ŕ¸™ŕ¸”โครงสร้างข้อมูลŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕ¸Łŕ¸ąŕ¸›ŕąŕ¸šŕ¸šŕ¸­ŕ¸˛ŕ¸ŁŕąŚŕą€ŕ¸Łŕ¸˘ŕąŚŕ¸‚อง "class" เทŕąŕ¸˛ŕ¸™ŕ¸±ŕą‰ŕ¸™', + 'Please fix the following input errors:' => 'โปรดŕąŕ¸ŕą‰ŕą„ขข้อผิดพลาดทางด้านลŕąŕ¸˛ŕ¸‡:', + 'Property "{class}.{property}" is not defined.' => 'คุณสมบัติ "{class}.{property}" ยังไมŕąŕ¸–ูŕ¸ŕ¸ŕ¸łŕ¸«ŕ¸™ŕ¸”', + 'Property "{class}.{property}" is read only.' => 'คุณสมบัติ "{class}.{property}" สามารถอŕąŕ¸˛ŕ¸™ŕą„ด้เทŕąŕ¸˛ŕ¸™ŕ¸±ŕą‰ŕ¸™', + 'Queue data must be an array or an object implementing Traversable.' => 'ข้อมูลŕ¸ŕ¸˛ŕ¸ŕ¸ŕ¸˛ŕ¸Łŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕ¸ŕ¸°ŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕ¸Łŕ¸ąŕ¸›ŕ¸‚องอาร์เรย์หรือรูปŕąŕ¸šŕ¸š Traversable', + 'Relation "{name}" is not defined in active record class "{class}".' => 'ความสัมพันŕ¸ŕąŚŕ¸‚อง "{name}" ไมŕąŕą„ด้ถูŕ¸ŕ¸ŕ¸łŕ¸«ŕ¸™ŕ¸”เอาไว้ŕąŕ¸™ "{class}"', + 'Stack data must be an array or an object implementing Traversable.' => 'ข้อมูลสŕąŕ¸•ŕ¸ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕ¸Łŕ¸ąŕ¸›ŕ¸‚องอาร์เรย์หรือรูปŕąŕ¸šŕ¸š Traversable', + 'Table "{table}" does not have a column named "{column}".' => 'ตาราง "{table}" ไมŕąŕ¸ˇŕ¸µŕ¸źŕ¸´ŕ¸Ąŕ¸”์ชืŕąŕ¸­ "{column}" อยูŕąŕąŕ¸™ŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡', + 'Table "{table}" does not have a primary key defined.' => 'ตาราง "{table}" ยังไมŕąŕą„ด้ŕ¸ŕ¸łŕ¸«ŕ¸™ŕ¸”คีย์หลัŕ¸', + 'The "filter" property must be specified with a valid callback.' => 'คุณสมบัติ "filter" ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸–ูŕ¸ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕąŕ¸«ŕą‰ŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ŕ¸•ŕ¸Łŕ¸§ŕ¸ŕ¸Şŕ¸­ŕ¸š', + 'The "pattern" property must be specified with a valid regular expression.' => 'คุณสมบัติ "pattern" ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕąŕ¸«ŕą‰ŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸•ŕ¸˛ŕ¸ˇŕ¸Łŕ¸ąŕ¸›ŕąŕ¸šŕ¸šŕ¸‚อง Regular Expression', + 'The "view" property is required.' => 'ต้องŕ¸ŕ¸˛ŕ¸Łŕ¸„ุณสมบัติ "view" ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™', + 'The CSRF token could not be verified.' => 'CSRF ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ตรวŕ¸ŕ¸Şŕ¸­ŕ¸šŕą„ด้', + 'The URL pattern "{pattern}" for route "{route}" is not a valid regular expression.' => 'รูปŕąŕ¸šŕ¸šŕ¸—ีŕąŕ¸­ŕ¸˘ŕ¸ąŕą "{pattern}" สำหรับ "{route}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸•ŕ¸˛ŕ¸ˇŕ¸Łŕ¸ąŕ¸›ŕąŕ¸šŕ¸šŕ¸‚อง Regular Expression', + 'The active record cannot be deleted because it is new.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ลบข้อมูลทีŕąŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ŕ¸˛ŕ¸Łŕą„ด้ เนืŕąŕ¸­ŕ¸‡ŕ¸ŕ¸˛ŕ¸ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸‚้อมูลŕąŕ¸«ŕ¸ˇŕą', + 'The active record cannot be inserted to database because it is not new.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เพิŕąŕ¸ˇŕ¸‚้อมูลทีŕąŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ŕ¸˛ŕ¸Łŕą„ด้ เนืŕąŕ¸­ŕ¸‡ŕ¸ŕ¸˛ŕ¸ŕą„มŕąŕąŕ¸Šŕąŕ¸‚้อมูลŕąŕ¸«ŕ¸ˇŕą', + 'The active record cannot be updated because it is new.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ปรับปรุงข้อมูลทีŕąŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ŕ¸˛ŕ¸Łŕą„ด้ เนืŕąŕ¸­ŕ¸‡ŕ¸ŕ¸˛ŕ¸ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸‚้อมูลŕąŕ¸«ŕ¸ˇŕą', + 'The asset "{asset}" to be pulished does not exist.' => 'ไมŕąŕ¸žŕ¸šŕ¸‚้อมูล "{asset}" ทีŕąŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ŕ¸˛ŕ¸Ł', + 'The column "{column}" is not a foreign key in table "{table}".' => 'ฟิลด์ "{column}" ไมŕąŕąŕ¸Šŕąŕ¸„ีย์เชืŕąŕ¸­ŕ¸ˇŕ¸•ŕąŕ¸­ŕąŕ¸™ŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡ "{table}"', + 'The command path "{path}" is not a valid directory.' => 'ทีŕąŕ¸­ŕ¸˘ŕ¸ąŕąŕ¸„ำสัŕąŕ¸‡ "{path}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', + 'The controller path "{path}" is not a valid directory.' => 'ทีŕąŕ¸­ŕ¸˘ŕ¸ąŕąŕ¸Şŕąŕ¸§ŕ¸™ŕ¸„วบคุม "{path}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', + 'The file "{file}" cannot be uploaded. Only files with these extensions are allowed: {extensions}.' => 'ไฟล์ "{file}" ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–อัพโหลดได้ ไฟล์ทีŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–อัพโหลดได้ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕą€ŕ¸›ŕą‡ŕ¸™: {extensions}.', + 'The file "{file}" is too large. Its size cannot exceed {limit} bytes.' => 'ไฟล์ "{file}" มีขนาดŕąŕ¸«ŕ¸Ťŕąŕą„ป ไฟล์ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ˇŕ¸µŕ¸‚นาดไมŕąŕą€ŕ¸ŕ¸´ŕ¸™ {limit} ไบต์', + 'The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.' => 'ไฟล์ "{file}" มีขนาดเล็ŕ¸ŕą€ŕ¸ŕ¸´ŕ¸™ŕą„ป ไฟล์ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ˇŕ¸µŕ¸‚นาดมาŕ¸ŕ¸ŕ¸§ŕąŕ¸˛ {limit} ไบต์', + 'The file "{file}" was only partially uploaded.' => 'ไฟล์ "{file}" ถูŕ¸ŕ¸­ŕ¸±ŕ¸žŕą‚หลดไมŕąŕ¸Şŕ¸ˇŕ¸šŕ¸ąŕ¸Łŕ¸“์', + 'The first element in a filter configuration must be the filter class.' => 'ŕ¸ŕ¸˛ŕ¸Łŕ¸ŕ¸łŕ¸«ŕ¸™ŕ¸”คŕąŕ¸˛ŕ¸•ŕ¸±ŕ¸§ŕ¸ŕ¸Łŕ¸­ŕ¸‡ ตัวŕąŕ¸Łŕ¸ŕ¸ŕ¸°ŕąŕ¸—นคŕąŕ¸˛ŕ¸›ŕ¸Łŕ¸°ŕą€ŕ¸ ŕ¸—ของŕ¸ŕ¸˛ŕ¸Łŕ¸ŕ¸Łŕ¸­ŕ¸‡', + 'The item "{name}" does not exist.' => 'ไมŕąŕ¸žŕ¸šŕ¸Łŕ¸˛ŕ¸˘ŕ¸ŕ¸˛ŕ¸Ł "{name}"', + 'The item "{parent}" already has a child "{child}".' => 'รายŕ¸ŕ¸˛ŕ¸Ł "{parent}" มีรายŕ¸ŕ¸˛ŕ¸Łŕ¸˘ŕąŕ¸­ŕ¸˘ "{child}"', + 'The layout path "{path}" is not a valid directory.' => 'รูปŕąŕ¸šŕ¸šŕ¸—ีŕąŕ¸­ŕ¸˘ŕ¸ąŕą "{path}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', + 'The list is read only.' => 'รายŕ¸ŕ¸˛ŕ¸Łŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–อŕąŕ¸˛ŕ¸™ŕą„ด้เทŕąŕ¸˛ŕ¸™ŕ¸±ŕą‰ŕ¸™', + 'The map is read only.' => 'ŕąŕ¸śŕ¸™ŕ¸—ีŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–อŕąŕ¸˛ŕ¸™ŕą„ด้อยŕąŕ¸˛ŕ¸‡ŕą€ŕ¸”ียว', + 'The pattern for 12 hour format must be "h" or "hh".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งรูปŕąŕ¸šŕ¸š 12 ชัŕąŕ¸§ŕą‚มง ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "h" หรือ "hh"', + 'The pattern for 24 hour format must be "H" or "HH".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งรูปŕąŕ¸šŕ¸š 24 ชัŕąŕ¸§ŕą‚มง ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "H" หรือ "HH"', + 'The pattern for AM/PM marker must be "a".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งŕąŕ¸šŕ¸š AM/PM ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "a"', + 'The pattern for day in month must be "F".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งชืŕąŕ¸­ŕ¸‚องเดือนŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "F"', + 'The pattern for day in year must be "D", "DD" or "DDD".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งวันของปีŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "D", "DD" หรือ "DDD"', + 'The pattern for day of the month must be "d" or "dd".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งวันของเดือนŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "d" หรือ "dd"', + 'The pattern for day of the week must be "E", "EE", "EEE", "EEEE" or "EEEEE".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งวันของสัปดาห์ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "E", "EE", "EEE", "EEEE" หรือ "EEEEE"', + 'The pattern for era must be "G", "GG", "GGG", "GGGG" or "GGGGG".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งยุคŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "G", "GG", "GGG", "GGGG" หรือ "GGGGG"', + 'The pattern for hour in AM/PM must be "K" or "KK".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งชัŕąŕ¸§ŕą‚มง (ŕąŕ¸šŕ¸š AM/PM) ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "K" หรือ "KK"', + 'The pattern for hour in day must be "k" or "kk".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งชัŕąŕ¸§ŕą‚มงของวันŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "k" หรือ "kk"', + 'The pattern for minutes must be "m" or "mm".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งนาทีŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "m" หรือ "mm"', + 'The pattern for month must be "M", "MM", "MMM", or "MMMM".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งเดือนŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "M", "MM", "MMM", หรือ "MMMM"', + 'The pattern for seconds must be "s" or "ss".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งวินาทีŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "s" หรือ "ss"', + 'The pattern for time zone must be "z" or "v".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งโซนเวลาŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "z" หรือ "v"', + 'The pattern for week in month must be "W".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งสัปดาห์ของเดือนŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "W"', + 'The pattern for week in year must be "w".' => 'ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งสัปดาห์ของปีŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą€ŕ¸›ŕą‡ŕ¸™ "w"', + 'The queue is empty.' => 'สถานะŕ¸ŕ¸˛ŕ¸Łŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕ¸§ŕąŕ¸˛ŕ¸‡', + 'The relation "{relation}" in active record class "{class}" is not specified correctly: the join table "{joinTable}" given in the foreign key cannot be found in the database.' => 'ความสัมพันŕ¸ŕąŚŕ¸Łŕ¸°ŕ¸«ŕ¸§ŕąŕ¸˛ŕ¸‡ "{relation}" ŕ¸ŕ¸±ŕ¸š "{class}" ถูŕ¸ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕą„ว้ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡: ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Šŕ¸·ŕąŕ¸­ŕ¸ˇŕ¸•ŕąŕ¸­ŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡ "{joinTable}" ไมŕąŕ¸žŕ¸šŕ¸„ีย์เชืŕąŕ¸­ŕ¸ˇŕ¸„วามสัมพันŕ¸ŕąŚŕąŕ¸™ŕ¸ŕ¸˛ŕ¸™ŕ¸‚้อมูล', + 'The relation "{relation}" in active record class "{class}" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.' => 'ความสัมพันŕ¸ŕąŚŕ¸Łŕ¸°ŕ¸«ŕ¸§ŕąŕ¸˛ŕ¸‡ "{relation}" ŕ¸ŕ¸±ŕ¸š "{class}" ถูŕ¸ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕ¸„ีย์เชืŕąŕ¸­ŕ¸ˇŕ¸•ŕąŕ¸­ŕą„มŕąŕ¸Şŕ¸ˇŕ¸šŕ¸ąŕ¸Łŕ¸“์ คีย์เชืŕąŕ¸­ŕ¸ˇŕ¸•ŕąŕ¸­ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ˇŕ¸µŕ¸„ŕąŕ¸˛ŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡ŕ¸—ีŕąŕą€ŕ¸Šŕ¸·ŕąŕ¸­ŕ¸ˇŕ¸•ŕąŕ¸­ŕ¸ŕ¸±ŕ¸™ŕ¸—ั้งสองตาราง', + 'The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key "{key}". The foreign key does not point to either joining table.' => 'ความสัมพันŕ¸ŕąŚŕ¸Łŕ¸°ŕ¸«ŕ¸§ŕąŕ¸˛ŕ¸‡ "{relation}" ŕ¸ŕ¸±ŕ¸š "{class}" ถูŕ¸ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕ¸”้วยคีย์เชืŕąŕ¸­ŕ¸ˇŕ¸•ŕąŕ¸­ŕ¸—ีŕąŕą„มŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸„ือ "{key}" คีย์เชืŕąŕ¸­ŕ¸ˇŕ¸•ŕąŕ¸­ŕ¸™ŕ¸µŕą‰ŕą„มŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ระบุความสัมพันŕ¸ŕąŚŕ¸Łŕ¸°ŕ¸«ŕ¸§ŕąŕ¸˛ŕ¸‡ŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡ŕą„ด้', + 'The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key. The format of the foreign key must be "joinTable(fk1,fk2,...)".' => 'ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕ¸„ีย์ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Šŕ¸·ŕąŕ¸­ŕ¸ˇŕ¸•ŕąŕ¸­ŕ¸„วามสัมพันŕ¸ŕąŚŕ¸Łŕ¸°ŕ¸«ŕ¸§ŕąŕ¸˛ŕ¸‡ "{class}" ŕ¸ŕ¸±ŕ¸š "{relation}" รูปŕąŕ¸šŕ¸šŕ¸‚องคีย์เชืŕąŕ¸­ŕ¸ˇŕ¸•ŕąŕ¸­ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕ¸Łŕ¸ąŕ¸›ŕąŕ¸šŕ¸š "joinTable(fk1,fk2,...)"', + 'The requested controller "{controller}" does not exist.' => 'ไมŕąŕ¸žŕ¸šŕ¸Şŕąŕ¸§ŕ¸™ŕ¸„วบคุม "{controller}" ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ŕąŕ¸Šŕą‰', + 'The requested view "{name}" was not found.' => 'ไมŕąŕ¸žŕ¸š "{name}" ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ŕąŕ¸Šŕą‰', + 'The stack is empty.' => 'สŕąŕ¸•ŕ¸ŕ¸‚้อมูลเป็นคŕąŕ¸˛ŕ¸§ŕąŕ¸˛ŕ¸‡', + 'The system is unable to find the requested action "{action}".' => 'ระบบไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ดำเนินŕ¸ŕ¸˛ŕ¸Ł "{action}" ตามคำสัŕąŕ¸‡ŕą„ด้', + 'The system view path "{path}" is not a valid directory.' => 'ระบบตรวŕ¸ŕ¸žŕ¸šŕ¸—ีŕąŕ¸­ŕ¸˘ŕ¸ąŕą "{path}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', + 'The table "{table}" for active record class "{class}" cannot be found in the database.' => 'ตาราง "{table}" สำหรับบันทึŕ¸ŕ¸‚้อมูลของ "{class}" ไมŕąŕ¸žŕ¸šŕąŕ¸™ŕ¸ŕ¸˛ŕ¸™ŕ¸‚้อมูล', + 'The value for the primary key "{key}" is not supplied when querying the table "{table}".' => 'คŕąŕ¸˛ŕ¸‚องคีย์หลัภ"{key}" ไมŕąŕ¸Łŕ¸­ŕ¸‡ŕ¸Łŕ¸±ŕ¸šŕą€ŕ¸ˇŕ¸·ŕąŕ¸­ŕ¸ˇŕ¸µŕ¸ŕ¸˛ŕ¸Łŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡ "{table}"', + 'The verification code is incorrect.' => 'รหัสป้องŕ¸ŕ¸±ŕ¸™ŕą„มŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', + 'The view path "{path}" is not a valid directory.' => 'ทีŕąŕ¸­ŕ¸˘ŕ¸ąŕą "{path}" ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', + 'Theme directory "{directory}" does not exist.' => 'ไมŕąŕ¸žŕ¸šŕą„ดเรŕ¸ŕ¸—อรีŕąŕ¸Łŕ¸ąŕ¸›ŕąŕ¸šŕ¸š "{directory}" ', + 'This content requires the Adobe Flash Player.' => 'ต้องŕ¸ŕ¸˛ŕ¸Ł Adobe Flash Player ŕąŕ¸™ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Şŕ¸”งข้อมูล', + 'Unable to add an item whose name is the same as an existing item.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เพิŕąŕ¸ˇŕ¸Łŕ¸˛ŕ¸˘ŕ¸ŕ¸˛ŕ¸Łŕ¸—ีŕąŕ¸ˇŕ¸µŕ¸Šŕ¸·ŕąŕ¸­ŕą€ŕ¸”ียวŕ¸ŕ¸±ŕ¸™ŕą„ด้', + 'Unable to change the item name. The name "{name}" is already used by another item.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เปลีŕąŕ¸˘ŕ¸™ŕ¸Šŕ¸·ŕąŕ¸­ŕ¸Łŕ¸˛ŕ¸˘ŕ¸ŕ¸˛ŕ¸Łŕą„ด้ "{name}" ŕ¸ŕ¸łŕ¸Ąŕ¸±ŕ¸‡ŕ¸–ูŕ¸ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕ¸ŕ¸˛ŕ¸ŕ¸Łŕ¸˛ŕ¸˘ŕ¸ŕ¸˛ŕ¸Łŕ¸­ŕ¸·ŕąŕ¸™', + 'Unable to create application state file "{file}". Make sure the directory containing the file exists and is writable by the Web server process.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–สร้างไฟล์ระบบ "{file}" ได้ ŕ¸ŕ¸Łŕ¸¸ŕ¸“าตรวŕ¸ŕ¸Şŕ¸­ŕ¸šŕ¸§ŕąŕ¸˛ŕą„ดเรŕ¸ŕ¸—อรีŕąŕą€ŕ¸ŕą‡ŕ¸šŕą„ฟล์ สามารถเขียนได้โดยคำสัŕąŕ¸‡ŕ¸‚องเซิฟเวอร์', + 'Unable to find the decorator view "{view}".' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–หาสŕąŕ¸§ŕ¸™ŕ¸„วบคุมของ "{view}" ได้', + 'Unable to find the list item.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–หารายŕ¸ŕ¸˛ŕ¸Łŕą„ด้', + 'Unable to lock file "{file}" for reading.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ล็อคไฟล์ "{file}" สำหรับอŕąŕ¸˛ŕ¸™ŕą„ด้', + 'Unable to lock file "{file}" for writing.' => ' ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ล็อคไฟล์ "{file}" สำหรับเขียนได้', + 'Unable to read file "{file}".' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–อŕąŕ¸˛ŕ¸™ŕą„ฟล์ "{file}" ได้', + 'Unable to replay the action "{object}.{method}". The method does not exist.' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ดำเนินŕ¸ŕ¸˛ŕ¸Ł "{object}.{method}" ซ้ำได้ ŕ¸ŕ¸˛ŕ¸Łŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕ¸–ูŕ¸ŕ¸Łŕ¸°ŕ¸‡ŕ¸±ŕ¸š', + 'Unable to write file "{file}".' => 'ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เขียนทับไฟล์ "{file}" ได้', + 'Unknown authorization item "{name}".' => 'ไมŕąŕ¸žŕ¸šŕ¸Łŕ¸˛ŕ¸˘ŕ¸ŕ¸˛ŕ¸Ł "{name}"', + 'Unrecognized locale "{locale}".' => 'ไมŕąŕ¸žŕ¸šŕ¸›ŕ¸Ąŕ¸˛ŕ¸˘ŕ¸—าง "{locale}"', + 'View file "{file}" does not exist.' => 'ไมŕąŕ¸žŕ¸šŕą„ฟล์ "{file}" ทีŕąŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ŕ¸˛ŕ¸Łŕ¸”ู', + 'Yii application can only be created once.' => 'โปรŕąŕ¸ŕ¸Łŕ¸ˇ Yii สามารถสร้างได้เพียงครั้งเดียวเทŕąŕ¸˛ŕ¸™ŕ¸±ŕą‰ŕ¸™', + 'You are not authorized to perform this action.' => 'คุณไมŕąŕą„ด้รับอนุญาตŕąŕ¸«ŕą‰ŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕ¸”ังŕ¸ŕ¸Ąŕąŕ¸˛ŕ¸§', + 'Your request is not valid.' => 'ŕ¸ŕ¸˛ŕ¸Łŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Łŕą„มŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', + '{attribute} "{value}" has already been taken.' => '{attribute} "{value}" ได้รับŕ¸ŕ¸˛ŕ¸Łŕ¸šŕ¸±ŕ¸™ŕ¸—ึŕ¸', + '{attribute} cannot be blank.' => '{attribute} ไมŕąŕ¸„วรเป็นคŕąŕ¸˛ŕ¸§ŕąŕ¸˛ŕ¸‡', + '{attribute} is invalid.' => '{attribute} ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', + '{attribute} is not a valid URL.' => '{attribute} URL ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', + '{attribute} is not a valid email address.' => '{attribute} รูปŕąŕ¸šŕ¸šŕ¸­ŕ¸µŕą€ŕ¸ˇŕ¸ĄŕąŚŕą„มŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡', + '{attribute} is not in the list.' => '{attribute} ไมŕąŕ¸ˇŕ¸µŕ¸­ŕ¸˘ŕ¸ąŕąŕąŕ¸™ŕ¸Łŕ¸˛ŕ¸˘ŕ¸ŕ¸˛ŕ¸Ł', + '{attribute} is of the wrong length (should be {length} characters).' => '{attribute} ขนาดความยาวไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ (ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ˇŕ¸µŕ¸‚นาด {length} ตัวอัŕ¸ŕ¸©ŕ¸Ł)', + '{attribute} is too big (maximum is {max}).' => '{attribute} มีขนาดŕąŕ¸«ŕ¸Ťŕąŕą€ŕ¸ŕ¸´ŕ¸™ŕą„ป (ขนาดสูงสุด {max})', + '{attribute} is too long (maximum is {max} characters).' => '{attribute} ยาวเŕ¸ŕ¸´ŕ¸™ŕą„ป (ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕą„มŕąŕą€ŕ¸ŕ¸´ŕ¸™ {max} ตัวอัŕ¸ŕ¸©ŕ¸Ł)', + '{attribute} is too short (minimum is {min} characters).' => '{attribute} สั้นเŕ¸ŕ¸´ŕ¸™ŕą„ป (ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ˇŕ¸˛ŕ¸ŕ¸ŕ¸§ŕąŕ¸˛ {min} ตัวอัŕ¸ŕ¸©ŕ¸Ł)', + '{attribute} is too small (minimum is {min}).' => '{attribute} มีขนาดเล็ŕ¸ŕą€ŕ¸ŕ¸´ŕ¸™ŕą„ป (ขนาดเล็ŕ¸ŕ¸Şŕ¸¸ŕ¸” {min})', + '{attribute} must be a number.' => '{attribute} ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸•ŕ¸±ŕ¸§ŕą€ŕ¸Ąŕ¸‚เทŕąŕ¸˛ŕ¸™ŕ¸±ŕą‰ŕ¸™', + '{attribute} must be an integer.' => '{attribute} ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕą€ŕ¸›ŕą‡ŕ¸™ŕ¸ŕ¸łŕ¸™ŕ¸§ŕ¸™ŕą€ŕ¸•ŕą‡ŕ¸ˇŕą€ŕ¸—ŕąŕ¸˛ŕ¸™ŕ¸±ŕą‰ŕ¸™', + '{attribute} must be repeated exactly.' => '{attribute} ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ˇŕ¸µŕ¸„ŕąŕ¸˛ŕą€ŕ¸«ŕ¸ˇŕ¸·ŕ¸­ŕ¸™ŕ¸ŕ¸±ŕ¸™', + '{attribute} must be {type}.' => '{attribute} ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕą€ŕ¸›ŕą‡ŕ¸™ {type}.', + '{className} does not support add() functionality.' => '{className} ไมŕąŕ¸Łŕ¸­ŕ¸‡ŕ¸Łŕ¸±ŕ¸šŕ¸źŕ¸±ŕ¸‡ŕ¸ŕąŚŕ¸Šŕ¸±ŕąŕ¸™ add()', + '{className} does not support delete() functionality.' => '{className} ไมŕąŕ¸Łŕ¸­ŕ¸‡ŕ¸Łŕ¸±ŕ¸šŕ¸źŕ¸±ŕ¸‡ŕ¸ŕąŚŕ¸Šŕ¸±ŕąŕ¸™ delete()', + '{className} does not support flush() functionality.' => '{className} ไมŕąŕ¸Łŕ¸­ŕ¸‡ŕ¸Łŕ¸±ŕ¸šŕ¸źŕ¸±ŕ¸‡ŕ¸ŕąŚŕ¸Šŕ¸±ŕąŕ¸™ flush()', + '{className} does not support get() functionality.' => '{className} ไมŕąŕ¸Łŕ¸­ŕ¸‡ŕ¸Łŕ¸±ŕ¸šŕ¸źŕ¸±ŕ¸‡ŕ¸ŕąŚŕ¸Šŕ¸±ŕąŕ¸™ get()', + '{className} does not support set() functionality.' => '{className} ไมŕąŕ¸Łŕ¸­ŕ¸‡ŕ¸Łŕ¸±ŕ¸šŕ¸źŕ¸±ŕ¸‡ŕ¸ŕąŚŕ¸Šŕ¸±ŕąŕ¸™ set()', + '{class} does not have attribute "{name}".' => '{class} ไมŕąŕ¸ˇŕ¸µŕ¸‚้อมูลของ "{name}"', + '{class} does not have relation "{name}".' => '{class} ไมŕąŕ¸ˇŕ¸µŕ¸„วามสัมพันŕ¸ŕąŚŕ¸ŕ¸±ŕ¸™ŕ¸ŕ¸±ŕ¸š "{name}"', + '{class} does not support fetching all table names.' => '{class} ไมŕąŕ¸Łŕ¸­ŕ¸‡ŕ¸Łŕ¸±ŕ¸šŕ¸ŕ¸˛ŕ¸Łŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ŕ¸‚้อมูลŕ¸ŕ¸˛ŕ¸ŕ¸•ŕ¸˛ŕ¸Łŕ¸˛ŕ¸‡ŕ¸—ั้งหมด', + '{class} has an invalid validation rule. The rule must specify attributes to be validated and the validator name.' => '{class} ไมŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸•ŕ¸˛ŕ¸ˇŕą€ŕ¸‡ŕ¸·ŕąŕ¸­ŕ¸™ŕą„ข. ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸—ำŕ¸ŕ¸˛ŕ¸Łŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕ¸„ŕąŕ¸˛ŕ¸‚องข้อมูลŕąŕ¸Ąŕ¸°ŕ¸Šŕ¸·ŕąŕ¸­ŕ¸‚องข้อมูล', + '{class} must specify "model" and "attribute" or "name" property values.' => '{class} ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸—ำŕ¸ŕ¸˛ŕ¸Łŕ¸Łŕ¸°ŕ¸šŕ¸¸ŕ¸„ŕąŕ¸˛ "model" ŕąŕ¸Ąŕ¸° "attribute" หรือ "name" เหลŕąŕ¸˛ŕ¸™ŕ¸µŕą‰ŕ¸”้วย', + '{class}.allowAutoLogin must be set true in order to use cookie-based authentication.' => '{class}.allowAutoLogin ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕą€ŕ¸›ŕ¸´ŕ¸”ŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕą€ŕ¸ˇŕ¸·ŕąŕ¸­ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸ŕ¸˛ŕ¸Łŕąŕ¸Šŕą‰ŕ¸‡ŕ¸˛ŕ¸™ŕ¸„ุ๊ŕ¸ŕ¸ŕ¸µŕą‰', + '{class}::authenticate() must be implemented.' => '{class}::authenticate() ŕ¸ŕ¸°ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸–ูŕ¸ŕ¸”ำเนินŕ¸ŕ¸˛ŕ¸Ł', + '{controller} cannot find the requested view "{view}".' => '{controller} ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ค้นหา "{view}" ตามคำสัŕąŕ¸‡ŕą„ด้', + '{controller} contains improperly nested widget tags in its view "{view}". A {widget} widget does not have an endWidget() call.' => '{controller} มีชุดคำสัŕąŕ¸‡ŕ¸—ีŕąŕą„มŕąŕ¸–ูŕ¸ŕ¸•ŕą‰ŕ¸­ŕ¸‡ŕ¸«ŕ¸Ąŕ¸˛ŕ¸˘ŕ¸­ŕ¸˘ŕąŕ¸˛ŕ¸‡ŕąŕ¸™ŕ¸ˇŕ¸¸ŕ¸ˇŕ¸ˇŕ¸­ŕ¸‡ŕ¸‚อง "{view}" ทำŕąŕ¸«ŕą‰ {widget} ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–เรียŕ¸ŕąŕ¸Šŕą‰ŕ¸„ำสัŕąŕ¸‡ endWidget() ได้', + '{controller} has an extra endWidget({id}) call in its view.' => '{controller} ŕ¸ŕ¸łŕ¸Ąŕ¸±ŕ¸‡ŕ¸–ูŕ¸ŕą€ŕ¸Łŕ¸µŕ¸˘ŕ¸ŕąŕ¸Šŕą‰ŕą‚ดย endWidget({id}) ŕąŕ¸™ŕ¸‚ณะนี้', + '{widget} cannot find the view "{view}".' => '{widget} ไมŕąŕ¸Şŕ¸˛ŕ¸ˇŕ¸˛ŕ¸Łŕ¸–ดู "{view}" ได้', +); diff --git a/framework/messages/uk/zii.php b/framework/messages/uk/zii.php index ad52bdeccb4..79c41d27cd8 100644 --- a/framework/messages/uk/zii.php +++ b/framework/messages/uk/zii.php @@ -1,36 +1,36 @@ - 'Головна', - 'The button type "{type}" is not supported.' => 'Тип кнопки "{type}" не підтримŃєтьŃŃŹ.', - 'Are you sure you want to delete this item?' => 'Ви впевнені, що хочете видалити цей елемент?', - 'Delete' => 'Видалити', - 'Displaying {start}-{end} of 1 result.|Displaying {start}-{end} of {count} results.' => 'Елементи {start}—{end} Ń–Đ· {count}.', - 'Either "name" or "value" must be specified for CDataColumn.' => 'Для CDataColumn необхідно вказати або "name" або "value".', - 'No results found.' => 'Немає резŃльтатів.', - 'Not set' => 'Не заданий', - 'Please specify the "attributes" property.' => 'Визначте влаŃтивіŃŃ‚ŃŚ "attributes".', - 'Please specify the "data" property.' => 'Визначте влаŃтивіŃŃ‚ŃŚ "data".', - 'Sort by: ' => 'СортŃвання: ', - 'The "dataProvider" property cannot be empty.' => 'ВлаŃтивіŃŃ‚ŃŚ "dataProvider" не може бŃти порожньою.', - 'The attribute must be specified in the format of "Name:Type:Label", where "Type" and "Label" are optional.' => 'ĐтрибŃŃ‚ повинен бŃти заданий Ń Ń„ĐľŃ€ĐĽĐ°Ń‚Ń– "Імʼя:Тип:Заголовок". "Тип" Ń‚Đ° "Заголовок" не обовʼязкові.', - 'The column must be specified in the format of "Name:Type:Label", where "Type" and "Label" are optional.' => 'Стовпець повинен бŃти заданий Ń Ń„ĐľŃ€ĐĽĐ°Ń‚Ń– "Name:Type:Label". "Тип" Ń‚Đ° "Заголовок" не обовʼязкові.', - 'The property "itemView" cannot be empty.' => 'ВлаŃтивіŃŃ‚ŃŚ "itemView" не може бŃти порожньою.', - 'Total 1 result.|Total {count} results.' => 'Đ’Ńього {count} резŃльтат.|Đ’Ńього {count} резŃльтата.|Đ’Ńього {count} резŃльтатів.|Đ’Ńього {count} резŃльтата.', - 'Update' => 'РедагŃвати', - 'View' => 'ПереглянŃти', - '{class} must specify "model" and "attribute" or "name" property values.' => 'ĐšĐ»Đ°Ń {class} повинен визначати значення влаŃтивоŃтей "model" Ń‚Đ° "attribute", або значення "name".', -); + 'Головна', + 'The button type "{type}" is not supported.' => 'Тип кнопки "{type}" не підтримŃєтьŃŃŹ.', + 'Are you sure you want to delete this item?' => 'Ви впевнені, що хочете видалити цей елемент?', + 'Delete' => 'Видалити', + 'Displaying {start}-{end} of 1 result.|Displaying {start}-{end} of {count} results.' => 'Елементи {start}—{end} Ń–Đ· {count}.', + 'Either "name" or "value" must be specified for CDataColumn.' => 'Для CDataColumn необхідно вказати або "name" або "value".', + 'No results found.' => 'Немає резŃльтатів.', + 'Not set' => 'Не заданий', + 'Please specify the "attributes" property.' => 'Визначте влаŃтивіŃŃ‚ŃŚ "attributes".', + 'Please specify the "data" property.' => 'Визначте влаŃтивіŃŃ‚ŃŚ "data".', + 'Sort by: ' => 'СортŃвання: ', + 'The "dataProvider" property cannot be empty.' => 'ВлаŃтивіŃŃ‚ŃŚ "dataProvider" не може бŃти порожньою.', + 'The attribute must be specified in the format of "Name:Type:Label", where "Type" and "Label" are optional.' => 'ĐтрибŃŃ‚ повинен бŃти заданий Ń Ń„ĐľŃ€ĐĽĐ°Ń‚Ń– "Імʼя:Тип:Заголовок". "Тип" Ń‚Đ° "Заголовок" не обовʼязкові.', + 'The column must be specified in the format of "Name:Type:Label", where "Type" and "Label" are optional.' => 'Стовпець повинен бŃти заданий Ń Ń„ĐľŃ€ĐĽĐ°Ń‚Ń– "Name:Type:Label". "Тип" Ń‚Đ° "Заголовок" не обовʼязкові.', + 'The property "itemView" cannot be empty.' => 'ВлаŃтивіŃŃ‚ŃŚ "itemView" не може бŃти порожньою.', + 'Total 1 result.|Total {count} results.' => 'Đ’Ńього {count} резŃльтат.|Đ’Ńього {count} резŃльтата.|Đ’Ńього {count} резŃльтатів.|Đ’Ńього {count} резŃльтата.', + 'Update' => 'РедагŃвати', + 'View' => 'ПереглянŃти', + '{class} must specify "model" and "attribute" or "name" property values.' => 'ĐšĐ»Đ°Ń {class} повинен визначати значення влаŃтивоŃтей "model" Ń‚Đ° "attribute", або значення "name".', +); diff --git a/framework/test/CTestCase.php b/framework/test/CTestCase.php index a62052a3a70..2a7c82e1800 100644 --- a/framework/test/CTestCase.php +++ b/framework/test/CTestCase.php @@ -9,17 +9,17 @@ */ if(!class_exists('PHPUnit_Runner_Version')) { - require_once('PHPUnit/Runner/Version.php'); - require_once('PHPUnit/Util/Filesystem.php'); // workaround for PHPUnit <= 3.6.11 + require_once('PHPUnit/Runner/Version.php'); + require_once('PHPUnit/Util/Filesystem.php'); // workaround for PHPUnit <= 3.6.11 - spl_autoload_unregister(array('YiiBase','autoload')); - require_once('PHPUnit/Autoload.php'); - spl_autoload_register(array('YiiBase','autoload')); // put yii's autoloader at the end + spl_autoload_unregister(array('YiiBase','autoload')); + require_once('PHPUnit/Autoload.php'); + spl_autoload_register(array('YiiBase','autoload')); // put yii's autoloader at the end - if (in_array('phpunit_autoload', spl_autoload_functions())) { // PHPUnit >= 3.7 'phpunit_autoload' was obsoleted - spl_autoload_unregister('phpunit_autoload'); - Yii::registerAutoloader('phpunit_autoload'); - } + if (in_array('phpunit_autoload', spl_autoload_functions())) { // PHPUnit >= 3.7 'phpunit_autoload' was obsoleted + spl_autoload_unregister('phpunit_autoload'); + Yii::registerAutoloader('phpunit_autoload'); + } } /** diff --git a/framework/utils/CDateTimeParser.php b/framework/utils/CDateTimeParser.php index be9c359f6b5..1c6192d581b 100644 --- a/framework/utils/CDateTimeParser.php +++ b/framework/utils/CDateTimeParser.php @@ -26,8 +26,8 @@ * y | 4 year digit, e.g., 2005 (available since 1.1.16) * yy | 2 year digit, e.g., 96, 05 * yyyy | 4 year digit, e.g., 2005 - * h | Hour in 0 to 23, no padding - * hh | Hour in 00 to 23, zero leading + * h | Hour in 0 to 12, no padding + * hh | Hour in 00 to 12, zero leading * H | Hour in 0 to 23, no padding * HH | Hour in 00 to 23, zero leading * m | Minutes in 0 to 59, no padding @@ -195,15 +195,15 @@ public static function parse($value,$pattern='MM/dd/yyyy',$defaults=array()) } case 'a': { - if(($ampm=self::parseAmPm($value,$i))===false) - return false; - if(isset($hour)) - { - if($hour==12 && $ampm==='am') - $hour=0; - elseif($hour<12 && $ampm==='pm') - $hour+=12; - } + if(($ampm=self::parseAmPm($value,$i))===false) + return false; + if(isset($hour)) + { + if($hour==12 && $ampm==='am') + $hour=0; + elseif($hour<12 && $ampm==='pm') + $hour+=12; + } $i+=2; break; } diff --git a/framework/utils/CFormatter.php b/framework/utils/CFormatter.php index 4fbebb65e00..69ee89b4de6 100644 --- a/framework/utils/CFormatter.php +++ b/framework/utils/CFormatter.php @@ -168,7 +168,7 @@ public function formatNtext($value,$paragraphs=false,$removeEmptyParagraphs=true { $value='

      '.str_replace(array("\r\n", "\n", "\r"), '

      ',$value).'

      '; if($removeEmptyParagraphs) - $value=preg_replace('/(<\/p>

      ){2,}/i','

      ',$value); + $value=preg_replace('/(<\/p>

      ){2,}/i','

      ',$value); return $value; } else diff --git a/framework/utils/CPasswordHelper.php b/framework/utils/CPasswordHelper.php index 0ad581e3b41..0d3692be5c9 100644 --- a/framework/utils/CPasswordHelper.php +++ b/framework/utils/CPasswordHelper.php @@ -15,11 +15,13 @@ * environments through the PHP {@link http://php.net/manual/en/function.crypt.php crypt()} * built-in function. As of Dec 2012 it is the strongest algorithm available in PHP * and the only algorithm without some security concerns surrounding it. For this reason, - * CPasswordHelper fails to initialize when run in and environment that does not have - * crypt() and its Blowfish option. Systems with the option include: + * CPasswordHelper fails when run in an environment that does not have + * crypt() with its Blowfish option and $2y hash fix. Compatible system is: + * * (1) Most *nix systems since PHP 4 (the algorithm is part of the library function crypt(3)); - * (2) All PHP systems since 5.3.0; (3) All PHP systems with the - * {@link http://www.hardened-php.net/suhosin/ Suhosin patch}. + * (2) Any PHP since 5.3.7 or PHP with the {@link http://www.hardened-php.net/suhosin/ Suhosin patch} including + * $2y fix backported. Note that Debian's 5.3.3 is not supported. + * * For more information about password hashing, crypt() and Blowfish, please read * the Yii Wiki article * {@link http://www.yiiframework.com/wiki/425/use-crypt-for-password-storage/ Use crypt() for password storage}. @@ -66,7 +68,7 @@ protected static function checkBlowfish() throw new CException(Yii::t('yii', '{class} requires the Blowfish option of the PHP crypt() function. This system does not have it.', array('{class}'=>__CLASS__))); - } + } /** * Generate a secure hash from a password and a random salt. @@ -97,7 +99,7 @@ public static function hashPassword($password,$cost=13) throw new CException(Yii::t('yii','Internal error while generating hash.')); return $hash; - } + } /** * Verify a password against a hash. @@ -183,7 +185,7 @@ public static function generateSalt($cost=13) $cost=(int)$cost; if($cost<4 || $cost>31) - throw new CException(Yii::t('yii','{class}::$cost must be between 4 and 31.',array('{class}'=>__CLASS__))); + throw new CException(Yii::t('yii','{class}::$cost must be between 4 and 31.',array('{class}'=>__CLASS__))); if(($random=Yii::app()->getSecurityManager()->generateRandomString(22,true))===false) if(($random=Yii::app()->getSecurityManager()->generateRandomString(22,false))===false) diff --git a/framework/utils/CPropertyValue.php b/framework/utils/CPropertyValue.php index 385a58fdc29..ddb91810fc2 100644 --- a/framework/utils/CPropertyValue.php +++ b/framework/utils/CPropertyValue.php @@ -94,11 +94,14 @@ public static function ensureFloat($value) } /** - * Converts a value to array type. If the value is a string and it is - * in the form (a,b,c) then an array consisting of each of the elements - * will be returned. If the value is a string and it is not in this form - * then an array consisting of just the string will be returned. If the value - * is not a string then + * Converts a value to array type. + * + * If the value is a string and it is in the form (a,b,c) then an array + * consisting of each of the elements will be returned. If the value is a string + * and it is not in this form then an array consisting of just the string will be returned, + * if the string is empty an empty array will be returned. + * If the value is not a string then it will return an array containing that value or + * the same value in case it is already an array. * @param mixed $value the value to be converted. * @return array */ diff --git a/framework/utils/CTimestamp.php b/framework/utils/CTimestamp.php index 621945b897a..c165d220eb2 100644 --- a/framework/utils/CTimestamp.php +++ b/framework/utils/CTimestamp.php @@ -67,11 +67,11 @@ public static function getDayofWeek($year, $month, $day) } if($month > 2) - $month -= 2; + $month -= 2; else { - $month += 10; - $year--; + $month += 10; + $year--; } $day = floor((13 * $month - 1) / 5) + diff --git a/framework/validators/CBooleanValidator.php b/framework/validators/CBooleanValidator.php index d4eed1d9d8c..d329ba4db83 100644 --- a/framework/validators/CBooleanValidator.php +++ b/framework/validators/CBooleanValidator.php @@ -56,8 +56,8 @@ protected function validateAttribute($object,$attribute) $value=$object->$attribute; if($this->allowEmpty && $this->isEmpty($value)) return; - if(!$this->strict && $value!=$this->trueValue && $value!=$this->falseValue - || $this->strict && $value!==$this->trueValue && $value!==$this->falseValue) + + if(!$this->validateValue($value)) { $message=$this->message!==null?$this->message:Yii::t('yii','{attribute} must be either {true} or {false}.'); $this->addError($object,$attribute,$message,array( @@ -66,6 +66,25 @@ protected function validateAttribute($object,$attribute) )); } } + + /** + * Validates a static value to see if it is a valid boolean. + * This method is provided so that you can call it directly without going + * through the model validation rule mechanism. + * + * Note that this method does not respect the {@link allowEmpty} property. + * + * @param mixed $value the value to be validated + * @return boolean whether the value is a valid boolean + * @since 1.1.17 + */ + public function validateValue($value) + { + if ($this->strict) + return $value===$this->trueValue || $value===$this->falseValue; + else + return $value==$this->trueValue || $value==$this->falseValue; + } /** * Returns the JavaScript needed for performing client-side validation. diff --git a/framework/validators/CEmailValidator.php b/framework/validators/CEmailValidator.php index b1b98bdfa6b..7a8a9b9445c 100644 --- a/framework/validators/CEmailValidator.php +++ b/framework/validators/CEmailValidator.php @@ -68,6 +68,8 @@ class CEmailValidator extends CValidator protected function validateAttribute($object,$attribute) { $value=$object->$attribute; + if($this->allowEmpty && $this->isEmpty($value)) + return; if(!$this->validateValue($value)) { @@ -78,17 +80,17 @@ protected function validateAttribute($object,$attribute) /** * Validates a static value to see if it is a valid email. - * Note that this method does not respect {@link allowEmpty} property. * This method is provided so that you can call it directly without going through the model validation rule mechanism. + * + * Note that this method does not respect the {@link allowEmpty} property. + * * @param mixed $value the value to be validated * @return boolean whether the value is a valid email * @since 1.1.1 + * @see https://github.com/yiisoft/yii/issues/3764#issuecomment-75457805 */ public function validateValue($value) { - if($this->allowEmpty && $this->isEmpty($value)) - return true; - if(is_string($value) && $this->validateIDN) $value=$this->encodeIDN($value); // make sure string length is limited to avoid DOS attacks diff --git a/framework/validators/CFileValidator.php b/framework/validators/CFileValidator.php index bc30d4ff3e5..070560ce208 100644 --- a/framework/validators/CFileValidator.php +++ b/framework/validators/CFileValidator.php @@ -301,7 +301,9 @@ protected function getSizeLimit() } /** - * Converts php.ini style size to bytes. Examples of size strings are: 150, 1g, 500k, 5M (size suffix + * Converts php.ini style size to bytes. + * + * Examples of size strings are: 150, 1g, 500k, 5M (size suffix * is case insensitive). If you pass here the number with a fractional part, then everything after * the decimal point will be ignored (php.ini values common behavior). For example 1.5G value would be * treated as 1G and 1073741824 number will be returned as a result. This method is public diff --git a/framework/validators/CRangeValidator.php b/framework/validators/CRangeValidator.php index aa72ad12aa8..27a703db9aa 100644 --- a/framework/validators/CRangeValidator.php +++ b/framework/validators/CRangeValidator.php @@ -11,6 +11,20 @@ /** * CRangeValidator validates that the attribute value is among the list (specified via {@link range}). * You may invert the validation logic with help of the {@link not} property (available since 1.1.5). + * For example, + *

      + * class QuestionForm extends CFormModel
      + * {
      + *     public function rules()
      + *     {
      + *         return array(
      + *             array('text, tag', 'required'),
      + *             array('text, 'type', 'type' => 'string'),
      + *             array('tag, 'in', 'range' => array('php', 'mysql', 'jquery')),
      + *         );
      + *     }
      + * }
      + * 
      * * @author Qiang Xue * @package system.validators diff --git a/framework/validators/CValidator.php b/framework/validators/CValidator.php index edcd7efb1b2..4393792126b 100644 --- a/framework/validators/CValidator.php +++ b/framework/validators/CValidator.php @@ -129,7 +129,7 @@ abstract protected function validateAttribute($object,$attribute); public static function createValidator($name,$object,$attributes,$params=array()) { if(is_string($attributes)) - $attributes=preg_split('/\s*,\s*/',$attributes,-1,PREG_SPLIT_NO_EMPTY); + $attributes=preg_split('/\s*,\s*/',trim($attributes, " \t\n\r\0\x0B,"),-1,PREG_SPLIT_NO_EMPTY); if(isset($params['on'])) { diff --git a/framework/vendors/TextHighlighter/Text/Highlighter.php b/framework/vendors/TextHighlighter/Text/Highlighter.php index 6a3fed96563..4823feb5096 100644 --- a/framework/vendors/TextHighlighter/Text/Highlighter.php +++ b/framework/vendors/TextHighlighter/Text/Highlighter.php @@ -1,397 +1,397 @@ - - * @copyright 2004-2006 Andrey Demenev - * @license http://www.php.net/license/3_0.txt PHP License - * @version CVS: $Id: Highlighter.php,v 1.1 2007/06/03 02:35:28 ssttoo Exp $ - * @link http://pear.php.net/package/Text_Highlighter - */ - -// {{{ BC constants - -// BC trick : define constants related to default -// renderer if needed -if (!defined('HL_NUMBERS_LI')) { - /**#@+ - * Constant for use with $options['numbers'] - * @see Text_Highlighter_Renderer_Html::_init() - */ - /** - * use numbered list - */ - define ('HL_NUMBERS_LI' , 1); - /** - * Use 2-column table with line numbers in left column and code in right column. - * Forces $options['tag'] = HL_TAG_PRE - */ - define ('HL_NUMBERS_TABLE' , 2); - /**#@-*/ -} - -// }}} -// {{{ constants -/** - * for our purpose, it is infinity - */ -define ('HL_INFINITY', 1000000000); - -// }}} - -/** - * Text highlighter base class - * - * @author Andrey Demenev - * @copyright 2004-2006 Andrey Demenev - * @license http://www.php.net/license/3_0.txt PHP License - * @version Release: 0.7.1 - * @link http://pear.php.net/package/Text_Highlighter - */ - -// {{{ Text_Highlighter - -/** - * Text highlighter base class - * - * This class implements all functions necessary for highlighting, - * but it does not contain highlighting rules. Actual highlighting is - * done using a descendent of this class. - * - * One is not supposed to manually create descendent classes. - * Instead, describe highlighting rules in XML format and - * use {@link Text_Highlighter_Generator} to create descendent class. - * Alternatively, an instance of a descendent class can be created - * directly. - * - * Use {@link Text_Highlighter::factory()} to create an - * object for particular language highlighter - * - * Usage example - * - *require_once 'Text/Highlighter.php'; - *$hlSQL =& Text_Highlighter::factory('SQL',array('numbers'=>true)); - *echo $hlSQL->highlight('SELECT * FROM table a WHERE id = 12'); - * - * - * @author Andrey Demenev - * @package Text_Highlighter - * @access public - */ - -class Text_Highlighter -{ - // {{{ members - - /** - * Syntax highlighting rules. - * Auto-generated classes set this var - * - * @access protected - * @see _init - * @var array - */ - var $_syntax; - - /** - * Renderer object. - * - * @access private - * @var array - */ - var $_renderer; - - /** - * Options. Keeped for BC - * - * @access protected - * @var array - */ - var $_options = array(); - - /** - * Conditionds - * - * @access protected - * @var array - */ - var $_conditions = array(); - - /** - * Disabled keywords - * - * @access protected - * @var array - */ - var $_disabled = array(); - - /** - * Language - * - * @access protected - * @var string - */ - var $_language = ''; - - // }}} - // {{{ _checkDefines - - /** - * Called by subclssses' constructors to enable/disable - * optional highlighter rules - * - * @param array $defines Conditional defines - * - * @access protected - */ - function _checkDefines() - { - if (isset($this->_options['defines'])) { - $defines = $this->_options['defines']; - } else { - $defines = array(); - } - foreach ($this->_conditions as $name => $actions) { - foreach($actions as $action) { - $present = in_array($name, $defines); - if (!$action[1]) { - $present = !$present; - } - if ($present) { - unset($this->_disabled[$action[0]]); - } else { - $this->_disabled[$action[0]] = true; - } - } - } - } - - // }}} - // {{{ factory - - /** - * Create a new Highlighter object for specified language - * - * @param string $lang language, for example "SQL" - * @param array $options Rendering options. This - * parameter is only keeped for BC reasons, use - * {@link Text_Highlighter::setRenderer()} instead - * - * @return mixed a newly created Highlighter object, or - * a PEAR error object on error - * - * @static - * @access public - */ - public static function factory($lang, $options = array()) - { - $lang = strtoupper($lang); - $langFile = dirname(__FILE__)."/Highlighter/$lang.php"; - if (is_file($langFile)) - include_once $langFile; - else - return false; - - $classname = 'Text_Highlighter_' . $lang; - - if (!class_exists($classname)) - return false; - - return new $classname($options); - } - - // }}} - // {{{ setRenderer - - /** - * Set renderer object - * - * @param object $renderer Text_Highlighter_Renderer - * - * @access public - */ - function setRenderer($renderer) - { - $this->_renderer = $renderer; - } - - // }}} - - /** - * Helper function to find matching brackets - * - * @access private - */ - function _matchingBrackets($str) - { - return strtr($str, '()<>[]{}', ')(><][}{'); - } - - - - - function _getToken() - { - if (!empty($this->_tokenStack)) { - return array_pop($this->_tokenStack); - } - if ($this->_pos >= $this->_len) { - return NULL; - } - - if ($this->_state != -1 && preg_match($this->_endpattern, $this->_str, $m, PREG_OFFSET_CAPTURE, $this->_pos)) { - $endpos = $m[0][1]; - $endmatch = $m[0][0]; - } else { - $endpos = -1; - } - preg_match ($this->_regs[$this->_state], $this->_str, $m, PREG_OFFSET_CAPTURE, $this->_pos); - $n = 1; - - - foreach ($this->_counts[$this->_state] as $i=>$count) { - if (!isset($m[$n])) { - break; - } - if ($m[$n][1]>-1 && ($endpos == -1 || $m[$n][1] < $endpos)) { - if ($this->_states[$this->_state][$i] != -1) { - $this->_tokenStack[] = array($this->_delim[$this->_state][$i], $m[$n][0]); - } else { - $inner = $this->_inner[$this->_state][$i]; - if (isset($this->_parts[$this->_state][$i])) { - $parts = array(); - $partpos = $m[$n][1]; - for ($j=1; $j<=$count; $j++) { - if ($m[$j+$n][1] < 0) { - continue; - } - if (isset($this->_parts[$this->_state][$i][$j])) { - if ($m[$j+$n][1] > $partpos) { - array_unshift($parts, array($inner, substr($this->_str, $partpos, $m[$j+$n][1]-$partpos))); - } - array_unshift($parts, array($this->_parts[$this->_state][$i][$j], $m[$j+$n][0])); - } - $partpos = $m[$j+$n][1] + strlen($m[$j+$n][0]); - } - if ($partpos < $m[$n][1] + strlen($m[$n][0])) { - array_unshift($parts, array($inner, substr($this->_str, $partpos, $m[$n][1] - $partpos + strlen($m[$n][0])))); - } - $this->_tokenStack = array_merge($this->_tokenStack, $parts); - } else { - foreach ($this->_keywords[$this->_state][$i] as $g => $re) { - if (isset($this->_disabled[$g])) { - continue; - } - if (preg_match($re, $m[$n][0])) { - $inner = $this->_kwmap[$g]; - break; - } - } - $this->_tokenStack[] = array($inner, $m[$n][0]); - } - } - if ($m[$n][1] > $this->_pos) { - $this->_tokenStack[] = array($this->_lastinner, substr($this->_str, $this->_pos, $m[$n][1]-$this->_pos)); - } - $this->_pos = $m[$n][1] + strlen($m[$n][0]); - if ($this->_states[$this->_state][$i] != -1) { - $this->_stack[] = array($this->_state, $this->_lastdelim, $this->_lastinner, $this->_endpattern); - $this->_lastinner = $this->_inner[$this->_state][$i]; - $this->_lastdelim = $this->_delim[$this->_state][$i]; - $l = $this->_state; - $this->_state = $this->_states[$this->_state][$i]; - $this->_endpattern = $this->_end[$this->_state]; - if ($this->_subst[$l][$i]) { - for ($k=0; $k<=$this->_counts[$l][$i]; $k++) { - if (!isset($m[$i+$k])) { - break; - } - $quoted = preg_quote($m[$n+$k][0], '/'); - $this->_endpattern = str_replace('%'.$k.'%', $quoted, $this->_endpattern); - $this->_endpattern = str_replace('%b'.$k.'%', $this->_matchingBrackets($quoted), $this->_endpattern); - } - } - } - return array_pop($this->_tokenStack); - } - $n += $count + 1; - } - - if ($endpos > -1) { - $this->_tokenStack[] = array($this->_lastdelim, $endmatch); - if ($endpos > $this->_pos) { - $this->_tokenStack[] = array($this->_lastinner, substr($this->_str, $this->_pos, $endpos-$this->_pos)); - } - list($this->_state, $this->_lastdelim, $this->_lastinner, $this->_endpattern) = array_pop($this->_stack); - $this->_pos = $endpos + strlen($endmatch); - return array_pop($this->_tokenStack); - } - $p = $this->_pos; - $this->_pos = HL_INFINITY; - return array($this->_lastinner, substr($this->_str, $p)); - } - - - - - // {{{ highlight - - /** - * Highlights code - * - * @param string $str Code to highlight - * @access public - * @return string Highlighted text - * - */ - - function highlight($str) - { - if (!($this->_renderer)) { - include_once(dirname(__FILE__).'/Renderer/Html.php'); - $this->_renderer = new Text_Highlighter_Renderer_Html($this->_options); - } - $this->_state = -1; - $this->_pos = 0; - $this->_stack = array(); - $this->_tokenStack = array(); - $this->_lastinner = $this->_defClass; - $this->_lastdelim = $this->_defClass; - $this->_endpattern = ''; - $this->_renderer->reset(); - $this->_renderer->setCurrentLanguage($this->_language); - $this->_str = $this->_renderer->preprocess($str); - $this->_len = strlen($this->_str); - while ($token = $this->_getToken()) { - $this->_renderer->acceptToken($token[0], $token[1]); - } - $this->_renderer->finalize(); - return $this->_renderer->getOutput(); - } - - // }}} - -} - -// }}} - -/* - * Local variables: - * tab-width: 4 - * c-basic-offset: 4 - * c-hanging-comment-ender-p: nil - * End: - */ - -?> + + * @copyright 2004-2006 Andrey Demenev + * @license http://www.php.net/license/3_0.txt PHP License + * @version CVS: $Id: Highlighter.php,v 1.1 2007/06/03 02:35:28 ssttoo Exp $ + * @link http://pear.php.net/package/Text_Highlighter + */ + +// {{{ BC constants + +// BC trick : define constants related to default +// renderer if needed +if (!defined('HL_NUMBERS_LI')) { + /**#@+ + * Constant for use with $options['numbers'] + * @see Text_Highlighter_Renderer_Html::_init() + */ + /** + * use numbered list + */ + define ('HL_NUMBERS_LI' , 1); + /** + * Use 2-column table with line numbers in left column and code in right column. + * Forces $options['tag'] = HL_TAG_PRE + */ + define ('HL_NUMBERS_TABLE' , 2); + /**#@-*/ +} + +// }}} +// {{{ constants +/** + * for our purpose, it is infinity + */ +define ('HL_INFINITY', 1000000000); + +// }}} + +/** + * Text highlighter base class + * + * @author Andrey Demenev + * @copyright 2004-2006 Andrey Demenev + * @license http://www.php.net/license/3_0.txt PHP License + * @version Release: 0.7.1 + * @link http://pear.php.net/package/Text_Highlighter + */ + +// {{{ Text_Highlighter + +/** + * Text highlighter base class + * + * This class implements all functions necessary for highlighting, + * but it does not contain highlighting rules. Actual highlighting is + * done using a descendent of this class. + * + * One is not supposed to manually create descendent classes. + * Instead, describe highlighting rules in XML format and + * use {@link Text_Highlighter_Generator} to create descendent class. + * Alternatively, an instance of a descendent class can be created + * directly. + * + * Use {@link Text_Highlighter::factory()} to create an + * object for particular language highlighter + * + * Usage example + * + *require_once 'Text/Highlighter.php'; + *$hlSQL =& Text_Highlighter::factory('SQL',array('numbers'=>true)); + *echo $hlSQL->highlight('SELECT * FROM table a WHERE id = 12'); + * + * + * @author Andrey Demenev + * @package Text_Highlighter + * @access public + */ + +class Text_Highlighter +{ + // {{{ members + + /** + * Syntax highlighting rules. + * Auto-generated classes set this var + * + * @access protected + * @see _init + * @var array + */ + var $_syntax; + + /** + * Renderer object. + * + * @access private + * @var array + */ + var $_renderer; + + /** + * Options. Keeped for BC + * + * @access protected + * @var array + */ + var $_options = array(); + + /** + * Conditionds + * + * @access protected + * @var array + */ + var $_conditions = array(); + + /** + * Disabled keywords + * + * @access protected + * @var array + */ + var $_disabled = array(); + + /** + * Language + * + * @access protected + * @var string + */ + var $_language = ''; + + // }}} + // {{{ _checkDefines + + /** + * Called by subclssses' constructors to enable/disable + * optional highlighter rules + * + * @param array $defines Conditional defines + * + * @access protected + */ + function _checkDefines() + { + if (isset($this->_options['defines'])) { + $defines = $this->_options['defines']; + } else { + $defines = array(); + } + foreach ($this->_conditions as $name => $actions) { + foreach($actions as $action) { + $present = in_array($name, $defines); + if (!$action[1]) { + $present = !$present; + } + if ($present) { + unset($this->_disabled[$action[0]]); + } else { + $this->_disabled[$action[0]] = true; + } + } + } + } + + // }}} + // {{{ factory + + /** + * Create a new Highlighter object for specified language + * + * @param string $lang language, for example "SQL" + * @param array $options Rendering options. This + * parameter is only keeped for BC reasons, use + * {@link Text_Highlighter::setRenderer()} instead + * + * @return mixed a newly created Highlighter object, or + * a PEAR error object on error + * + * @static + * @access public + */ + public static function factory($lang, $options = array()) + { + $lang = strtoupper($lang); + $langFile = dirname(__FILE__)."/Highlighter/$lang.php"; + if (is_file($langFile)) + include_once $langFile; + else + return false; + + $classname = 'Text_Highlighter_' . $lang; + + if (!class_exists($classname)) + return false; + + return new $classname($options); + } + + // }}} + // {{{ setRenderer + + /** + * Set renderer object + * + * @param object $renderer Text_Highlighter_Renderer + * + * @access public + */ + function setRenderer($renderer) + { + $this->_renderer = $renderer; + } + + // }}} + + /** + * Helper function to find matching brackets + * + * @access private + */ + function _matchingBrackets($str) + { + return strtr($str, '()<>[]{}', ')(><][}{'); + } + + + + + function _getToken() + { + if (!empty($this->_tokenStack)) { + return array_pop($this->_tokenStack); + } + if ($this->_pos >= $this->_len) { + return NULL; + } + + if ($this->_state != -1 && preg_match($this->_endpattern, $this->_str, $m, PREG_OFFSET_CAPTURE, $this->_pos)) { + $endpos = $m[0][1]; + $endmatch = $m[0][0]; + } else { + $endpos = -1; + } + preg_match ($this->_regs[$this->_state], $this->_str, $m, PREG_OFFSET_CAPTURE, $this->_pos); + $n = 1; + + + foreach ($this->_counts[$this->_state] as $i=>$count) { + if (!isset($m[$n])) { + break; + } + if ($m[$n][1]>-1 && ($endpos == -1 || $m[$n][1] < $endpos)) { + if ($this->_states[$this->_state][$i] != -1) { + $this->_tokenStack[] = array($this->_delim[$this->_state][$i], $m[$n][0]); + } else { + $inner = $this->_inner[$this->_state][$i]; + if (isset($this->_parts[$this->_state][$i])) { + $parts = array(); + $partpos = $m[$n][1]; + for ($j=1; $j<=$count; $j++) { + if ($m[$j+$n][1] < 0) { + continue; + } + if (isset($this->_parts[$this->_state][$i][$j])) { + if ($m[$j+$n][1] > $partpos) { + array_unshift($parts, array($inner, substr($this->_str, $partpos, $m[$j+$n][1]-$partpos))); + } + array_unshift($parts, array($this->_parts[$this->_state][$i][$j], $m[$j+$n][0])); + } + $partpos = $m[$j+$n][1] + strlen($m[$j+$n][0]); + } + if ($partpos < $m[$n][1] + strlen($m[$n][0])) { + array_unshift($parts, array($inner, substr($this->_str, $partpos, $m[$n][1] - $partpos + strlen($m[$n][0])))); + } + $this->_tokenStack = array_merge($this->_tokenStack, $parts); + } else { + foreach ($this->_keywords[$this->_state][$i] as $g => $re) { + if (isset($this->_disabled[$g])) { + continue; + } + if (preg_match($re, $m[$n][0])) { + $inner = $this->_kwmap[$g]; + break; + } + } + $this->_tokenStack[] = array($inner, $m[$n][0]); + } + } + if ($m[$n][1] > $this->_pos) { + $this->_tokenStack[] = array($this->_lastinner, substr($this->_str, $this->_pos, $m[$n][1]-$this->_pos)); + } + $this->_pos = $m[$n][1] + strlen($m[$n][0]); + if ($this->_states[$this->_state][$i] != -1) { + $this->_stack[] = array($this->_state, $this->_lastdelim, $this->_lastinner, $this->_endpattern); + $this->_lastinner = $this->_inner[$this->_state][$i]; + $this->_lastdelim = $this->_delim[$this->_state][$i]; + $l = $this->_state; + $this->_state = $this->_states[$this->_state][$i]; + $this->_endpattern = $this->_end[$this->_state]; + if ($this->_subst[$l][$i]) { + for ($k=0; $k<=$this->_counts[$l][$i]; $k++) { + if (!isset($m[$i+$k])) { + break; + } + $quoted = preg_quote($m[$n+$k][0], '/'); + $this->_endpattern = str_replace('%'.$k.'%', $quoted, $this->_endpattern); + $this->_endpattern = str_replace('%b'.$k.'%', $this->_matchingBrackets($quoted), $this->_endpattern); + } + } + } + return array_pop($this->_tokenStack); + } + $n += $count + 1; + } + + if ($endpos > -1) { + $this->_tokenStack[] = array($this->_lastdelim, $endmatch); + if ($endpos > $this->_pos) { + $this->_tokenStack[] = array($this->_lastinner, substr($this->_str, $this->_pos, $endpos-$this->_pos)); + } + list($this->_state, $this->_lastdelim, $this->_lastinner, $this->_endpattern) = array_pop($this->_stack); + $this->_pos = $endpos + strlen($endmatch); + return array_pop($this->_tokenStack); + } + $p = $this->_pos; + $this->_pos = HL_INFINITY; + return array($this->_lastinner, substr($this->_str, $p)); + } + + + + + // {{{ highlight + + /** + * Highlights code + * + * @param string $str Code to highlight + * @access public + * @return string Highlighted text + * + */ + + function highlight($str) + { + if (!($this->_renderer)) { + include_once(dirname(__FILE__).'/Renderer/Html.php'); + $this->_renderer = new Text_Highlighter_Renderer_Html($this->_options); + } + $this->_state = -1; + $this->_pos = 0; + $this->_stack = array(); + $this->_tokenStack = array(); + $this->_lastinner = $this->_defClass; + $this->_lastdelim = $this->_defClass; + $this->_endpattern = ''; + $this->_renderer->reset(); + $this->_renderer->setCurrentLanguage($this->_language); + $this->_str = $this->_renderer->preprocess($str); + $this->_len = strlen($this->_str); + while ($token = $this->_getToken()) { + $this->_renderer->acceptToken($token[0], $token[1]); + } + $this->_renderer->finalize(); + return $this->_renderer->getOutput(); + } + + // }}} + +} + +// }}} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * c-hanging-comment-ender-p: nil + * End: + */ + +?> diff --git a/framework/vendors/TextHighlighter/Text/Highlighter/ABAP.php b/framework/vendors/TextHighlighter/Text/Highlighter/ABAP.php index 38214b425ff..187daff1576 100644 --- a/framework/vendors/TextHighlighter/Text/Highlighter/ABAP.php +++ b/framework/vendors/TextHighlighter/Text/Highlighter/ABAP.php @@ -1,53 +1,53 @@ - - * - */ - -/** - * @ignore - */ - -/** - * Auto-generated class. ABAP syntax highlighting - * + * + */ + +/** + * @ignore + */ + +/** + * Auto-generated class. ABAP syntax highlighting + * * @author Stoyan Stefanov - * @category Text - * @package Text_Highlighter - * @copyright 2004-2006 Andrey Demenev - * @license http://www.php.net/license/3_0.txt PHP License - * @version Release: 0.7.1 - * @link http://pear.php.net/package/Text_Highlighter - */ -class Text_Highlighter_ABAP extends Text_Highlighter -{ + * @category Text + * @package Text_Highlighter + * @copyright 2004-2006 Andrey Demenev + * @license http://www.php.net/license/3_0.txt PHP License + * @version Release: 0.7.1 + * @link http://pear.php.net/package/Text_Highlighter + */ +class Text_Highlighter_ABAP extends Text_Highlighter +{ var $_language = 'abap'; - /** - * Constructor - * - * @param array $options - * @access public - */ - function __construct($options=array()) - { - + /** + * Constructor + * + * @param array $options + * @access public + */ + function __construct($options=array()) + { + $this->_options = $options; $this->_regs = array ( -1 => '/((?i)\\{)|((?i)\\()|((?i)\\[)|((?i)^\\*|")|((?i)\')|((?i)[a-z_\\-]\\w*)/', @@ -498,8 +498,8 @@ function __construct($options=array()) 'reserved' => 'reserved', 'constants' => 'reserved', ); - $this->_defClass = 'code'; - $this->_checkDefines(); - } - + $this->_defClass = 'code'; + $this->_checkDefines(); + } + } \ No newline at end of file diff --git a/framework/vendors/TextHighlighter/Text/Highlighter/CPP.php b/framework/vendors/TextHighlighter/Text/Highlighter/CPP.php index ad13437fa57..eccaa084e78 100644 --- a/framework/vendors/TextHighlighter/Text/Highlighter/CPP.php +++ b/framework/vendors/TextHighlighter/Text/Highlighter/CPP.php @@ -1,56 +1,56 @@ - - * - */ - -/** - * Auto-generated class. CPP syntax highlighting - * + * + */ + +/** + * Auto-generated class. CPP syntax highlighting + * * @author Aaron Kalin * @author Andrey Demenev - * @category Text - * @package Text_Highlighter - * @copyright 2004-2006 Andrey Demenev - * @license http://www.php.net/license/3_0.txt PHP License - * @version Release: 0.7.1 - * @link http://pear.php.net/package/Text_Highlighter - */ -class Text_Highlighter_CPP extends Text_Highlighter -{ + * @category Text + * @package Text_Highlighter + * @copyright 2004-2006 Andrey Demenev + * @license http://www.php.net/license/3_0.txt PHP License + * @version Release: 0.7.1 + * @link http://pear.php.net/package/Text_Highlighter + */ +class Text_Highlighter_CPP extends Text_Highlighter +{ var $_language = 'cpp'; - /** - * Constructor - * - * @param array $options - * @access public - */ - function __construct($options=array()) - { - + /** + * Constructor + * + * @param array $options + * @access public + */ + function __construct($options=array()) + { + $this->_options = $options; $this->_regs = array ( -1 => '/((?i)")|((?i)\\{)|((?i)\\()|((?i)\\[)|((?i)[a-z_]\\w*)|((?mi)^[ \\t]*#include)|((?mii)^[ \\t]*#[ \\t]*[a-z]+)|((?i)\\d*\\.?\\d+)|((?i)\\/\\*)|((?i)\\/\\/.+)/', @@ -833,8 +833,8 @@ function __construct($options=array()) 'types' => 'types', 'Common Macros' => 'prepro', ); - $this->_defClass = 'code'; - $this->_checkDefines(); - } - + $this->_defClass = 'code'; + $this->_checkDefines(); + } + } \ No newline at end of file diff --git a/framework/vendors/TextHighlighter/Text/Highlighter/CSS.php b/framework/vendors/TextHighlighter/Text/Highlighter/CSS.php index 86a69accaad..bf04a2fb060 100644 --- a/framework/vendors/TextHighlighter/Text/Highlighter/CSS.php +++ b/framework/vendors/TextHighlighter/Text/Highlighter/CSS.php @@ -1,419 +1,419 @@ - - * - */ - -/** - * Auto-generated class. CSS syntax highlighting - * - * @author Andrey Demenev - * @category Text - * @package Text_Highlighter - * @copyright 2004-2006 Andrey Demenev - * @license http://www.php.net/license/3_0.txt PHP License - * @version Release: 0.7.1 - * @link http://pear.php.net/package/Text_Highlighter - */ -class Text_Highlighter_CSS extends Text_Highlighter -{ - var $_language = 'css'; - - /** - * Constructor - * - * @param array $options - * @access public - */ - function __construct($options=array()) - { - - $this->_options = $options; - $this->_regs = array ( - -1 => '/((?i)\\/\\*)|((?i)(@[a-z\\d]+))|((?i)(((\\.|#)?[a-z]+[a-z\\d\\-]*(?![a-z\\d\\-]))|(\\*))(?!\\s*:\\s*[\\s\\{]))|((?i):[a-z][a-z\\d\\-]*)|((?i)\\[)|((?i)\\{)/', - 0 => '//', - 1 => '/((?i)\\d*\\.?\\d+(\\%|em|ex|pc|pt|px|in|mm|cm))|((?i)\\d*\\.?\\d+)|((?i)[a-z][a-z\\d\\-]*)|((?i)#([\\da-f]{6}|[\\da-f]{3})\\b)/', - 2 => '/((?i)\')|((?i)")|((?i)[\\w\\-\\:]+)/', - 3 => '/((?i)\\/\\*)|((?i)[a-z][a-z\\d\\-]*\\s*:)|((?i)(((\\.|#)?[a-z]+[a-z\\d\\-]*(?![a-z\\d\\-]))|(\\*))(?!\\s*:\\s*[\\s\\{]))|((?i)\\{)/', - 4 => '/((?i)\\\\[\\\\(\\\\)\\\\])/', - 5 => '/((?i)\\\\\\\\|\\\\"|\\\\\'|\\\\`)/', - 6 => '/((?i)\\\\\\\\|\\\\"|\\\\\'|\\\\`|\\\\t|\\\\n|\\\\r)/', - ); - $this->_counts = array ( - -1 => - array ( - 0 => 0, - 1 => 1, - 2 => 4, - 3 => 0, - 4 => 0, - 5 => 0, - ), - 0 => - array ( - ), - 1 => - array ( - 0 => 1, - 1 => 0, - 2 => 0, - 3 => 1, - ), - 2 => - array ( - 0 => 0, - 1 => 0, - 2 => 0, - ), - 3 => - array ( - 0 => 0, - 1 => 0, - 2 => 4, - 3 => 0, - ), - 4 => - array ( - 0 => 0, - ), - 5 => - array ( - 0 => 0, - ), - 6 => - array ( - 0 => 0, - ), - ); - $this->_delim = array ( - -1 => - array ( - 0 => 'comment', - 1 => '', - 2 => '', - 3 => '', - 4 => 'brackets', - 5 => 'brackets', - ), - 0 => - array ( - ), - 1 => - array ( - 0 => '', - 1 => '', - 2 => '', - 3 => '', - ), - 2 => - array ( - 0 => 'quotes', - 1 => 'quotes', - 2 => '', - ), - 3 => - array ( - 0 => 'comment', - 1 => 'reserved', - 2 => '', - 3 => 'brackets', - ), - 4 => - array ( - 0 => '', - ), - 5 => - array ( - 0 => '', - ), - 6 => - array ( - 0 => '', - ), - ); - $this->_inner = array ( - -1 => - array ( - 0 => 'comment', - 1 => 'var', - 2 => 'identifier', - 3 => 'special', - 4 => 'code', - 5 => 'code', - ), - 0 => - array ( - ), - 1 => - array ( - 0 => 'number', - 1 => 'number', - 2 => 'code', - 3 => 'var', - ), - 2 => - array ( - 0 => 'string', - 1 => 'string', - 2 => 'var', - ), - 3 => - array ( - 0 => 'comment', - 1 => 'code', - 2 => 'identifier', - 3 => 'code', - ), - 4 => - array ( - 0 => 'string', - ), - 5 => - array ( - 0 => 'special', - ), - 6 => - array ( - 0 => 'special', - ), - ); - $this->_end = array ( - 0 => '/(?i)\\*\\//', - 1 => '/(?i)(?=;|\\})/', - 2 => '/(?i)\\]/', - 3 => '/(?i)\\}/', - 4 => '/(?i)\\)/', - 5 => '/(?i)\'/', - 6 => '/(?i)"/', - ); - $this->_states = array ( - -1 => - array ( - 0 => 0, - 1 => -1, - 2 => -1, - 3 => -1, - 4 => 2, - 5 => 3, - ), - 0 => - array ( - ), - 1 => - array ( - 0 => -1, - 1 => -1, - 2 => -1, - 3 => -1, - ), - 2 => - array ( - 0 => 5, - 1 => 6, - 2 => -1, - ), - 3 => - array ( - 0 => 0, - 1 => 1, - 2 => -1, - 3 => 3, - ), - 4 => - array ( - 0 => -1, - ), - 5 => - array ( - 0 => -1, - ), - 6 => - array ( - 0 => -1, - ), - ); - $this->_keywords = array ( - -1 => - array ( - 0 => -1, - 1 => - array ( - ), - 2 => - array ( - ), - 3 => - array ( - ), - 4 => -1, - 5 => -1, - ), - 0 => - array ( - ), - 1 => - array ( - 0 => - array ( - ), - 1 => - array ( - ), - 2 => - array ( - 'propertyValue' => '/^((?i)far-left|left|center-left|center-right|center|far-right|right-side|right|behind|leftwards|rightwards|inherit|scroll|fixed|transparent|none|repeat-x|repeat-y|repeat|no-repeat|collapse|separate|auto|top|bottom|both|open-quote|close-quote|no-open-quote|no-close-quote|crosshair|default|pointer|move|e-resize|ne-resize|nw-resize|n-resize|se-resize|sw-resize|s-resize|text|wait|help|ltr|rtl|inline|block|list-item|run-in|compact|marker|table|inline-table|table-row-group|table-header-group|table-footer-group|table-row|table-column-group|table-column|table-cell|table-caption|below|level|above|higher|lower|show|hide|caption|icon|menu|message-box|small-caption|status-bar|normal|wider|narrower|ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded|italic|oblique|small-caps|bold|bolder|lighter|inside|outside|disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-greek|lower-alpha|lower-latin|upper-alpha|upper-latin|hebrew|armenian|georgian|cjk-ideographic|hiragana|katakana|hiragana-iroha|katakana-iroha|crop|cross|invert|visible|hidden|always|avoid|x-low|low|medium|high|x-high|mix?|repeat?|static|relative|absolute|portrait|landscape|spell-out|once|digits|continuous|code|x-slow|slow|fast|x-fast|faster|slower|justify|underline|overline|line-through|blink|capitalize|uppercase|lowercase|embed|bidi-override|baseline|sub|super|text-top|middle|text-bottom|silent|x-soft|soft|loud|x-loud|pre|nowrap|serif|sans-serif|cursive|fantasy|monospace|empty|string|strict|loose|char|true|false|dotted|dashed|solid|double|groove|ridge|inset|outset|larger|smaller|xx-small|x-small|small|large|x-large|xx-large|all|newspaper|distribute|distribute-all-lines|distribute-center-last|inter-word|inter-ideograph|inter-cluster|kashida|ideograph-alpha|ideograph-numeric|ideograph-parenthesis|ideograph-space|keep-all|break-all|break-word|lr-tb|tb-rl|thin|thick|inline-block|w-resize|hand|distribute-letter|distribute-space|whitespace|male|female|child)$/', - 'namedcolor' => '/^((?i)aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|purple|red|silver|teal|white|yellow|activeborder|activecaption|appworkspace|background|buttonface|buttonhighlight|buttonshadow|buttontext|captiontext|graytext|highlight|highlighttext|inactiveborder|inactivecaption|inactivecaptiontext|infobackground|infotext|menu|menutext|scrollbar|threeddarkshadow|threedface|threedhighlight|threedlightshadow|threedshadow|window|windowframe|windowtext)$/', - ), - 3 => - array ( - ), - ), - 2 => - array ( - 0 => -1, - 1 => -1, - 2 => - array ( - ), - ), - 3 => - array ( - 0 => -1, - 1 => -1, - 2 => - array ( - ), - 3 => -1, - ), - 4 => - array ( - 0 => - array ( - ), - ), - 5 => - array ( - 0 => - array ( - ), - ), - 6 => - array ( - 0 => - array ( - ), - ), - ); - $this->_parts = array ( - 0 => - array ( - ), - 1 => - array ( - 0 => - array ( - 1 => 'string', - ), - 1 => NULL, - 2 => NULL, - 3 => NULL, - ), - 2 => - array ( - 0 => NULL, - 1 => NULL, - 2 => NULL, - ), - 3 => - array ( - 0 => NULL, - 1 => NULL, - 2 => NULL, - 3 => NULL, - ), - 4 => - array ( - 0 => NULL, - ), - 5 => - array ( - 0 => NULL, - ), - 6 => - array ( - 0 => NULL, - ), - ); - $this->_subst = array ( - -1 => - array ( - 0 => false, - 1 => false, - 2 => false, - 3 => false, - 4 => false, - 5 => false, - ), - 0 => - array ( - ), - 1 => - array ( - 0 => false, - 1 => false, - 2 => false, - 3 => false, - ), - 2 => - array ( - 0 => false, - 1 => false, - 2 => false, - ), - 3 => - array ( - 0 => false, - 1 => false, - 2 => false, - 3 => false, - ), - 4 => - array ( - 0 => false, - ), - 5 => - array ( - 0 => false, - ), - 6 => - array ( - 0 => false, - ), - ); - $this->_conditions = array ( - ); - $this->_kwmap = array ( - 'propertyValue' => 'string', - 'namedcolor' => 'var', - ); - $this->_defClass = 'code'; - $this->_checkDefines(); - } - + + * + */ + +/** + * Auto-generated class. CSS syntax highlighting + * + * @author Andrey Demenev + * @category Text + * @package Text_Highlighter + * @copyright 2004-2006 Andrey Demenev + * @license http://www.php.net/license/3_0.txt PHP License + * @version Release: 0.7.1 + * @link http://pear.php.net/package/Text_Highlighter + */ +class Text_Highlighter_CSS extends Text_Highlighter +{ + var $_language = 'css'; + + /** + * Constructor + * + * @param array $options + * @access public + */ + function __construct($options=array()) + { + + $this->_options = $options; + $this->_regs = array ( + -1 => '/((?i)\\/\\*)|((?i)(@[a-z\\d]+))|((?i)(((\\.|#)?[a-z]+[a-z\\d\\-]*(?![a-z\\d\\-]))|(\\*))(?!\\s*:\\s*[\\s\\{]))|((?i):[a-z][a-z\\d\\-]*)|((?i)\\[)|((?i)\\{)/', + 0 => '//', + 1 => '/((?i)\\d*\\.?\\d+(\\%|em|ex|pc|pt|px|in|mm|cm))|((?i)\\d*\\.?\\d+)|((?i)[a-z][a-z\\d\\-]*)|((?i)#([\\da-f]{6}|[\\da-f]{3})\\b)/', + 2 => '/((?i)\')|((?i)")|((?i)[\\w\\-\\:]+)/', + 3 => '/((?i)\\/\\*)|((?i)[a-z][a-z\\d\\-]*\\s*:)|((?i)(((\\.|#)?[a-z]+[a-z\\d\\-]*(?![a-z\\d\\-]))|(\\*))(?!\\s*:\\s*[\\s\\{]))|((?i)\\{)/', + 4 => '/((?i)\\\\[\\\\(\\\\)\\\\])/', + 5 => '/((?i)\\\\\\\\|\\\\"|\\\\\'|\\\\`)/', + 6 => '/((?i)\\\\\\\\|\\\\"|\\\\\'|\\\\`|\\\\t|\\\\n|\\\\r)/', + ); + $this->_counts = array ( + -1 => + array ( + 0 => 0, + 1 => 1, + 2 => 4, + 3 => 0, + 4 => 0, + 5 => 0, + ), + 0 => + array ( + ), + 1 => + array ( + 0 => 1, + 1 => 0, + 2 => 0, + 3 => 1, + ), + 2 => + array ( + 0 => 0, + 1 => 0, + 2 => 0, + ), + 3 => + array ( + 0 => 0, + 1 => 0, + 2 => 4, + 3 => 0, + ), + 4 => + array ( + 0 => 0, + ), + 5 => + array ( + 0 => 0, + ), + 6 => + array ( + 0 => 0, + ), + ); + $this->_delim = array ( + -1 => + array ( + 0 => 'comment', + 1 => '', + 2 => '', + 3 => '', + 4 => 'brackets', + 5 => 'brackets', + ), + 0 => + array ( + ), + 1 => + array ( + 0 => '', + 1 => '', + 2 => '', + 3 => '', + ), + 2 => + array ( + 0 => 'quotes', + 1 => 'quotes', + 2 => '', + ), + 3 => + array ( + 0 => 'comment', + 1 => 'reserved', + 2 => '', + 3 => 'brackets', + ), + 4 => + array ( + 0 => '', + ), + 5 => + array ( + 0 => '', + ), + 6 => + array ( + 0 => '', + ), + ); + $this->_inner = array ( + -1 => + array ( + 0 => 'comment', + 1 => 'var', + 2 => 'identifier', + 3 => 'special', + 4 => 'code', + 5 => 'code', + ), + 0 => + array ( + ), + 1 => + array ( + 0 => 'number', + 1 => 'number', + 2 => 'code', + 3 => 'var', + ), + 2 => + array ( + 0 => 'string', + 1 => 'string', + 2 => 'var', + ), + 3 => + array ( + 0 => 'comment', + 1 => 'code', + 2 => 'identifier', + 3 => 'code', + ), + 4 => + array ( + 0 => 'string', + ), + 5 => + array ( + 0 => 'special', + ), + 6 => + array ( + 0 => 'special', + ), + ); + $this->_end = array ( + 0 => '/(?i)\\*\\//', + 1 => '/(?i)(?=;|\\})/', + 2 => '/(?i)\\]/', + 3 => '/(?i)\\}/', + 4 => '/(?i)\\)/', + 5 => '/(?i)\'/', + 6 => '/(?i)"/', + ); + $this->_states = array ( + -1 => + array ( + 0 => 0, + 1 => -1, + 2 => -1, + 3 => -1, + 4 => 2, + 5 => 3, + ), + 0 => + array ( + ), + 1 => + array ( + 0 => -1, + 1 => -1, + 2 => -1, + 3 => -1, + ), + 2 => + array ( + 0 => 5, + 1 => 6, + 2 => -1, + ), + 3 => + array ( + 0 => 0, + 1 => 1, + 2 => -1, + 3 => 3, + ), + 4 => + array ( + 0 => -1, + ), + 5 => + array ( + 0 => -1, + ), + 6 => + array ( + 0 => -1, + ), + ); + $this->_keywords = array ( + -1 => + array ( + 0 => -1, + 1 => + array ( + ), + 2 => + array ( + ), + 3 => + array ( + ), + 4 => -1, + 5 => -1, + ), + 0 => + array ( + ), + 1 => + array ( + 0 => + array ( + ), + 1 => + array ( + ), + 2 => + array ( + 'propertyValue' => '/^((?i)far-left|left|center-left|center-right|center|far-right|right-side|right|behind|leftwards|rightwards|inherit|scroll|fixed|transparent|none|repeat-x|repeat-y|repeat|no-repeat|collapse|separate|auto|top|bottom|both|open-quote|close-quote|no-open-quote|no-close-quote|crosshair|default|pointer|move|e-resize|ne-resize|nw-resize|n-resize|se-resize|sw-resize|s-resize|text|wait|help|ltr|rtl|inline|block|list-item|run-in|compact|marker|table|inline-table|table-row-group|table-header-group|table-footer-group|table-row|table-column-group|table-column|table-cell|table-caption|below|level|above|higher|lower|show|hide|caption|icon|menu|message-box|small-caption|status-bar|normal|wider|narrower|ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded|italic|oblique|small-caps|bold|bolder|lighter|inside|outside|disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-greek|lower-alpha|lower-latin|upper-alpha|upper-latin|hebrew|armenian|georgian|cjk-ideographic|hiragana|katakana|hiragana-iroha|katakana-iroha|crop|cross|invert|visible|hidden|always|avoid|x-low|low|medium|high|x-high|mix?|repeat?|static|relative|absolute|portrait|landscape|spell-out|once|digits|continuous|code|x-slow|slow|fast|x-fast|faster|slower|justify|underline|overline|line-through|blink|capitalize|uppercase|lowercase|embed|bidi-override|baseline|sub|super|text-top|middle|text-bottom|silent|x-soft|soft|loud|x-loud|pre|nowrap|serif|sans-serif|cursive|fantasy|monospace|empty|string|strict|loose|char|true|false|dotted|dashed|solid|double|groove|ridge|inset|outset|larger|smaller|xx-small|x-small|small|large|x-large|xx-large|all|newspaper|distribute|distribute-all-lines|distribute-center-last|inter-word|inter-ideograph|inter-cluster|kashida|ideograph-alpha|ideograph-numeric|ideograph-parenthesis|ideograph-space|keep-all|break-all|break-word|lr-tb|tb-rl|thin|thick|inline-block|w-resize|hand|distribute-letter|distribute-space|whitespace|male|female|child)$/', + 'namedcolor' => '/^((?i)aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|purple|red|silver|teal|white|yellow|activeborder|activecaption|appworkspace|background|buttonface|buttonhighlight|buttonshadow|buttontext|captiontext|graytext|highlight|highlighttext|inactiveborder|inactivecaption|inactivecaptiontext|infobackground|infotext|menu|menutext|scrollbar|threeddarkshadow|threedface|threedhighlight|threedlightshadow|threedshadow|window|windowframe|windowtext)$/', + ), + 3 => + array ( + ), + ), + 2 => + array ( + 0 => -1, + 1 => -1, + 2 => + array ( + ), + ), + 3 => + array ( + 0 => -1, + 1 => -1, + 2 => + array ( + ), + 3 => -1, + ), + 4 => + array ( + 0 => + array ( + ), + ), + 5 => + array ( + 0 => + array ( + ), + ), + 6 => + array ( + 0 => + array ( + ), + ), + ); + $this->_parts = array ( + 0 => + array ( + ), + 1 => + array ( + 0 => + array ( + 1 => 'string', + ), + 1 => NULL, + 2 => NULL, + 3 => NULL, + ), + 2 => + array ( + 0 => NULL, + 1 => NULL, + 2 => NULL, + ), + 3 => + array ( + 0 => NULL, + 1 => NULL, + 2 => NULL, + 3 => NULL, + ), + 4 => + array ( + 0 => NULL, + ), + 5 => + array ( + 0 => NULL, + ), + 6 => + array ( + 0 => NULL, + ), + ); + $this->_subst = array ( + -1 => + array ( + 0 => false, + 1 => false, + 2 => false, + 3 => false, + 4 => false, + 5 => false, + ), + 0 => + array ( + ), + 1 => + array ( + 0 => false, + 1 => false, + 2 => false, + 3 => false, + ), + 2 => + array ( + 0 => false, + 1 => false, + 2 => false, + ), + 3 => + array ( + 0 => false, + 1 => false, + 2 => false, + 3 => false, + ), + 4 => + array ( + 0 => false, + ), + 5 => + array ( + 0 => false, + ), + 6 => + array ( + 0 => false, + ), + ); + $this->_conditions = array ( + ); + $this->_kwmap = array ( + 'propertyValue' => 'string', + 'namedcolor' => 'var', + ); + $this->_defClass = 'code'; + $this->_checkDefines(); + } + } \ No newline at end of file diff --git a/framework/vendors/TextHighlighter/Text/Highlighter/DIFF.php b/framework/vendors/TextHighlighter/Text/Highlighter/DIFF.php index 1463e6abb04..d084974b1d4 100644 --- a/framework/vendors/TextHighlighter/Text/Highlighter/DIFF.php +++ b/framework/vendors/TextHighlighter/Text/Highlighter/DIFF.php @@ -1,49 +1,49 @@ - - * - */ - -/** - * Auto-generated class. DIFF syntax highlighting - * + * + */ + +/** + * Auto-generated class. DIFF syntax highlighting + * * @author Andrey Demenev - * @category Text - * @package Text_Highlighter - * @copyright 2004-2006 Andrey Demenev - * @license http://www.php.net/license/3_0.txt PHP License - * @version Release: 0.7.1 - * @link http://pear.php.net/package/Text_Highlighter - */ -class Text_Highlighter_DIFF extends Text_Highlighter -{ + * @category Text + * @package Text_Highlighter + * @copyright 2004-2006 Andrey Demenev + * @license http://www.php.net/license/3_0.txt PHP License + * @version Release: 0.7.1 + * @link http://pear.php.net/package/Text_Highlighter + */ +class Text_Highlighter_DIFF extends Text_Highlighter +{ var $_language = 'diff'; - /** - * Constructor - * - * @param array $options - * @access public - */ - function __construct($options=array()) - { - + /** + * Constructor + * + * @param array $options + * @access public + */ + function __construct($options=array()) + { + $this->_options = $options; $this->_regs = array ( -1 => '/((?m)^\\\\\\sNo\\snewline.+$)|((?m)^\\-\\-\\-$)|((?m)^(diff\\s+\\-|Only\\s+|Index).*$)|((?m)^(\\-\\-\\-|\\+\\+\\+)\\s.+$)|((?m)^\\*.*$)|((?m)^\\+.*$)|((?m)^!.*$)|((?m)^\\<\\s.*$)|((?m)^\\>\\s.*$)|((?m)^\\d+(\\,\\d+)?[acd]\\d+(,\\d+)?$)|((?m)^\\-.*$)|((?m)^\\+.*$)|((?m)^@@.+@@$)|((?m)^d\\d+\\s\\d+$)|((?m)^a\\d+\\s\\d+$)|((?m)^(\\d+)(,\\d+)?(a)$)|((?m)^(\\d+)(,\\d+)?(c)$)|((?m)^(\\d+)(,\\d+)?(d)$)|((?m)^a(\\d+)(\\s\\d+)?$)|((?m)^c(\\d+)(\\s\\d+)?$)|((?m)^d(\\d+)(\\s\\d+)?$)/', @@ -359,8 +359,8 @@ function __construct($options=array()) ); $this->_kwmap = array ( ); - $this->_defClass = 'default'; - $this->_checkDefines(); - } - + $this->_defClass = 'default'; + $this->_checkDefines(); + } + } \ No newline at end of file diff --git a/framework/vendors/TextHighlighter/Text/Highlighter/DTD.php b/framework/vendors/TextHighlighter/Text/Highlighter/DTD.php index 51c7617b313..22eaa9c36d9 100644 --- a/framework/vendors/TextHighlighter/Text/Highlighter/DTD.php +++ b/framework/vendors/TextHighlighter/Text/Highlighter/DTD.php @@ -1,49 +1,49 @@ - - * - */ - -/** - * Auto-generated class. DTD syntax highlighting - * + * + */ + +/** + * Auto-generated class. DTD syntax highlighting + * * @author Andrey Demenev - * @category Text - * @package Text_Highlighter - * @copyright 2004-2006 Andrey Demenev - * @license http://www.php.net/license/3_0.txt PHP License - * @version Release: 0.7.1 - * @link http://pear.php.net/package/Text_Highlighter - */ -class Text_Highlighter_DTD extends Text_Highlighter -{ + * @category Text + * @package Text_Highlighter + * @copyright 2004-2006 Andrey Demenev + * @license http://www.php.net/license/3_0.txt PHP License + * @version Release: 0.7.1 + * @link http://pear.php.net/package/Text_Highlighter + */ +class Text_Highlighter_DTD extends Text_Highlighter +{ var $_language = 'dtd'; - /** - * Constructor - * - * @param array $options - * @access public - */ - function __construct($options=array()) - { - + /** + * Constructor + * + * @param array $options + * @access public + */ + function __construct($options=array()) + { + $this->_options = $options; $this->_regs = array ( -1 => '/(\\'; - } else { - return ''; - - } - } - - /** - * Special case processor for the contents of script tags - * @param HTMLPurifier_Token $token HTMLPurifier_Token object. - * @return string - * @warning This runs into problems if there's already a literal - * --> somewhere inside the script contents. - */ - public function generateScriptFromToken($token) - { - if (!$token instanceof HTMLPurifier_Token_Text) { - return $this->generateFromToken($token); - } - // Thanks - $data = preg_replace('#//\s*$#', '', $token->data); - return ''; - } - - /** - * Generates attribute declarations from attribute array. - * @note This does not include the leading or trailing space. - * @param array $assoc_array_of_attributes Attribute array - * @param string $element Name of element attributes are for, used to check - * attribute minimization. - * @return string Generated HTML fragment for insertion. - */ - public function generateAttributes($assoc_array_of_attributes, $element = '') - { - $html = ''; - if ($this->_sortAttr) { - ksort($assoc_array_of_attributes); - } - foreach ($assoc_array_of_attributes as $key => $value) { - if (!$this->_xhtml) { - // Remove namespaced attributes - if (strpos($key, ':') !== false) { - continue; - } - // Check if we should minimize the attribute: val="val" -> val - if ($element && !empty($this->_def->info[$element]->attr[$key]->minimized)) { - $html .= $key . ' '; - continue; - } - } - // Workaround for Internet Explorer innerHTML bug. - // Essentially, Internet Explorer, when calculating - // innerHTML, omits quotes if there are no instances of - // angled brackets, quotes or spaces. However, when parsing - // HTML (for example, when you assign to innerHTML), it - // treats backticks as quotes. Thus, - // `` - // becomes - // `` - // becomes - // - // Fortunately, all we need to do is trigger an appropriate - // quoting style, which we do by adding an extra space. - // This also is consistent with the W3C spec, which states - // that user agents may ignore leading or trailing - // whitespace (in fact, most don't, at least for attributes - // like alt, but an extra space at the end is barely - // noticeable). Still, we have a configuration knob for - // this, since this transformation is not necesary if you - // don't process user input with innerHTML or you don't plan - // on supporting Internet Explorer. - if ($this->_innerHTMLFix) { - if (strpos($value, '`') !== false) { - // check if correct quoting style would not already be - // triggered - if (strcspn($value, '"\' <>') === strlen($value)) { - // protect! - $value .= ' '; - } - } - } - $html .= $key.'="'.$this->escape($value).'" '; - } - return rtrim($html); - } - - /** - * Escapes raw text data. - * @todo This really ought to be protected, but until we have a facility - * for properly generating HTML here w/o using tokens, it stays - * public. - * @param string $string String data to escape for HTML. - * @param int $quote Quoting style, like htmlspecialchars. ENT_NOQUOTES is - * permissible for non-attribute output. - * @return string escaped data. - */ - public function escape($string, $quote = null) - { - // Workaround for APC bug on Mac Leopard reported by sidepodcast - // http://htmlpurifier.org/phorum/read.php?3,4823,4846 - if ($quote === null) { - $quote = ENT_COMPAT; - } - return htmlspecialchars($string, $quote, 'UTF-8'); - } -} - - - - - -/** - * Definition of the purified HTML that describes allowed children, - * attributes, and many other things. - * - * Conventions: - * - * All member variables that are prefixed with info - * (including the main $info array) are used by HTML Purifier internals - * and should not be directly edited when customizing the HTMLDefinition. - * They can usually be set via configuration directives or custom - * modules. - * - * On the other hand, member variables without the info prefix are used - * internally by the HTMLDefinition and MUST NOT be used by other HTML - * Purifier internals. Many of them, however, are public, and may be - * edited by userspace code to tweak the behavior of HTMLDefinition. - * - * @note This class is inspected by Printer_HTMLDefinition; please - * update that class if things here change. - * - * @warning Directives that change this object's structure must be in - * the HTML or Attr namespace! - */ -class HTMLPurifier_HTMLDefinition extends HTMLPurifier_Definition -{ - - // FULLY-PUBLIC VARIABLES --------------------------------------------- - - /** - * Associative array of element names to HTMLPurifier_ElementDef. - * @type HTMLPurifier_ElementDef[] - */ - public $info = array(); - - /** - * Associative array of global attribute name to attribute definition. - * @type array - */ - public $info_global_attr = array(); - - /** - * String name of parent element HTML will be going into. - * @type string - */ - public $info_parent = 'div'; - - /** - * Definition for parent element, allows parent element to be a - * tag that's not allowed inside the HTML fragment. - * @type HTMLPurifier_ElementDef - */ - public $info_parent_def; - - /** - * String name of element used to wrap inline elements in block context. - * @type string - * @note This is rarely used except for BLOCKQUOTEs in strict mode - */ - public $info_block_wrapper = 'p'; - - /** - * Associative array of deprecated tag name to HTMLPurifier_TagTransform. - * @type array - */ - public $info_tag_transform = array(); - - /** - * Indexed list of HTMLPurifier_AttrTransform to be performed before validation. - * @type HTMLPurifier_AttrTransform[] - */ - public $info_attr_transform_pre = array(); - - /** - * Indexed list of HTMLPurifier_AttrTransform to be performed after validation. - * @type HTMLPurifier_AttrTransform[] - */ - public $info_attr_transform_post = array(); - - /** - * Nested lookup array of content set name (Block, Inline) to - * element name to whether or not it belongs in that content set. - * @type array - */ - public $info_content_sets = array(); - - /** - * Indexed list of HTMLPurifier_Injector to be used. - * @type HTMLPurifier_Injector[] - */ - public $info_injector = array(); - - /** - * Doctype object - * @type HTMLPurifier_Doctype - */ - public $doctype; - - - - // RAW CUSTOMIZATION STUFF -------------------------------------------- - - /** - * Adds a custom attribute to a pre-existing element - * @note This is strictly convenience, and does not have a corresponding - * method in HTMLPurifier_HTMLModule - * @param string $element_name Element name to add attribute to - * @param string $attr_name Name of attribute - * @param mixed $def Attribute definition, can be string or object, see - * HTMLPurifier_AttrTypes for details - */ - public function addAttribute($element_name, $attr_name, $def) - { - $module = $this->getAnonymousModule(); - if (!isset($module->info[$element_name])) { - $element = $module->addBlankElement($element_name); - } else { - $element = $module->info[$element_name]; - } - $element->attr[$attr_name] = $def; - } - - /** - * Adds a custom element to your HTML definition - * @see HTMLPurifier_HTMLModule::addElement() for detailed - * parameter and return value descriptions. - */ - public function addElement($element_name, $type, $contents, $attr_collections, $attributes = array()) - { - $module = $this->getAnonymousModule(); - // assume that if the user is calling this, the element - // is safe. This may not be a good idea - $element = $module->addElement($element_name, $type, $contents, $attr_collections, $attributes); - return $element; - } - - /** - * Adds a blank element to your HTML definition, for overriding - * existing behavior - * @param string $element_name - * @return HTMLPurifier_ElementDef - * @see HTMLPurifier_HTMLModule::addBlankElement() for detailed - * parameter and return value descriptions. - */ - public function addBlankElement($element_name) - { - $module = $this->getAnonymousModule(); - $element = $module->addBlankElement($element_name); - return $element; - } - - /** - * Retrieves a reference to the anonymous module, so you can - * bust out advanced features without having to make your own - * module. - * @return HTMLPurifier_HTMLModule - */ - public function getAnonymousModule() - { - if (!$this->_anonModule) { - $this->_anonModule = new HTMLPurifier_HTMLModule(); - $this->_anonModule->name = 'Anonymous'; - } - return $this->_anonModule; - } - - private $_anonModule = null; - - // PUBLIC BUT INTERNAL VARIABLES -------------------------------------- - - /** - * @type string - */ - public $type = 'HTML'; - - /** - * @type HTMLPurifier_HTMLModuleManager - */ - public $manager; - - /** - * Performs low-cost, preliminary initialization. - */ - public function __construct() - { - $this->manager = new HTMLPurifier_HTMLModuleManager(); - } - - /** - * @param HTMLPurifier_Config $config - */ - protected function doSetup($config) - { - $this->processModules($config); - $this->setupConfigStuff($config); - unset($this->manager); - - // cleanup some of the element definitions - foreach ($this->info as $k => $v) { - unset($this->info[$k]->content_model); - unset($this->info[$k]->content_model_type); - } - } - - /** - * Extract out the information from the manager - * @param HTMLPurifier_Config $config - */ - protected function processModules($config) - { - if ($this->_anonModule) { - // for user specific changes - // this is late-loaded so we don't have to deal with PHP4 - // reference wonky-ness - $this->manager->addModule($this->_anonModule); - unset($this->_anonModule); - } - - $this->manager->setup($config); - $this->doctype = $this->manager->doctype; - - foreach ($this->manager->modules as $module) { - foreach ($module->info_tag_transform as $k => $v) { - if ($v === false) { - unset($this->info_tag_transform[$k]); - } else { - $this->info_tag_transform[$k] = $v; - } - } - foreach ($module->info_attr_transform_pre as $k => $v) { - if ($v === false) { - unset($this->info_attr_transform_pre[$k]); - } else { - $this->info_attr_transform_pre[$k] = $v; - } - } - foreach ($module->info_attr_transform_post as $k => $v) { - if ($v === false) { - unset($this->info_attr_transform_post[$k]); - } else { - $this->info_attr_transform_post[$k] = $v; - } - } - foreach ($module->info_injector as $k => $v) { - if ($v === false) { - unset($this->info_injector[$k]); - } else { - $this->info_injector[$k] = $v; - } - } - } - $this->info = $this->manager->getElements(); - $this->info_content_sets = $this->manager->contentSets->lookup; - } - - /** - * Sets up stuff based on config. We need a better way of doing this. - * @param HTMLPurifier_Config $config - */ - protected function setupConfigStuff($config) - { - $block_wrapper = $config->get('HTML.BlockWrapper'); - if (isset($this->info_content_sets['Block'][$block_wrapper])) { - $this->info_block_wrapper = $block_wrapper; - } else { - trigger_error( - 'Cannot use non-block element as block wrapper', - E_USER_ERROR - ); - } - - $parent = $config->get('HTML.Parent'); - $def = $this->manager->getElement($parent, true); - if ($def) { - $this->info_parent = $parent; - $this->info_parent_def = $def; - } else { - trigger_error( - 'Cannot use unrecognized element as parent', - E_USER_ERROR - ); - $this->info_parent_def = $this->manager->getElement($this->info_parent, true); - } - - // support template text - $support = "(for information on implementing this, see the support forums) "; - - // setup allowed elements ----------------------------------------- - - $allowed_elements = $config->get('HTML.AllowedElements'); - $allowed_attributes = $config->get('HTML.AllowedAttributes'); // retrieve early - - if (!is_array($allowed_elements) && !is_array($allowed_attributes)) { - $allowed = $config->get('HTML.Allowed'); - if (is_string($allowed)) { - list($allowed_elements, $allowed_attributes) = $this->parseTinyMCEAllowedList($allowed); - } - } - - if (is_array($allowed_elements)) { - foreach ($this->info as $name => $d) { - if (!isset($allowed_elements[$name])) { - unset($this->info[$name]); - } - unset($allowed_elements[$name]); - } - // emit errors - foreach ($allowed_elements as $element => $d) { - $element = htmlspecialchars($element); // PHP doesn't escape errors, be careful! - trigger_error("Element '$element' is not supported $support", E_USER_WARNING); - } - } - - // setup allowed attributes --------------------------------------- - - $allowed_attributes_mutable = $allowed_attributes; // by copy! - if (is_array($allowed_attributes)) { - // This actually doesn't do anything, since we went away from - // global attributes. It's possible that userland code uses - // it, but HTMLModuleManager doesn't! - foreach ($this->info_global_attr as $attr => $x) { - $keys = array($attr, "*@$attr", "*.$attr"); - $delete = true; - foreach ($keys as $key) { - if ($delete && isset($allowed_attributes[$key])) { - $delete = false; - } - if (isset($allowed_attributes_mutable[$key])) { - unset($allowed_attributes_mutable[$key]); - } - } - if ($delete) { - unset($this->info_global_attr[$attr]); - } - } - - foreach ($this->info as $tag => $info) { - foreach ($info->attr as $attr => $x) { - $keys = array("$tag@$attr", $attr, "*@$attr", "$tag.$attr", "*.$attr"); - $delete = true; - foreach ($keys as $key) { - if ($delete && isset($allowed_attributes[$key])) { - $delete = false; - } - if (isset($allowed_attributes_mutable[$key])) { - unset($allowed_attributes_mutable[$key]); - } - } - if ($delete) { - if ($this->info[$tag]->attr[$attr]->required) { - trigger_error( - "Required attribute '$attr' in element '$tag' " . - "was not allowed, which means '$tag' will not be allowed either", - E_USER_WARNING - ); - } - unset($this->info[$tag]->attr[$attr]); - } - } - } - // emit errors - foreach ($allowed_attributes_mutable as $elattr => $d) { - $bits = preg_split('/[.@]/', $elattr, 2); - $c = count($bits); - switch ($c) { - case 2: - if ($bits[0] !== '*') { - $element = htmlspecialchars($bits[0]); - $attribute = htmlspecialchars($bits[1]); - if (!isset($this->info[$element])) { - trigger_error( - "Cannot allow attribute '$attribute' if element " . - "'$element' is not allowed/supported $support" - ); - } else { - trigger_error( - "Attribute '$attribute' in element '$element' not supported $support", - E_USER_WARNING - ); - } - break; - } - // otherwise fall through - case 1: - $attribute = htmlspecialchars($bits[0]); - trigger_error( - "Global attribute '$attribute' is not ". - "supported in any elements $support", - E_USER_WARNING - ); - break; - } - } - } - - // setup forbidden elements --------------------------------------- - - $forbidden_elements = $config->get('HTML.ForbiddenElements'); - $forbidden_attributes = $config->get('HTML.ForbiddenAttributes'); - - foreach ($this->info as $tag => $info) { - if (isset($forbidden_elements[$tag])) { - unset($this->info[$tag]); - continue; - } - foreach ($info->attr as $attr => $x) { - if (isset($forbidden_attributes["$tag@$attr"]) || - isset($forbidden_attributes["*@$attr"]) || - isset($forbidden_attributes[$attr]) - ) { - unset($this->info[$tag]->attr[$attr]); - continue; - } elseif (isset($forbidden_attributes["$tag.$attr"])) { // this segment might get removed eventually - // $tag.$attr are not user supplied, so no worries! - trigger_error( - "Error with $tag.$attr: tag.attr syntax not supported for " . - "HTML.ForbiddenAttributes; use tag@attr instead", - E_USER_WARNING - ); - } - } - } - foreach ($forbidden_attributes as $key => $v) { - if (strlen($key) < 2) { - continue; - } - if ($key[0] != '*') { - continue; - } - if ($key[1] == '.') { - trigger_error( - "Error with $key: *.attr syntax not supported for HTML.ForbiddenAttributes; use attr instead", - E_USER_WARNING - ); - } - } - - // setup injectors ----------------------------------------------------- - foreach ($this->info_injector as $i => $injector) { - if ($injector->checkNeeded($config) !== false) { - // remove injector that does not have it's required - // elements/attributes present, and is thus not needed. - unset($this->info_injector[$i]); - } - } - } - - /** - * Parses a TinyMCE-flavored Allowed Elements and Attributes list into - * separate lists for processing. Format is element[attr1|attr2],element2... - * @warning Although it's largely drawn from TinyMCE's implementation, - * it is different, and you'll probably have to modify your lists - * @param array $list String list to parse - * @return array - * @todo Give this its own class, probably static interface - */ - public function parseTinyMCEAllowedList($list) - { - $list = str_replace(array(' ', "\t"), '', $list); - - $elements = array(); - $attributes = array(); - - $chunks = preg_split('/(,|[\n\r]+)/', $list); - foreach ($chunks as $chunk) { - if (empty($chunk)) { - continue; - } - // remove TinyMCE element control characters - if (!strpos($chunk, '[')) { - $element = $chunk; - $attr = false; - } else { - list($element, $attr) = explode('[', $chunk); - } - if ($element !== '*') { - $elements[$element] = true; - } - if (!$attr) { - continue; - } - $attr = substr($attr, 0, strlen($attr) - 1); // remove trailing ] - $attr = explode('|', $attr); - foreach ($attr as $key) { - $attributes["$element.$key"] = true; - } - } - return array($elements, $attributes); - } -} - - - - - -/** - * Represents an XHTML 1.1 module, with information on elements, tags - * and attributes. - * @note Even though this is technically XHTML 1.1, it is also used for - * regular HTML parsing. We are using modulization as a convenient - * way to represent the internals of HTMLDefinition, and our - * implementation is by no means conforming and does not directly - * use the normative DTDs or XML schemas. - * @note The public variables in a module should almost directly - * correspond to the variables in HTMLPurifier_HTMLDefinition. - * However, the prefix info carries no special meaning in these - * objects (include it anyway if that's the correspondence though). - * @todo Consider making some member functions protected - */ - -class HTMLPurifier_HTMLModule -{ - - // -- Overloadable ---------------------------------------------------- - - /** - * Short unique string identifier of the module. - * @type string - */ - public $name; - - /** - * Informally, a list of elements this module changes. - * Not used in any significant way. - * @type array - */ - public $elements = array(); - - /** - * Associative array of element names to element definitions. - * Some definitions may be incomplete, to be merged in later - * with the full definition. - * @type array - */ - public $info = array(); - - /** - * Associative array of content set names to content set additions. - * This is commonly used to, say, add an A element to the Inline - * content set. This corresponds to an internal variable $content_sets - * and NOT info_content_sets member variable of HTMLDefinition. - * @type array - */ - public $content_sets = array(); - - /** - * Associative array of attribute collection names to attribute - * collection additions. More rarely used for adding attributes to - * the global collections. Example is the StyleAttribute module adding - * the style attribute to the Core. Corresponds to HTMLDefinition's - * attr_collections->info, since the object's data is only info, - * with extra behavior associated with it. - * @type array - */ - public $attr_collections = array(); - - /** - * Associative array of deprecated tag name to HTMLPurifier_TagTransform. - * @type array - */ - public $info_tag_transform = array(); - - /** - * List of HTMLPurifier_AttrTransform to be performed before validation. - * @type array - */ - public $info_attr_transform_pre = array(); - - /** - * List of HTMLPurifier_AttrTransform to be performed after validation. - * @type array - */ - public $info_attr_transform_post = array(); - - /** - * List of HTMLPurifier_Injector to be performed during well-formedness fixing. - * An injector will only be invoked if all of it's pre-requisites are met; - * if an injector fails setup, there will be no error; it will simply be - * silently disabled. - * @type array - */ - public $info_injector = array(); - - /** - * Boolean flag that indicates whether or not getChildDef is implemented. - * For optimization reasons: may save a call to a function. Be sure - * to set it if you do implement getChildDef(), otherwise it will have - * no effect! - * @type bool - */ - public $defines_child_def = false; - - /** - * Boolean flag whether or not this module is safe. If it is not safe, all - * of its members are unsafe. Modules are safe by default (this might be - * slightly dangerous, but it doesn't make much sense to force HTML Purifier, - * which is based off of safe HTML, to explicitly say, "This is safe," even - * though there are modules which are "unsafe") - * - * @type bool - * @note Previously, safety could be applied at an element level granularity. - * We've removed this ability, so in order to add "unsafe" elements - * or attributes, a dedicated module with this property set to false - * must be used. - */ - public $safe = true; - - /** - * Retrieves a proper HTMLPurifier_ChildDef subclass based on - * content_model and content_model_type member variables of - * the HTMLPurifier_ElementDef class. There is a similar function - * in HTMLPurifier_HTMLDefinition. - * @param HTMLPurifier_ElementDef $def - * @return HTMLPurifier_ChildDef subclass - */ - public function getChildDef($def) - { - return false; - } - - // -- Convenience ----------------------------------------------------- - - /** - * Convenience function that sets up a new element - * @param string $element Name of element to add - * @param string|bool $type What content set should element be registered to? - * Set as false to skip this step. - * @param string $contents Allowed children in form of: - * "$content_model_type: $content_model" - * @param array $attr_includes What attribute collections to register to - * element? - * @param array $attr What unique attributes does the element define? - * @see HTMLPurifier_ElementDef:: for in-depth descriptions of these parameters. - * @return HTMLPurifier_ElementDef Created element definition object, so you - * can set advanced parameters - */ - public function addElement($element, $type, $contents, $attr_includes = array(), $attr = array()) - { - $this->elements[] = $element; - // parse content_model - list($content_model_type, $content_model) = $this->parseContents($contents); - // merge in attribute inclusions - $this->mergeInAttrIncludes($attr, $attr_includes); - // add element to content sets - if ($type) { - $this->addElementToContentSet($element, $type); - } - // create element - $this->info[$element] = HTMLPurifier_ElementDef::create( - $content_model, - $content_model_type, - $attr - ); - // literal object $contents means direct child manipulation - if (!is_string($contents)) { - $this->info[$element]->child = $contents; - } - return $this->info[$element]; - } - - /** - * Convenience function that creates a totally blank, non-standalone - * element. - * @param string $element Name of element to create - * @return HTMLPurifier_ElementDef Created element - */ - public function addBlankElement($element) - { - if (!isset($this->info[$element])) { - $this->elements[] = $element; - $this->info[$element] = new HTMLPurifier_ElementDef(); - $this->info[$element]->standalone = false; - } else { - trigger_error("Definition for $element already exists in module, cannot redefine"); - } - return $this->info[$element]; - } - - /** - * Convenience function that registers an element to a content set - * @param string $element Element to register - * @param string $type Name content set (warning: case sensitive, usually upper-case - * first letter) - */ - public function addElementToContentSet($element, $type) - { - if (!isset($this->content_sets[$type])) { - $this->content_sets[$type] = ''; - } else { - $this->content_sets[$type] .= ' | '; - } - $this->content_sets[$type] .= $element; - } - - /** - * Convenience function that transforms single-string contents - * into separate content model and content model type - * @param string $contents Allowed children in form of: - * "$content_model_type: $content_model" - * @return array - * @note If contents is an object, an array of two nulls will be - * returned, and the callee needs to take the original $contents - * and use it directly. - */ - public function parseContents($contents) - { - if (!is_string($contents)) { - return array(null, null); - } // defer - switch ($contents) { - // check for shorthand content model forms - case 'Empty': - return array('empty', ''); - case 'Inline': - return array('optional', 'Inline | #PCDATA'); - case 'Flow': - return array('optional', 'Flow | #PCDATA'); - } - list($content_model_type, $content_model) = explode(':', $contents); - $content_model_type = strtolower(trim($content_model_type)); - $content_model = trim($content_model); - return array($content_model_type, $content_model); - } - - /** - * Convenience function that merges a list of attribute includes into - * an attribute array. - * @param array $attr Reference to attr array to modify - * @param array $attr_includes Array of includes / string include to merge in - */ - public function mergeInAttrIncludes(&$attr, $attr_includes) - { - if (!is_array($attr_includes)) { - if (empty($attr_includes)) { - $attr_includes = array(); - } else { - $attr_includes = array($attr_includes); - } - } - $attr[0] = $attr_includes; - } - - /** - * Convenience function that generates a lookup table with boolean - * true as value. - * @param string $list List of values to turn into a lookup - * @note You can also pass an arbitrary number of arguments in - * place of the regular argument - * @return array array equivalent of list - */ - public function makeLookup($list) - { - if (is_string($list)) { - $list = func_get_args(); - } - $ret = array(); - foreach ($list as $value) { - if (is_null($value)) { - continue; - } - $ret[$value] = true; - } - return $ret; - } - - /** - * Lazy load construction of the module after determining whether - * or not it's needed, and also when a finalized configuration object - * is available. - * @param HTMLPurifier_Config $config - */ - public function setup($config) - { - } -} - - - - - -class HTMLPurifier_HTMLModuleManager -{ - - /** - * @type HTMLPurifier_DoctypeRegistry - */ - public $doctypes; - - /** - * Instance of current doctype. - * @type string - */ - public $doctype; - - /** - * @type HTMLPurifier_AttrTypes - */ - public $attrTypes; - - /** - * Active instances of modules for the specified doctype are - * indexed, by name, in this array. - * @type HTMLPurifier_HTMLModule[] - */ - public $modules = array(); - - /** - * Array of recognized HTMLPurifier_HTMLModule instances, - * indexed by module's class name. This array is usually lazy loaded, but a - * user can overload a module by pre-emptively registering it. - * @type HTMLPurifier_HTMLModule[] - */ - public $registeredModules = array(); - - /** - * List of extra modules that were added by the user - * using addModule(). These get unconditionally merged into the current doctype, whatever - * it may be. - * @type HTMLPurifier_HTMLModule[] - */ - public $userModules = array(); - - /** - * Associative array of element name to list of modules that have - * definitions for the element; this array is dynamically filled. - * @type array - */ - public $elementLookup = array(); - - /** - * List of prefixes we should use for registering small names. - * @type array - */ - public $prefixes = array('HTMLPurifier_HTMLModule_'); - - /** - * @type HTMLPurifier_ContentSets - */ - public $contentSets; - - /** - * @type HTMLPurifier_AttrCollections - */ - public $attrCollections; - - /** - * If set to true, unsafe elements and attributes will be allowed. - * @type bool - */ - public $trusted = false; - - public function __construct() - { - // editable internal objects - $this->attrTypes = new HTMLPurifier_AttrTypes(); - $this->doctypes = new HTMLPurifier_DoctypeRegistry(); - - // setup basic modules - $common = array( - 'CommonAttributes', 'Text', 'Hypertext', 'List', - 'Presentation', 'Edit', 'Bdo', 'Tables', 'Image', - 'StyleAttribute', - // Unsafe: - 'Scripting', 'Object', 'Forms', - // Sorta legacy, but present in strict: - 'Name', - ); - $transitional = array('Legacy', 'Target', 'Iframe'); - $xml = array('XMLCommonAttributes'); - $non_xml = array('NonXMLCommonAttributes'); - - // setup basic doctypes - $this->doctypes->register( - 'HTML 4.01 Transitional', - false, - array_merge($common, $transitional, $non_xml), - array('Tidy_Transitional', 'Tidy_Proprietary'), - array(), - '-//W3C//DTD HTML 4.01 Transitional//EN', - 'http://www.w3.org/TR/html4/loose.dtd' - ); - - $this->doctypes->register( - 'HTML 4.01 Strict', - false, - array_merge($common, $non_xml), - array('Tidy_Strict', 'Tidy_Proprietary', 'Tidy_Name'), - array(), - '-//W3C//DTD HTML 4.01//EN', - 'http://www.w3.org/TR/html4/strict.dtd' - ); - - $this->doctypes->register( - 'XHTML 1.0 Transitional', - true, - array_merge($common, $transitional, $xml, $non_xml), - array('Tidy_Transitional', 'Tidy_XHTML', 'Tidy_Proprietary', 'Tidy_Name'), - array(), - '-//W3C//DTD XHTML 1.0 Transitional//EN', - 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd' - ); - - $this->doctypes->register( - 'XHTML 1.0 Strict', - true, - array_merge($common, $xml, $non_xml), - array('Tidy_Strict', 'Tidy_XHTML', 'Tidy_Strict', 'Tidy_Proprietary', 'Tidy_Name'), - array(), - '-//W3C//DTD XHTML 1.0 Strict//EN', - 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd' - ); - - $this->doctypes->register( - 'XHTML 1.1', - true, - // Iframe is a real XHTML 1.1 module, despite being - // "transitional"! - array_merge($common, $xml, array('Ruby', 'Iframe')), - array('Tidy_Strict', 'Tidy_XHTML', 'Tidy_Proprietary', 'Tidy_Strict', 'Tidy_Name'), // Tidy_XHTML1_1 - array(), - '-//W3C//DTD XHTML 1.1//EN', - 'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd' - ); - - } - - /** - * Registers a module to the recognized module list, useful for - * overloading pre-existing modules. - * @param $module Mixed: string module name, with or without - * HTMLPurifier_HTMLModule prefix, or instance of - * subclass of HTMLPurifier_HTMLModule. - * @param $overload Boolean whether or not to overload previous modules. - * If this is not set, and you do overload a module, - * HTML Purifier will complain with a warning. - * @note This function will not call autoload, you must instantiate - * (and thus invoke) autoload outside the method. - * @note If a string is passed as a module name, different variants - * will be tested in this order: - * - Check for HTMLPurifier_HTMLModule_$name - * - Check all prefixes with $name in order they were added - * - Check for literal object name - * - Throw fatal error - * If your object name collides with an internal class, specify - * your module manually. All modules must have been included - * externally: registerModule will not perform inclusions for you! - */ - public function registerModule($module, $overload = false) - { - if (is_string($module)) { - // attempt to load the module - $original_module = $module; - $ok = false; - foreach ($this->prefixes as $prefix) { - $module = $prefix . $original_module; - if (class_exists($module)) { - $ok = true; - break; - } - } - if (!$ok) { - $module = $original_module; - if (!class_exists($module)) { - trigger_error( - $original_module . ' module does not exist', - E_USER_ERROR - ); - return; - } - } - $module = new $module(); - } - if (empty($module->name)) { - trigger_error('Module instance of ' . get_class($module) . ' must have name'); - return; - } - if (!$overload && isset($this->registeredModules[$module->name])) { - trigger_error('Overloading ' . $module->name . ' without explicit overload parameter', E_USER_WARNING); - } - $this->registeredModules[$module->name] = $module; - } - - /** - * Adds a module to the current doctype by first registering it, - * and then tacking it on to the active doctype - */ - public function addModule($module) - { - $this->registerModule($module); - if (is_object($module)) { - $module = $module->name; - } - $this->userModules[] = $module; - } - - /** - * Adds a class prefix that registerModule() will use to resolve a - * string name to a concrete class - */ - public function addPrefix($prefix) - { - $this->prefixes[] = $prefix; - } - - /** - * Performs processing on modules, after being called you may - * use getElement() and getElements() - * @param HTMLPurifier_Config $config - */ - public function setup($config) - { - $this->trusted = $config->get('HTML.Trusted'); - - // generate - $this->doctype = $this->doctypes->make($config); - $modules = $this->doctype->modules; - - // take out the default modules that aren't allowed - $lookup = $config->get('HTML.AllowedModules'); - $special_cases = $config->get('HTML.CoreModules'); - - if (is_array($lookup)) { - foreach ($modules as $k => $m) { - if (isset($special_cases[$m])) { - continue; - } - if (!isset($lookup[$m])) { - unset($modules[$k]); - } - } - } - - // custom modules - if ($config->get('HTML.Proprietary')) { - $modules[] = 'Proprietary'; - } - if ($config->get('HTML.SafeObject')) { - $modules[] = 'SafeObject'; - } - if ($config->get('HTML.SafeEmbed')) { - $modules[] = 'SafeEmbed'; - } - if ($config->get('HTML.SafeScripting') !== array()) { - $modules[] = 'SafeScripting'; - } - if ($config->get('HTML.Nofollow')) { - $modules[] = 'Nofollow'; - } - if ($config->get('HTML.TargetBlank')) { - $modules[] = 'TargetBlank'; - } - - // merge in custom modules - $modules = array_merge($modules, $this->userModules); - - foreach ($modules as $module) { - $this->processModule($module); - $this->modules[$module]->setup($config); - } - - foreach ($this->doctype->tidyModules as $module) { - $this->processModule($module); - $this->modules[$module]->setup($config); - } - - // prepare any injectors - foreach ($this->modules as $module) { - $n = array(); - foreach ($module->info_injector as $injector) { - if (!is_object($injector)) { - $class = "HTMLPurifier_Injector_$injector"; - $injector = new $class; - } - $n[$injector->name] = $injector; - } - $module->info_injector = $n; - } - - // setup lookup table based on all valid modules - foreach ($this->modules as $module) { - foreach ($module->info as $name => $def) { - if (!isset($this->elementLookup[$name])) { - $this->elementLookup[$name] = array(); - } - $this->elementLookup[$name][] = $module->name; - } - } - - // note the different choice - $this->contentSets = new HTMLPurifier_ContentSets( - // content set assembly deals with all possible modules, - // not just ones deemed to be "safe" - $this->modules - ); - $this->attrCollections = new HTMLPurifier_AttrCollections( - $this->attrTypes, - // there is no way to directly disable a global attribute, - // but using AllowedAttributes or simply not including - // the module in your custom doctype should be sufficient - $this->modules - ); - } - - /** - * Takes a module and adds it to the active module collection, - * registering it if necessary. - */ - public function processModule($module) - { - if (!isset($this->registeredModules[$module]) || is_object($module)) { - $this->registerModule($module); - } - $this->modules[$module] = $this->registeredModules[$module]; - } - - /** - * Retrieves merged element definitions. - * @return Array of HTMLPurifier_ElementDef - */ - public function getElements() - { - $elements = array(); - foreach ($this->modules as $module) { - if (!$this->trusted && !$module->safe) { - continue; - } - foreach ($module->info as $name => $v) { - if (isset($elements[$name])) { - continue; - } - $elements[$name] = $this->getElement($name); - } - } - - // remove dud elements, this happens when an element that - // appeared to be safe actually wasn't - foreach ($elements as $n => $v) { - if ($v === false) { - unset($elements[$n]); - } - } - - return $elements; - - } - - /** - * Retrieves a single merged element definition - * @param string $name Name of element - * @param bool $trusted Boolean trusted overriding parameter: set to true - * if you want the full version of an element - * @return HTMLPurifier_ElementDef Merged HTMLPurifier_ElementDef - * @note You may notice that modules are getting iterated over twice (once - * in getElements() and once here). This - * is because - */ - public function getElement($name, $trusted = null) - { - if (!isset($this->elementLookup[$name])) { - return false; - } - - // setup global state variables - $def = false; - if ($trusted === null) { - $trusted = $this->trusted; - } - - // iterate through each module that has registered itself to this - // element - foreach ($this->elementLookup[$name] as $module_name) { - $module = $this->modules[$module_name]; - - // refuse to create/merge from a module that is deemed unsafe-- - // pretend the module doesn't exist--when trusted mode is not on. - if (!$trusted && !$module->safe) { - continue; - } - - // clone is used because, ideally speaking, the original - // definition should not be modified. Usually, this will - // make no difference, but for consistency's sake - $new_def = clone $module->info[$name]; - - if (!$def && $new_def->standalone) { - $def = $new_def; - } elseif ($def) { - // This will occur even if $new_def is standalone. In practice, - // this will usually result in a full replacement. - $def->mergeIn($new_def); - } else { - // :TODO: - // non-standalone definitions that don't have a standalone - // to merge into could be deferred to the end - // HOWEVER, it is perfectly valid for a non-standalone - // definition to lack a standalone definition, even - // after all processing: this allows us to safely - // specify extra attributes for elements that may not be - // enabled all in one place. In particular, this might - // be the case for trusted elements. WARNING: care must - // be taken that the /extra/ definitions are all safe. - continue; - } - - // attribute value expansions - $this->attrCollections->performInclusions($def->attr); - $this->attrCollections->expandIdentifiers($def->attr, $this->attrTypes); - - // descendants_are_inline, for ChildDef_Chameleon - if (is_string($def->content_model) && - strpos($def->content_model, 'Inline') !== false) { - if ($name != 'del' && $name != 'ins') { - // this is for you, ins/del - $def->descendants_are_inline = true; - } - } - - $this->contentSets->generateChildDef($def, $module); - } - - // This can occur if there is a blank definition, but no base to - // mix it in with - if (!$def) { - return false; - } - - // add information on required attributes - foreach ($def->attr as $attr_name => $attr_def) { - if ($attr_def->required) { - $def->required_attr[] = $attr_name; - } - } - return $def; - } -} - - - - - -/** - * Component of HTMLPurifier_AttrContext that accumulates IDs to prevent dupes - * @note In Slashdot-speak, dupe means duplicate. - * @note The default constructor does not accept $config or $context objects: - * use must use the static build() factory method to perform initialization. - */ -class HTMLPurifier_IDAccumulator -{ - - /** - * Lookup table of IDs we've accumulated. - * @public - */ - public $ids = array(); - - /** - * Builds an IDAccumulator, also initializing the default blacklist - * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config - * @param HTMLPurifier_Context $context Instance of HTMLPurifier_Context - * @return HTMLPurifier_IDAccumulator Fully initialized HTMLPurifier_IDAccumulator - */ - public static function build($config, $context) - { - $id_accumulator = new HTMLPurifier_IDAccumulator(); - $id_accumulator->load($config->get('Attr.IDBlacklist')); - return $id_accumulator; - } - - /** - * Add an ID to the lookup table. - * @param string $id ID to be added. - * @return bool status, true if success, false if there's a dupe - */ - public function add($id) - { - if (isset($this->ids[$id])) { - return false; - } - return $this->ids[$id] = true; - } - - /** - * Load a list of IDs into the lookup table - * @param $array_of_ids Array of IDs to load - * @note This function doesn't care about duplicates - */ - public function load($array_of_ids) - { - foreach ($array_of_ids as $id) { - $this->ids[$id] = true; - } - } -} - - - - - -/** - * Injects tokens into the document while parsing for well-formedness. - * This enables "formatter-like" functionality such as auto-paragraphing, - * smiley-ification and linkification to take place. - * - * A note on how handlers create changes; this is done by assigning a new - * value to the $token reference. These values can take a variety of forms and - * are best described HTMLPurifier_Strategy_MakeWellFormed->processToken() - * documentation. - * - * @todo Allow injectors to request a re-run on their output. This - * would help if an operation is recursive. - */ -abstract class HTMLPurifier_Injector -{ - - /** - * Advisory name of injector, this is for friendly error messages. - * @type string - */ - public $name; - - /** - * @type HTMLPurifier_HTMLDefinition - */ - protected $htmlDefinition; - - /** - * Reference to CurrentNesting variable in Context. This is an array - * list of tokens that we are currently "inside" - * @type array - */ - protected $currentNesting; - - /** - * Reference to current token. - * @type HTMLPurifier_Token - */ - protected $currentToken; - - /** - * Reference to InputZipper variable in Context. - * @type HTMLPurifier_Zipper - */ - protected $inputZipper; - - /** - * Array of elements and attributes this injector creates and therefore - * need to be allowed by the definition. Takes form of - * array('element' => array('attr', 'attr2'), 'element2') - * @type array - */ - public $needed = array(); - - /** - * Number of elements to rewind backwards (relative). - * @type bool|int - */ - protected $rewindOffset = false; - - /** - * Rewind to a spot to re-perform processing. This is useful if you - * deleted a node, and now need to see if this change affected any - * earlier nodes. Rewinding does not affect other injectors, and can - * result in infinite loops if not used carefully. - * @param bool|int $offset - * @warning HTML Purifier will prevent you from fast-forwarding with this - * function. - */ - public function rewindOffset($offset) - { - $this->rewindOffset = $offset; - } - - /** - * Retrieves rewind offset, and then unsets it. - * @return bool|int - */ - public function getRewindOffset() - { - $r = $this->rewindOffset; - $this->rewindOffset = false; - return $r; - } - - /** - * Prepares the injector by giving it the config and context objects: - * this allows references to important variables to be made within - * the injector. This function also checks if the HTML environment - * will work with the Injector (see checkNeeded()). - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string Boolean false if success, string of missing needed element/attribute if failure - */ - public function prepare($config, $context) - { - $this->htmlDefinition = $config->getHTMLDefinition(); - // Even though this might fail, some unit tests ignore this and - // still test checkNeeded, so be careful. Maybe get rid of that - // dependency. - $result = $this->checkNeeded($config); - if ($result !== false) { - return $result; - } - $this->currentNesting =& $context->get('CurrentNesting'); - $this->currentToken =& $context->get('CurrentToken'); - $this->inputZipper =& $context->get('InputZipper'); - return false; - } - - /** - * This function checks if the HTML environment - * will work with the Injector: if p tags are not allowed, the - * Auto-Paragraphing injector should not be enabled. - * @param HTMLPurifier_Config $config - * @return bool|string Boolean false if success, string of missing needed element/attribute if failure - */ - public function checkNeeded($config) - { - $def = $config->getHTMLDefinition(); - foreach ($this->needed as $element => $attributes) { - if (is_int($element)) { - $element = $attributes; - } - if (!isset($def->info[$element])) { - return $element; - } - if (!is_array($attributes)) { - continue; - } - foreach ($attributes as $name) { - if (!isset($def->info[$element]->attr[$name])) { - return "$element.$name"; - } - } - } - return false; - } - - /** - * Tests if the context node allows a certain element - * @param string $name Name of element to test for - * @return bool True if element is allowed, false if it is not - */ - public function allowsElement($name) - { - if (!empty($this->currentNesting)) { - $parent_token = array_pop($this->currentNesting); - $this->currentNesting[] = $parent_token; - $parent = $this->htmlDefinition->info[$parent_token->name]; - } else { - $parent = $this->htmlDefinition->info_parent_def; - } - if (!isset($parent->child->elements[$name]) || isset($parent->excludes[$name])) { - return false; - } - // check for exclusion - for ($i = count($this->currentNesting) - 2; $i >= 0; $i--) { - $node = $this->currentNesting[$i]; - $def = $this->htmlDefinition->info[$node->name]; - if (isset($def->excludes[$name])) { - return false; - } - } - return true; - } - - /** - * Iterator function, which starts with the next token and continues until - * you reach the end of the input tokens. - * @warning Please prevent previous references from interfering with this - * functions by setting $i = null beforehand! - * @param int $i Current integer index variable for inputTokens - * @param HTMLPurifier_Token $current Current token variable. - * Do NOT use $token, as that variable is also a reference - * @return bool - */ - protected function forward(&$i, &$current) - { - if ($i === null) { - $i = count($this->inputZipper->back) - 1; - } else { - $i--; - } - if ($i < 0) { - return false; - } - $current = $this->inputZipper->back[$i]; - return true; - } - - /** - * Similar to _forward, but accepts a third parameter $nesting (which - * should be initialized at 0) and stops when we hit the end tag - * for the node $this->inputIndex starts in. - * @param int $i Current integer index variable for inputTokens - * @param HTMLPurifier_Token $current Current token variable. - * Do NOT use $token, as that variable is also a reference - * @param int $nesting - * @return bool - */ - protected function forwardUntilEndToken(&$i, &$current, &$nesting) - { - $result = $this->forward($i, $current); - if (!$result) { - return false; - } - if ($nesting === null) { - $nesting = 0; - } - if ($current instanceof HTMLPurifier_Token_Start) { - $nesting++; - } elseif ($current instanceof HTMLPurifier_Token_End) { - if ($nesting <= 0) { - return false; - } - $nesting--; - } - return true; - } - - /** - * Iterator function, starts with the previous token and continues until - * you reach the beginning of input tokens. - * @warning Please prevent previous references from interfering with this - * functions by setting $i = null beforehand! - * @param int $i Current integer index variable for inputTokens - * @param HTMLPurifier_Token $current Current token variable. - * Do NOT use $token, as that variable is also a reference - * @return bool - */ - protected function backward(&$i, &$current) - { - if ($i === null) { - $i = count($this->inputZipper->front) - 1; - } else { - $i--; - } - if ($i < 0) { - return false; - } - $current = $this->inputZipper->front[$i]; - return true; - } - - /** - * Handler that is called when a text token is processed - */ - public function handleText(&$token) - { - } - - /** - * Handler that is called when a start or empty token is processed - */ - public function handleElement(&$token) - { - } - - /** - * Handler that is called when an end token is processed - */ - public function handleEnd(&$token) - { - $this->notifyEnd($token); - } - - /** - * Notifier that is called when an end token is processed - * @param HTMLPurifier_Token $token Current token variable. - * @note This differs from handlers in that the token is read-only - * @deprecated - */ - public function notifyEnd($token) - { - } -} - - - - - -/** - * Represents a language and defines localizable string formatting and - * other functions, as well as the localized messages for HTML Purifier. - */ -class HTMLPurifier_Language -{ - - /** - * ISO 639 language code of language. Prefers shortest possible version. - * @type string - */ - public $code = 'en'; - - /** - * Fallback language code. - * @type bool|string - */ - public $fallback = false; - - /** - * Array of localizable messages. - * @type array - */ - public $messages = array(); - - /** - * Array of localizable error codes. - * @type array - */ - public $errorNames = array(); - - /** - * True if no message file was found for this language, so English - * is being used instead. Check this if you'd like to notify the - * user that they've used a non-supported language. - * @type bool - */ - public $error = false; - - /** - * Has the language object been loaded yet? - * @type bool - * @todo Make it private, fix usage in HTMLPurifier_LanguageTest - */ - public $_loaded = false; - - /** - * @type HTMLPurifier_Config - */ - protected $config; - - /** - * @type HTMLPurifier_Context - */ - protected $context; - - /** - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - */ - public function __construct($config, $context) - { - $this->config = $config; - $this->context = $context; - } - - /** - * Loads language object with necessary info from factory cache - * @note This is a lazy loader - */ - public function load() - { - if ($this->_loaded) { - return; - } - $factory = HTMLPurifier_LanguageFactory::instance(); - $factory->loadLanguage($this->code); - foreach ($factory->keys as $key) { - $this->$key = $factory->cache[$this->code][$key]; - } - $this->_loaded = true; - } - - /** - * Retrieves a localised message. - * @param string $key string identifier of message - * @return string localised message - */ - public function getMessage($key) - { - if (!$this->_loaded) { - $this->load(); - } - if (!isset($this->messages[$key])) { - return "[$key]"; - } - return $this->messages[$key]; - } - - /** - * Retrieves a localised error name. - * @param int $int error number, corresponding to PHP's error reporting - * @return string localised message - */ - public function getErrorName($int) - { - if (!$this->_loaded) { - $this->load(); - } - if (!isset($this->errorNames[$int])) { - return "[Error: $int]"; - } - return $this->errorNames[$int]; - } - - /** - * Converts an array list into a string readable representation - * @param array $array - * @return string - */ - public function listify($array) - { - $sep = $this->getMessage('Item separator'); - $sep_last = $this->getMessage('Item separator last'); - $ret = ''; - for ($i = 0, $c = count($array); $i < $c; $i++) { - if ($i == 0) { - } elseif ($i + 1 < $c) { - $ret .= $sep; - } else { - $ret .= $sep_last; - } - $ret .= $array[$i]; - } - return $ret; - } - - /** - * Formats a localised message with passed parameters - * @param string $key string identifier of message - * @param array $args Parameters to substitute in - * @return string localised message - * @todo Implement conditionals? Right now, some messages make - * reference to line numbers, but those aren't always available - */ - public function formatMessage($key, $args = array()) - { - if (!$this->_loaded) { - $this->load(); - } - if (!isset($this->messages[$key])) { - return "[$key]"; - } - $raw = $this->messages[$key]; - $subst = array(); - $generator = false; - foreach ($args as $i => $value) { - if (is_object($value)) { - if ($value instanceof HTMLPurifier_Token) { - // factor this out some time - if (!$generator) { - $generator = $this->context->get('Generator'); - } - if (isset($value->name)) { - $subst['$'.$i.'.Name'] = $value->name; - } - if (isset($value->data)) { - $subst['$'.$i.'.Data'] = $value->data; - } - $subst['$'.$i.'.Compact'] = - $subst['$'.$i.'.Serialized'] = $generator->generateFromToken($value); - // a more complex algorithm for compact representation - // could be introduced for all types of tokens. This - // may need to be factored out into a dedicated class - if (!empty($value->attr)) { - $stripped_token = clone $value; - $stripped_token->attr = array(); - $subst['$'.$i.'.Compact'] = $generator->generateFromToken($stripped_token); - } - $subst['$'.$i.'.Line'] = $value->line ? $value->line : 'unknown'; - } - continue; - } elseif (is_array($value)) { - $keys = array_keys($value); - if (array_keys($keys) === $keys) { - // list - $subst['$'.$i] = $this->listify($value); - } else { - // associative array - // no $i implementation yet, sorry - $subst['$'.$i.'.Keys'] = $this->listify($keys); - $subst['$'.$i.'.Values'] = $this->listify(array_values($value)); - } - continue; - } - $subst['$' . $i] = $value; - } - return strtr($raw, $subst); - } -} - - - - - -/** - * Class responsible for generating HTMLPurifier_Language objects, managing - * caching and fallbacks. - * @note Thanks to MediaWiki for the general logic, although this version - * has been entirely rewritten - * @todo Serialized cache for languages - */ -class HTMLPurifier_LanguageFactory -{ - - /** - * Cache of language code information used to load HTMLPurifier_Language objects. - * Structure is: $factory->cache[$language_code][$key] = $value - * @type array - */ - public $cache; - - /** - * Valid keys in the HTMLPurifier_Language object. Designates which - * variables to slurp out of a message file. - * @type array - */ - public $keys = array('fallback', 'messages', 'errorNames'); - - /** - * Instance to validate language codes. - * @type HTMLPurifier_AttrDef_Lang - * - */ - protected $validator; - - /** - * Cached copy of dirname(__FILE__), directory of current file without - * trailing slash. - * @type string - */ - protected $dir; - - /** - * Keys whose contents are a hash map and can be merged. - * @type array - */ - protected $mergeable_keys_map = array('messages' => true, 'errorNames' => true); - - /** - * Keys whose contents are a list and can be merged. - * @value array lookup - */ - protected $mergeable_keys_list = array(); - - /** - * Retrieve sole instance of the factory. - * @param HTMLPurifier_LanguageFactory $prototype Optional prototype to overload sole instance with, - * or bool true to reset to default factory. - * @return HTMLPurifier_LanguageFactory - */ - public static function instance($prototype = null) - { - static $instance = null; - if ($prototype !== null) { - $instance = $prototype; - } elseif ($instance === null || $prototype == true) { - $instance = new HTMLPurifier_LanguageFactory(); - $instance->setup(); - } - return $instance; - } - - /** - * Sets up the singleton, much like a constructor - * @note Prevents people from getting this outside of the singleton - */ - public function setup() - { - $this->validator = new HTMLPurifier_AttrDef_Lang(); - $this->dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier'; - } - - /** - * Creates a language object, handles class fallbacks - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @param bool|string $code Code to override configuration with. Private parameter. - * @return HTMLPurifier_Language - */ - public function create($config, $context, $code = false) - { - // validate language code - if ($code === false) { - $code = $this->validator->validate( - $config->get('Core.Language'), - $config, - $context - ); - } else { - $code = $this->validator->validate($code, $config, $context); - } - if ($code === false) { - $code = 'en'; // malformed code becomes English - } - - $pcode = str_replace('-', '_', $code); // make valid PHP classname - static $depth = 0; // recursion protection - - if ($code == 'en') { - $lang = new HTMLPurifier_Language($config, $context); - } else { - $class = 'HTMLPurifier_Language_' . $pcode; - $file = $this->dir . '/Language/classes/' . $code . '.php'; - if (file_exists($file) || class_exists($class, false)) { - $lang = new $class($config, $context); - } else { - // Go fallback - $raw_fallback = $this->getFallbackFor($code); - $fallback = $raw_fallback ? $raw_fallback : 'en'; - $depth++; - $lang = $this->create($config, $context, $fallback); - if (!$raw_fallback) { - $lang->error = true; - } - $depth--; - } - } - $lang->code = $code; - return $lang; - } - - /** - * Returns the fallback language for language - * @note Loads the original language into cache - * @param string $code language code - * @return string|bool - */ - public function getFallbackFor($code) - { - $this->loadLanguage($code); - return $this->cache[$code]['fallback']; - } - - /** - * Loads language into the cache, handles message file and fallbacks - * @param string $code language code - */ - public function loadLanguage($code) - { - static $languages_seen = array(); // recursion guard - - // abort if we've already loaded it - if (isset($this->cache[$code])) { - return; - } - - // generate filename - $filename = $this->dir . '/Language/messages/' . $code . '.php'; - - // default fallback : may be overwritten by the ensuing include - $fallback = ($code != 'en') ? 'en' : false; - - // load primary localisation - if (!file_exists($filename)) { - // skip the include: will rely solely on fallback - $filename = $this->dir . '/Language/messages/en.php'; - $cache = array(); - } else { - include $filename; - $cache = compact($this->keys); - } - - // load fallback localisation - if (!empty($fallback)) { - - // infinite recursion guard - if (isset($languages_seen[$code])) { - trigger_error( - 'Circular fallback reference in language ' . - $code, - E_USER_ERROR - ); - $fallback = 'en'; - } - $language_seen[$code] = true; - - // load the fallback recursively - $this->loadLanguage($fallback); - $fallback_cache = $this->cache[$fallback]; - - // merge fallback with current language - foreach ($this->keys as $key) { - if (isset($cache[$key]) && isset($fallback_cache[$key])) { - if (isset($this->mergeable_keys_map[$key])) { - $cache[$key] = $cache[$key] + $fallback_cache[$key]; - } elseif (isset($this->mergeable_keys_list[$key])) { - $cache[$key] = array_merge($fallback_cache[$key], $cache[$key]); - } - } else { - $cache[$key] = $fallback_cache[$key]; - } - } - } - - // save to cache for later retrieval - $this->cache[$code] = $cache; - return; - } -} - - - - - -/** - * Represents a measurable length, with a string numeric magnitude - * and a unit. This object is immutable. - */ -class HTMLPurifier_Length -{ - - /** - * String numeric magnitude. - * @type string - */ - protected $n; - - /** - * String unit. False is permitted if $n = 0. - * @type string|bool - */ - protected $unit; - - /** - * Whether or not this length is valid. Null if not calculated yet. - * @type bool - */ - protected $isValid; - - /** - * Array Lookup array of units recognized by CSS 2.1 - * @type array - */ - protected static $allowedUnits = array( - 'em' => true, 'ex' => true, 'px' => true, 'in' => true, - 'cm' => true, 'mm' => true, 'pt' => true, 'pc' => true - ); - - /** - * @param string $n Magnitude - * @param bool|string $u Unit - */ - public function __construct($n = '0', $u = false) - { - $this->n = (string) $n; - $this->unit = $u !== false ? (string) $u : false; - } - - /** - * @param string $s Unit string, like '2em' or '3.4in' - * @return HTMLPurifier_Length - * @warning Does not perform validation. - */ - public static function make($s) - { - if ($s instanceof HTMLPurifier_Length) { - return $s; - } - $n_length = strspn($s, '1234567890.+-'); - $n = substr($s, 0, $n_length); - $unit = substr($s, $n_length); - if ($unit === '') { - $unit = false; - } - return new HTMLPurifier_Length($n, $unit); - } - - /** - * Validates the number and unit. - * @return bool - */ - protected function validate() - { - // Special case: - if ($this->n === '+0' || $this->n === '-0') { - $this->n = '0'; - } - if ($this->n === '0' && $this->unit === false) { - return true; - } - if (!ctype_lower($this->unit)) { - $this->unit = strtolower($this->unit); - } - if (!isset(HTMLPurifier_Length::$allowedUnits[$this->unit])) { - return false; - } - // Hack: - $def = new HTMLPurifier_AttrDef_CSS_Number(); - $result = $def->validate($this->n, false, false); - if ($result === false) { - return false; - } - $this->n = $result; - return true; - } - - /** - * Returns string representation of number. - * @return string - */ - public function toString() - { - if (!$this->isValid()) { - return false; - } - return $this->n . $this->unit; - } - - /** - * Retrieves string numeric magnitude. - * @return string - */ - public function getN() - { - return $this->n; - } - - /** - * Retrieves string unit. - * @return string - */ - public function getUnit() - { - return $this->unit; - } - - /** - * Returns true if this length unit is valid. - * @return bool - */ - public function isValid() - { - if ($this->isValid === null) { - $this->isValid = $this->validate(); - } - return $this->isValid; - } - - /** - * Compares two lengths, and returns 1 if greater, -1 if less and 0 if equal. - * @param HTMLPurifier_Length $l - * @return int - * @warning If both values are too large or small, this calculation will - * not work properly - */ - public function compareTo($l) - { - if ($l === false) { - return false; - } - if ($l->unit !== $this->unit) { - $converter = new HTMLPurifier_UnitConverter(); - $l = $converter->convert($l, $this->unit); - if ($l === false) { - return false; - } - } - return $this->n - $l->n; - } -} - - - - - -/** - * Forgivingly lexes HTML (SGML-style) markup into tokens. - * - * A lexer parses a string of SGML-style markup and converts them into - * corresponding tokens. It doesn't check for well-formedness, although its - * internal mechanism may make this automatic (such as the case of - * HTMLPurifier_Lexer_DOMLex). There are several implementations to choose - * from. - * - * A lexer is HTML-oriented: it might work with XML, but it's not - * recommended, as we adhere to a subset of the specification for optimization - * reasons. This might change in the future. Also, most tokenizers are not - * expected to handle DTDs or PIs. - * - * This class should not be directly instantiated, but you may use create() to - * retrieve a default copy of the lexer. Being a supertype, this class - * does not actually define any implementation, but offers commonly used - * convenience functions for subclasses. - * - * @note The unit tests will instantiate this class for testing purposes, as - * many of the utility functions require a class to be instantiated. - * This means that, even though this class is not runnable, it will - * not be declared abstract. - * - * @par - * - * @note - * We use tokens rather than create a DOM representation because DOM would: - * - * @par - * -# Require more processing and memory to create, - * -# Is not streamable, and - * -# Has the entire document structure (html and body not needed). - * - * @par - * However, DOM is helpful in that it makes it easy to move around nodes - * without a lot of lookaheads to see when a tag is closed. This is a - * limitation of the token system and some workarounds would be nice. - */ -class HTMLPurifier_Lexer -{ - - /** - * Whether or not this lexer implements line-number/column-number tracking. - * If it does, set to true. - */ - public $tracksLineNumbers = false; - - // -- STATIC ---------------------------------------------------------- - - /** - * Retrieves or sets the default Lexer as a Prototype Factory. - * - * By default HTMLPurifier_Lexer_DOMLex will be returned. There are - * a few exceptions involving special features that only DirectLex - * implements. - * - * @note The behavior of this class has changed, rather than accepting - * a prototype object, it now accepts a configuration object. - * To specify your own prototype, set %Core.LexerImpl to it. - * This change in behavior de-singletonizes the lexer object. - * - * @param HTMLPurifier_Config $config - * @return HTMLPurifier_Lexer - * @throws HTMLPurifier_Exception - */ - public static function create($config) - { - if (!($config instanceof HTMLPurifier_Config)) { - $lexer = $config; - trigger_error( - "Passing a prototype to - HTMLPurifier_Lexer::create() is deprecated, please instead - use %Core.LexerImpl", - E_USER_WARNING - ); - } else { - $lexer = $config->get('Core.LexerImpl'); - } - - $needs_tracking = - $config->get('Core.MaintainLineNumbers') || - $config->get('Core.CollectErrors'); - - $inst = null; - if (is_object($lexer)) { - $inst = $lexer; - } else { - if (is_null($lexer)) { - do { - // auto-detection algorithm - if ($needs_tracking) { - $lexer = 'DirectLex'; - break; - } - - if (class_exists('DOMDocument') && - method_exists('DOMDocument', 'loadHTML') && - !extension_loaded('domxml') - ) { - // check for DOM support, because while it's part of the - // core, it can be disabled compile time. Also, the PECL - // domxml extension overrides the default DOM, and is evil - // and nasty and we shan't bother to support it - $lexer = 'DOMLex'; - } else { - $lexer = 'DirectLex'; - } - } while (0); - } // do..while so we can break - - // instantiate recognized string names - switch ($lexer) { - case 'DOMLex': - $inst = new HTMLPurifier_Lexer_DOMLex(); - break; - case 'DirectLex': - $inst = new HTMLPurifier_Lexer_DirectLex(); - break; - case 'PH5P': - $inst = new HTMLPurifier_Lexer_PH5P(); - break; - default: - throw new HTMLPurifier_Exception( - "Cannot instantiate unrecognized Lexer type " . - htmlspecialchars($lexer) - ); - } - } - - if (!$inst) { - throw new HTMLPurifier_Exception('No lexer was instantiated'); - } - - // once PHP DOM implements native line numbers, or we - // hack out something using XSLT, remove this stipulation - if ($needs_tracking && !$inst->tracksLineNumbers) { - throw new HTMLPurifier_Exception( - 'Cannot use lexer that does not support line numbers with ' . - 'Core.MaintainLineNumbers or Core.CollectErrors (use DirectLex instead)' - ); - } - - return $inst; - - } - - // -- CONVENIENCE MEMBERS --------------------------------------------- - - public function __construct() - { - $this->_entity_parser = new HTMLPurifier_EntityParser(); - } - - /** - * Most common entity to raw value conversion table for special entities. - * @type array - */ - protected $_special_entity2str = - array( - '"' => '"', - '&' => '&', - '<' => '<', - '>' => '>', - ''' => "'", - ''' => "'", - ''' => "'" - ); - - /** - * Parses special entities into the proper characters. - * - * This string will translate escaped versions of the special characters - * into the correct ones. - * - * @warning - * You should be able to treat the output of this function as - * completely parsed, but that's only because all other entities should - * have been handled previously in substituteNonSpecialEntities() - * - * @param string $string String character data to be parsed. - * @return string Parsed character data. - */ - public function parseData($string) - { - // following functions require at least one character - if ($string === '') { - return ''; - } - - // subtracts amps that cannot possibly be escaped - $num_amp = substr_count($string, '&') - substr_count($string, '& ') - - ($string[strlen($string) - 1] === '&' ? 1 : 0); - - if (!$num_amp) { - return $string; - } // abort if no entities - $num_esc_amp = substr_count($string, '&'); - $string = strtr($string, $this->_special_entity2str); - - // code duplication for sake of optimization, see above - $num_amp_2 = substr_count($string, '&') - substr_count($string, '& ') - - ($string[strlen($string) - 1] === '&' ? 1 : 0); - - if ($num_amp_2 <= $num_esc_amp) { - return $string; - } - - // hmm... now we have some uncommon entities. Use the callback. - $string = $this->_entity_parser->substituteSpecialEntities($string); - return $string; - } - - /** - * Lexes an HTML string into tokens. - * @param $string String HTML. - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return HTMLPurifier_Token[] array representation of HTML. - */ - public function tokenizeHTML($string, $config, $context) - { - trigger_error('Call to abstract class', E_USER_ERROR); - } - - /** - * Translates CDATA sections into regular sections (through escaping). - * @param string $string HTML string to process. - * @return string HTML with CDATA sections escaped. - */ - protected static function escapeCDATA($string) - { - return preg_replace_callback( - '//s', - array('HTMLPurifier_Lexer', 'CDATACallback'), - $string - ); - } - - /** - * Special CDATA case that is especially convoluted for )#si', - array($this, 'scriptCallback'), - $html - ); - } - - $html = $this->normalize($html, $config, $context); - - $cursor = 0; // our location in the text - $inside_tag = false; // whether or not we're parsing the inside of a tag - $array = array(); // result array - - // This is also treated to mean maintain *column* numbers too - $maintain_line_numbers = $config->get('Core.MaintainLineNumbers'); - - if ($maintain_line_numbers === null) { - // automatically determine line numbering by checking - // if error collection is on - $maintain_line_numbers = $config->get('Core.CollectErrors'); - } - - if ($maintain_line_numbers) { - $current_line = 1; - $current_col = 0; - $length = strlen($html); - } else { - $current_line = false; - $current_col = false; - $length = false; - } - $context->register('CurrentLine', $current_line); - $context->register('CurrentCol', $current_col); - $nl = "\n"; - // how often to manually recalculate. This will ALWAYS be right, - // but it's pretty wasteful. Set to 0 to turn off - $synchronize_interval = $config->get('Core.DirectLexLineNumberSyncInterval'); - - $e = false; - if ($config->get('Core.CollectErrors')) { - $e =& $context->get('ErrorCollector'); - } - - // for testing synchronization - $loops = 0; - - while (++$loops) { - // $cursor is either at the start of a token, or inside of - // a tag (i.e. there was a < immediately before it), as indicated - // by $inside_tag - - if ($maintain_line_numbers) { - // $rcursor, however, is always at the start of a token. - $rcursor = $cursor - (int)$inside_tag; - - // Column number is cheap, so we calculate it every round. - // We're interested at the *end* of the newline string, so - // we need to add strlen($nl) == 1 to $nl_pos before subtracting it - // from our "rcursor" position. - $nl_pos = strrpos($html, $nl, $rcursor - $length); - $current_col = $rcursor - (is_bool($nl_pos) ? 0 : $nl_pos + 1); - - // recalculate lines - if ($synchronize_interval && // synchronization is on - $cursor > 0 && // cursor is further than zero - $loops % $synchronize_interval === 0) { // time to synchronize! - $current_line = 1 + $this->substrCount($html, $nl, 0, $cursor); - } - } - - $position_next_lt = strpos($html, '<', $cursor); - $position_next_gt = strpos($html, '>', $cursor); - - // triggers on "asdf" but not "asdf " - // special case to set up context - if ($position_next_lt === $cursor) { - $inside_tag = true; - $cursor++; - } - - if (!$inside_tag && $position_next_lt !== false) { - // We are not inside tag and there still is another tag to parse - $token = new - HTMLPurifier_Token_Text( - $this->parseData( - substr( - $html, - $cursor, - $position_next_lt - $cursor - ) - ) - ); - if ($maintain_line_numbers) { - $token->rawPosition($current_line, $current_col); - $current_line += $this->substrCount($html, $nl, $cursor, $position_next_lt - $cursor); - } - $array[] = $token; - $cursor = $position_next_lt + 1; - $inside_tag = true; - continue; - } elseif (!$inside_tag) { - // We are not inside tag but there are no more tags - // If we're already at the end, break - if ($cursor === strlen($html)) { - break; - } - // Create Text of rest of string - $token = new - HTMLPurifier_Token_Text( - $this->parseData( - substr( - $html, - $cursor - ) - ) - ); - if ($maintain_line_numbers) { - $token->rawPosition($current_line, $current_col); - } - $array[] = $token; - break; - } elseif ($inside_tag && $position_next_gt !== false) { - // We are in tag and it is well formed - // Grab the internals of the tag - $strlen_segment = $position_next_gt - $cursor; - - if ($strlen_segment < 1) { - // there's nothing to process! - $token = new HTMLPurifier_Token_Text('<'); - $cursor++; - continue; - } - - $segment = substr($html, $cursor, $strlen_segment); - - if ($segment === false) { - // somehow, we attempted to access beyond the end of - // the string, defense-in-depth, reported by Nate Abele - break; - } - - // Check if it's a comment - if (substr($segment, 0, 3) === '!--') { - // re-determine segment length, looking for --> - $position_comment_end = strpos($html, '-->', $cursor); - if ($position_comment_end === false) { - // uh oh, we have a comment that extends to - // infinity. Can't be helped: set comment - // end position to end of string - if ($e) { - $e->send(E_WARNING, 'Lexer: Unclosed comment'); - } - $position_comment_end = strlen($html); - $end = true; - } else { - $end = false; - } - $strlen_segment = $position_comment_end - $cursor; - $segment = substr($html, $cursor, $strlen_segment); - $token = new - HTMLPurifier_Token_Comment( - substr( - $segment, - 3, - $strlen_segment - 3 - ) - ); - if ($maintain_line_numbers) { - $token->rawPosition($current_line, $current_col); - $current_line += $this->substrCount($html, $nl, $cursor, $strlen_segment); - } - $array[] = $token; - $cursor = $end ? $position_comment_end : $position_comment_end + 3; - $inside_tag = false; - continue; - } - - // Check if it's an end tag - $is_end_tag = (strpos($segment, '/') === 0); - if ($is_end_tag) { - $type = substr($segment, 1); - $token = new HTMLPurifier_Token_End($type); - if ($maintain_line_numbers) { - $token->rawPosition($current_line, $current_col); - $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); - } - $array[] = $token; - $inside_tag = false; - $cursor = $position_next_gt + 1; - continue; - } - - // Check leading character is alnum, if not, we may - // have accidently grabbed an emoticon. Translate into - // text and go our merry way - if (!ctype_alpha($segment[0])) { - // XML: $segment[0] !== '_' && $segment[0] !== ':' - if ($e) { - $e->send(E_NOTICE, 'Lexer: Unescaped lt'); - } - $token = new HTMLPurifier_Token_Text('<'); - if ($maintain_line_numbers) { - $token->rawPosition($current_line, $current_col); - $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); - } - $array[] = $token; - $inside_tag = false; - continue; - } - - // Check if it is explicitly self closing, if so, remove - // trailing slash. Remember, we could have a tag like
      , so - // any later token processing scripts must convert improperly - // classified EmptyTags from StartTags. - $is_self_closing = (strrpos($segment, '/') === $strlen_segment - 1); - if ($is_self_closing) { - $strlen_segment--; - $segment = substr($segment, 0, $strlen_segment); - } - - // Check if there are any attributes - $position_first_space = strcspn($segment, $this->_whitespace); - - if ($position_first_space >= $strlen_segment) { - if ($is_self_closing) { - $token = new HTMLPurifier_Token_Empty($segment); - } else { - $token = new HTMLPurifier_Token_Start($segment); - } - if ($maintain_line_numbers) { - $token->rawPosition($current_line, $current_col); - $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); - } - $array[] = $token; - $inside_tag = false; - $cursor = $position_next_gt + 1; - continue; - } - - // Grab out all the data - $type = substr($segment, 0, $position_first_space); - $attribute_string = - trim( - substr( - $segment, - $position_first_space - ) - ); - if ($attribute_string) { - $attr = $this->parseAttributeString( - $attribute_string, - $config, - $context - ); - } else { - $attr = array(); - } - - if ($is_self_closing) { - $token = new HTMLPurifier_Token_Empty($type, $attr); - } else { - $token = new HTMLPurifier_Token_Start($type, $attr); - } - if ($maintain_line_numbers) { - $token->rawPosition($current_line, $current_col); - $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); - } - $array[] = $token; - $cursor = $position_next_gt + 1; - $inside_tag = false; - continue; - } else { - // inside tag, but there's no ending > sign - if ($e) { - $e->send(E_WARNING, 'Lexer: Missing gt'); - } - $token = new - HTMLPurifier_Token_Text( - '<' . - $this->parseData( - substr($html, $cursor) - ) - ); - if ($maintain_line_numbers) { - $token->rawPosition($current_line, $current_col); - } - // no cursor scroll? Hmm... - $array[] = $token; - break; - } - break; - } - - $context->destroy('CurrentLine'); - $context->destroy('CurrentCol'); - return $array; - } - - /** - * PHP 5.0.x compatible substr_count that implements offset and length - * @param string $haystack - * @param string $needle - * @param int $offset - * @param int $length - * @return int - */ - protected function substrCount($haystack, $needle, $offset, $length) - { - static $oldVersion; - if ($oldVersion === null) { - $oldVersion = version_compare(PHP_VERSION, '5.1', '<'); - } - if ($oldVersion) { - $haystack = substr($haystack, $offset, $length); - return substr_count($haystack, $needle); - } else { - return substr_count($haystack, $needle, $offset, $length); - } - } - - /** - * Takes the inside of an HTML tag and makes an assoc array of attributes. - * - * @param string $string Inside of tag excluding name. - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return array Assoc array of attributes. - */ - public function parseAttributeString($string, $config, $context) - { - $string = (string)$string; // quick typecast - - if ($string == '') { - return array(); - } // no attributes - - $e = false; - if ($config->get('Core.CollectErrors')) { - $e =& $context->get('ErrorCollector'); - } - - // let's see if we can abort as quickly as possible - // one equal sign, no spaces => one attribute - $num_equal = substr_count($string, '='); - $has_space = strpos($string, ' '); - if ($num_equal === 0 && !$has_space) { - // bool attribute - return array($string => $string); - } elseif ($num_equal === 1 && !$has_space) { - // only one attribute - list($key, $quoted_value) = explode('=', $string); - $quoted_value = trim($quoted_value); - if (!$key) { - if ($e) { - $e->send(E_ERROR, 'Lexer: Missing attribute key'); - } - return array(); - } - if (!$quoted_value) { - return array($key => ''); - } - $first_char = @$quoted_value[0]; - $last_char = @$quoted_value[strlen($quoted_value) - 1]; - - $same_quote = ($first_char == $last_char); - $open_quote = ($first_char == '"' || $first_char == "'"); - - if ($same_quote && $open_quote) { - // well behaved - $value = substr($quoted_value, 1, strlen($quoted_value) - 2); - } else { - // not well behaved - if ($open_quote) { - if ($e) { - $e->send(E_ERROR, 'Lexer: Missing end quote'); - } - $value = substr($quoted_value, 1); - } else { - $value = $quoted_value; - } - } - if ($value === false) { - $value = ''; - } - return array($key => $this->parseData($value)); - } - - // setup loop environment - $array = array(); // return assoc array of attributes - $cursor = 0; // current position in string (moves forward) - $size = strlen($string); // size of the string (stays the same) - - // if we have unquoted attributes, the parser expects a terminating - // space, so let's guarantee that there's always a terminating space. - $string .= ' '; - - $old_cursor = -1; - while ($cursor < $size) { - if ($old_cursor >= $cursor) { - throw new Exception("Infinite loop detected"); - } - $old_cursor = $cursor; - - $cursor += ($value = strspn($string, $this->_whitespace, $cursor)); - // grab the key - - $key_begin = $cursor; //we're currently at the start of the key - - // scroll past all characters that are the key (not whitespace or =) - $cursor += strcspn($string, $this->_whitespace . '=', $cursor); - - $key_end = $cursor; // now at the end of the key - - $key = substr($string, $key_begin, $key_end - $key_begin); - - if (!$key) { - if ($e) { - $e->send(E_ERROR, 'Lexer: Missing attribute key'); - } - $cursor += 1 + strcspn($string, $this->_whitespace, $cursor + 1); // prevent infinite loop - continue; // empty key - } - - // scroll past all whitespace - $cursor += strspn($string, $this->_whitespace, $cursor); - - if ($cursor >= $size) { - $array[$key] = $key; - break; - } - - // if the next character is an equal sign, we've got a regular - // pair, otherwise, it's a bool attribute - $first_char = @$string[$cursor]; - - if ($first_char == '=') { - // key="value" - - $cursor++; - $cursor += strspn($string, $this->_whitespace, $cursor); - - if ($cursor === false) { - $array[$key] = ''; - break; - } - - // we might be in front of a quote right now - - $char = @$string[$cursor]; - - if ($char == '"' || $char == "'") { - // it's quoted, end bound is $char - $cursor++; - $value_begin = $cursor; - $cursor = strpos($string, $char, $cursor); - $value_end = $cursor; - } else { - // it's not quoted, end bound is whitespace - $value_begin = $cursor; - $cursor += strcspn($string, $this->_whitespace, $cursor); - $value_end = $cursor; - } - - // we reached a premature end - if ($cursor === false) { - $cursor = $size; - $value_end = $cursor; - } - - $value = substr($string, $value_begin, $value_end - $value_begin); - if ($value === false) { - $value = ''; - } - $array[$key] = $this->parseData($value); - $cursor++; - } else { - // boolattr - if ($key !== '') { - $array[$key] = $key; - } else { - // purely theoretical - if ($e) { - $e->send(E_ERROR, 'Lexer: Missing attribute key'); - } - } - } - } - return $array; - } -} - - - - - -/** - * Concrete comment node class. - */ -class HTMLPurifier_Node_Comment extends HTMLPurifier_Node -{ - /** - * Character data within comment. - * @type string - */ - public $data; - - /** - * @type bool - */ - public $is_whitespace = true; - - /** - * Transparent constructor. - * - * @param string $data String comment data. - * @param int $line - * @param int $col - */ - public function __construct($data, $line = null, $col = null) - { - $this->data = $data; - $this->line = $line; - $this->col = $col; - } - - public function toTokenPair() { - return array(new HTMLPurifier_Token_Comment($this->data, $this->line, $this->col), null); - } -} - - - -/** - * Concrete element node class. - */ -class HTMLPurifier_Node_Element extends HTMLPurifier_Node -{ - /** - * The lower-case name of the tag, like 'a', 'b' or 'blockquote'. - * - * @note Strictly speaking, XML tags are case sensitive, so we shouldn't - * be lower-casing them, but these tokens cater to HTML tags, which are - * insensitive. - * @type string - */ - public $name; - - /** - * Associative array of the node's attributes. - * @type array - */ - public $attr = array(); - - /** - * List of child elements. - * @type array - */ - public $children = array(); - - /** - * Does this use the form or the form, i.e. - * is it a pair of start/end tokens or an empty token. - * @bool - */ - public $empty = false; - - public $endCol = null, $endLine = null, $endArmor = array(); - - public function __construct($name, $attr = array(), $line = null, $col = null, $armor = array()) { - $this->name = $name; - $this->attr = $attr; - $this->line = $line; - $this->col = $col; - $this->armor = $armor; - } - - public function toTokenPair() { - // XXX inefficiency here, normalization is not necessary - if ($this->empty) { - return array(new HTMLPurifier_Token_Empty($this->name, $this->attr, $this->line, $this->col, $this->armor), null); - } else { - $start = new HTMLPurifier_Token_Start($this->name, $this->attr, $this->line, $this->col, $this->armor); - $end = new HTMLPurifier_Token_End($this->name, array(), $this->endLine, $this->endCol, $this->endArmor); - //$end->start = $start; - return array($start, $end); - } - } -} - - - - -/** - * Concrete text token class. - * - * Text tokens comprise of regular parsed character data (PCDATA) and raw - * character data (from the CDATA sections). Internally, their - * data is parsed with all entities expanded. Surprisingly, the text token - * does have a "tag name" called #PCDATA, which is how the DTD represents it - * in permissible child nodes. - */ -class HTMLPurifier_Node_Text extends HTMLPurifier_Node -{ - - /** - * PCDATA tag name compatible with DTD, see - * HTMLPurifier_ChildDef_Custom for details. - * @type string - */ - public $name = '#PCDATA'; - - /** - * @type string - */ - public $data; - /**< Parsed character data of text. */ - - /** - * @type bool - */ - public $is_whitespace; - - /**< Bool indicating if node is whitespace. */ - - /** - * Constructor, accepts data and determines if it is whitespace. - * @param string $data String parsed character data. - * @param int $line - * @param int $col - */ - public function __construct($data, $is_whitespace, $line = null, $col = null) - { - $this->data = $data; - $this->is_whitespace = $is_whitespace; - $this->line = $line; - $this->col = $col; - } - - public function toTokenPair() { - return array(new HTMLPurifier_Token_Text($this->data, $this->line, $this->col), null); - } -} - - - - - -/** - * Composite strategy that runs multiple strategies on tokens. - */ -abstract class HTMLPurifier_Strategy_Composite extends HTMLPurifier_Strategy -{ - - /** - * List of strategies to run tokens through. - * @type HTMLPurifier_Strategy[] - */ - protected $strategies = array(); - - /** - * @param HTMLPurifier_Token[] $tokens - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return HTMLPurifier_Token[] - */ - public function execute($tokens, $config, $context) - { - foreach ($this->strategies as $strategy) { - $tokens = $strategy->execute($tokens, $config, $context); - } - return $tokens; - } -} - - - - - -/** - * Core strategy composed of the big four strategies. - */ -class HTMLPurifier_Strategy_Core extends HTMLPurifier_Strategy_Composite -{ - public function __construct() - { - $this->strategies[] = new HTMLPurifier_Strategy_RemoveForeignElements(); - $this->strategies[] = new HTMLPurifier_Strategy_MakeWellFormed(); - $this->strategies[] = new HTMLPurifier_Strategy_FixNesting(); - $this->strategies[] = new HTMLPurifier_Strategy_ValidateAttributes(); - } -} - - - - - -/** - * Takes a well formed list of tokens and fixes their nesting. - * - * HTML elements dictate which elements are allowed to be their children, - * for example, you can't have a p tag in a span tag. Other elements have - * much more rigorous definitions: tables, for instance, require a specific - * order for their elements. There are also constraints not expressible by - * document type definitions, such as the chameleon nature of ins/del - * tags and global child exclusions. - * - * The first major objective of this strategy is to iterate through all - * the nodes and determine whether or not their children conform to the - * element's definition. If they do not, the child definition may - * optionally supply an amended list of elements that is valid or - * require that the entire node be deleted (and the previous node - * rescanned). - * - * The second objective is to ensure that explicitly excluded elements of - * an element do not appear in its children. Code that accomplishes this - * task is pervasive through the strategy, though the two are distinct tasks - * and could, theoretically, be seperated (although it's not recommended). - * - * @note Whether or not unrecognized children are silently dropped or - * translated into text depends on the child definitions. - * - * @todo Enable nodes to be bubbled out of the structure. This is - * easier with our new algorithm. - */ - -class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy -{ - - /** - * @param HTMLPurifier_Token[] $tokens - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return array|HTMLPurifier_Token[] - */ - public function execute($tokens, $config, $context) - { - - //####################################################################// - // Pre-processing - - // O(n) pass to convert to a tree, so that we can efficiently - // refer to substrings - $top_node = HTMLPurifier_Arborize::arborize($tokens, $config, $context); - - // get a copy of the HTML definition - $definition = $config->getHTMLDefinition(); - - $excludes_enabled = !$config->get('Core.DisableExcludes'); - - // setup the context variable 'IsInline', for chameleon processing - // is 'false' when we are not inline, 'true' when it must always - // be inline, and an integer when it is inline for a certain - // branch of the document tree - $is_inline = $definition->info_parent_def->descendants_are_inline; - $context->register('IsInline', $is_inline); - - // setup error collector - $e =& $context->get('ErrorCollector', true); - - //####################################################################// - // Loop initialization - - // stack that contains all elements that are excluded - // it is organized by parent elements, similar to $stack, - // but it is only populated when an element with exclusions is - // processed, i.e. there won't be empty exclusions. - $exclude_stack = array($definition->info_parent_def->excludes); - - // variable that contains the start token while we are processing - // nodes. This enables error reporting to do its job - $node = $top_node; - // dummy token - list($token, $d) = $node->toTokenPair(); - $context->register('CurrentNode', $node); - $context->register('CurrentToken', $token); - - //####################################################################// - // Loop - - // We need to implement a post-order traversal iteratively, to - // avoid running into stack space limits. This is pretty tricky - // to reason about, so we just manually stack-ify the recursive - // variant: - // - // function f($node) { - // foreach ($node->children as $child) { - // f($child); - // } - // validate($node); - // } - // - // Thus, we will represent a stack frame as array($node, - // $is_inline, stack of children) - // e.g. array_reverse($node->children) - already processed - // children. - - $parent_def = $definition->info_parent_def; - $stack = array( - array($top_node, - $parent_def->descendants_are_inline, - $parent_def->excludes, // exclusions - 0) - ); - - while (!empty($stack)) { - list($node, $is_inline, $excludes, $ix) = array_pop($stack); - // recursive call - $go = false; - $def = empty($stack) ? $definition->info_parent_def : $definition->info[$node->name]; - while (isset($node->children[$ix])) { - $child = $node->children[$ix++]; - if ($child instanceof HTMLPurifier_Node_Element) { - $go = true; - $stack[] = array($node, $is_inline, $excludes, $ix); - $stack[] = array($child, - // ToDo: I don't think it matters if it's def or - // child_def, but double check this... - $is_inline || $def->descendants_are_inline, - empty($def->excludes) ? $excludes - : array_merge($excludes, $def->excludes), - 0); - break; - } - }; - if ($go) continue; - list($token, $d) = $node->toTokenPair(); - // base case - if ($excludes_enabled && isset($excludes[$node->name])) { - $node->dead = true; - if ($e) $e->send(E_ERROR, 'Strategy_FixNesting: Node excluded'); - } else { - // XXX I suppose it would be slightly more efficient to - // avoid the allocation here and have children - // strategies handle it - $children = array(); - foreach ($node->children as $child) { - if (!$child->dead) $children[] = $child; - } - $result = $def->child->validateChildren($children, $config, $context); - if ($result === true) { - // nop - $node->children = $children; - } elseif ($result === false) { - $node->dead = true; - if ($e) $e->send(E_ERROR, 'Strategy_FixNesting: Node removed'); - } else { - $node->children = $result; - if ($e) { - // XXX This will miss mutations of internal nodes. Perhaps defer to the child validators - if (empty($result) && !empty($children)) { - $e->send(E_ERROR, 'Strategy_FixNesting: Node contents removed'); - } else if ($result != $children) { - $e->send(E_WARNING, 'Strategy_FixNesting: Node reorganized'); - } - } - } - } - } - - //####################################################################// - // Post-processing - - // remove context variables - $context->destroy('IsInline'); - $context->destroy('CurrentNode'); - $context->destroy('CurrentToken'); - - //####################################################################// - // Return - - return HTMLPurifier_Arborize::flatten($node, $config, $context); - } -} - - - - - -/** - * Takes tokens makes them well-formed (balance end tags, etc.) - * - * Specification of the armor attributes this strategy uses: - * - * - MakeWellFormed_TagClosedError: This armor field is used to - * suppress tag closed errors for certain tokens [TagClosedSuppress], - * in particular, if a tag was generated automatically by HTML - * Purifier, we may rely on our infrastructure to close it for us - * and shouldn't report an error to the user [TagClosedAuto]. - */ -class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy -{ - - /** - * Array stream of tokens being processed. - * @type HTMLPurifier_Token[] - */ - protected $tokens; - - /** - * Current token. - * @type HTMLPurifier_Token - */ - protected $token; - - /** - * Zipper managing the true state. - * @type HTMLPurifier_Zipper - */ - protected $zipper; - - /** - * Current nesting of elements. - * @type array - */ - protected $stack; - - /** - * Injectors active in this stream processing. - * @type HTMLPurifier_Injector[] - */ - protected $injectors; - - /** - * Current instance of HTMLPurifier_Config. - * @type HTMLPurifier_Config - */ - protected $config; - - /** - * Current instance of HTMLPurifier_Context. - * @type HTMLPurifier_Context - */ - protected $context; - - /** - * @param HTMLPurifier_Token[] $tokens - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return HTMLPurifier_Token[] - * @throws HTMLPurifier_Exception - */ - public function execute($tokens, $config, $context) - { - $definition = $config->getHTMLDefinition(); - - // local variables - $generator = new HTMLPurifier_Generator($config, $context); - $escape_invalid_tags = $config->get('Core.EscapeInvalidTags'); - // used for autoclose early abortion - $global_parent_allowed_elements = $definition->info_parent_def->child->getAllowedElements($config); - $e = $context->get('ErrorCollector', true); - $i = false; // injector index - list($zipper, $token) = HTMLPurifier_Zipper::fromArray($tokens); - if ($token === NULL) { - return array(); - } - $reprocess = false; // whether or not to reprocess the same token - $stack = array(); - - // member variables - $this->stack =& $stack; - $this->tokens =& $tokens; - $this->token =& $token; - $this->zipper =& $zipper; - $this->config = $config; - $this->context = $context; - - // context variables - $context->register('CurrentNesting', $stack); - $context->register('InputZipper', $zipper); - $context->register('CurrentToken', $token); - - // -- begin INJECTOR -- - - $this->injectors = array(); - - $injectors = $config->getBatch('AutoFormat'); - $def_injectors = $definition->info_injector; - $custom_injectors = $injectors['Custom']; - unset($injectors['Custom']); // special case - foreach ($injectors as $injector => $b) { - // XXX: Fix with a legitimate lookup table of enabled filters - if (strpos($injector, '.') !== false) { - continue; - } - $injector = "HTMLPurifier_Injector_$injector"; - if (!$b) { - continue; - } - $this->injectors[] = new $injector; - } - foreach ($def_injectors as $injector) { - // assumed to be objects - $this->injectors[] = $injector; - } - foreach ($custom_injectors as $injector) { - if (!$injector) { - continue; - } - if (is_string($injector)) { - $injector = "HTMLPurifier_Injector_$injector"; - $injector = new $injector; - } - $this->injectors[] = $injector; - } - - // give the injectors references to the definition and context - // variables for performance reasons - foreach ($this->injectors as $ix => $injector) { - $error = $injector->prepare($config, $context); - if (!$error) { - continue; - } - array_splice($this->injectors, $ix, 1); // rm the injector - trigger_error("Cannot enable {$injector->name} injector because $error is not allowed", E_USER_WARNING); - } - - // -- end INJECTOR -- - - // a note on reprocessing: - // In order to reduce code duplication, whenever some code needs - // to make HTML changes in order to make things "correct", the - // new HTML gets sent through the purifier, regardless of its - // status. This means that if we add a start token, because it - // was totally necessary, we don't have to update nesting; we just - // punt ($reprocess = true; continue;) and it does that for us. - - // isset is in loop because $tokens size changes during loop exec - for (;; - // only increment if we don't need to reprocess - $reprocess ? $reprocess = false : $token = $zipper->next($token)) { - - // check for a rewind - if (is_int($i)) { - // possibility: disable rewinding if the current token has a - // rewind set on it already. This would offer protection from - // infinite loop, but might hinder some advanced rewinding. - $rewind_offset = $this->injectors[$i]->getRewindOffset(); - if (is_int($rewind_offset)) { - for ($j = 0; $j < $rewind_offset; $j++) { - if (empty($zipper->front)) break; - $token = $zipper->prev($token); - // indicate that other injectors should not process this token, - // but we need to reprocess it - unset($token->skip[$i]); - $token->rewind = $i; - if ($token instanceof HTMLPurifier_Token_Start) { - array_pop($this->stack); - } elseif ($token instanceof HTMLPurifier_Token_End) { - $this->stack[] = $token->start; - } - } - } - $i = false; - } - - // handle case of document end - if ($token === NULL) { - // kill processing if stack is empty - if (empty($this->stack)) { - break; - } - - // peek - $top_nesting = array_pop($this->stack); - $this->stack[] = $top_nesting; - - // send error [TagClosedSuppress] - if ($e && !isset($top_nesting->armor['MakeWellFormed_TagClosedError'])) { - $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by document end', $top_nesting); - } - - // append, don't splice, since this is the end - $token = new HTMLPurifier_Token_End($top_nesting->name); - - // punt! - $reprocess = true; - continue; - } - - //echo '
      '; printZipper($zipper, $token);//printTokens($this->stack); - //flush(); - - // quick-check: if it's not a tag, no need to process - if (empty($token->is_tag)) { - if ($token instanceof HTMLPurifier_Token_Text) { - foreach ($this->injectors as $i => $injector) { - if (isset($token->skip[$i])) { - continue; - } - if ($token->rewind !== null && $token->rewind !== $i) { - continue; - } - // XXX fuckup - $r = $token; - $injector->handleText($r); - $token = $this->processToken($r, $i); - $reprocess = true; - break; - } - } - // another possibility is a comment - continue; - } - - if (isset($definition->info[$token->name])) { - $type = $definition->info[$token->name]->child->type; - } else { - $type = false; // Type is unknown, treat accordingly - } - - // quick tag checks: anything that's *not* an end tag - $ok = false; - if ($type === 'empty' && $token instanceof HTMLPurifier_Token_Start) { - // claims to be a start tag but is empty - $token = new HTMLPurifier_Token_Empty( - $token->name, - $token->attr, - $token->line, - $token->col, - $token->armor - ); - $ok = true; - } elseif ($type && $type !== 'empty' && $token instanceof HTMLPurifier_Token_Empty) { - // claims to be empty but really is a start tag - // NB: this assignment is required - $old_token = $token; - $token = new HTMLPurifier_Token_End($token->name); - $token = $this->insertBefore( - new HTMLPurifier_Token_Start($old_token->name, $old_token->attr, $old_token->line, $old_token->col, $old_token->armor) - ); - // punt (since we had to modify the input stream in a non-trivial way) - $reprocess = true; - continue; - } elseif ($token instanceof HTMLPurifier_Token_Empty) { - // real empty token - $ok = true; - } elseif ($token instanceof HTMLPurifier_Token_Start) { - // start tag - - // ...unless they also have to close their parent - if (!empty($this->stack)) { - - // Performance note: you might think that it's rather - // inefficient, recalculating the autoclose information - // for every tag that a token closes (since when we - // do an autoclose, we push a new token into the - // stream and then /process/ that, before - // re-processing this token.) But this is - // necessary, because an injector can make an - // arbitrary transformations to the autoclosing - // tokens we introduce, so things may have changed - // in the meantime. Also, doing the inefficient thing is - // "easy" to reason about (for certain perverse definitions - // of "easy") - - $parent = array_pop($this->stack); - $this->stack[] = $parent; - - $parent_def = null; - $parent_elements = null; - $autoclose = false; - if (isset($definition->info[$parent->name])) { - $parent_def = $definition->info[$parent->name]; - $parent_elements = $parent_def->child->getAllowedElements($config); - $autoclose = !isset($parent_elements[$token->name]); - } - - if ($autoclose && $definition->info[$token->name]->wrap) { - // Check if an element can be wrapped by another - // element to make it valid in a context (for - // example,
          needs a
        • in between) - $wrapname = $definition->info[$token->name]->wrap; - $wrapdef = $definition->info[$wrapname]; - $elements = $wrapdef->child->getAllowedElements($config); - if (isset($elements[$token->name]) && isset($parent_elements[$wrapname])) { - $newtoken = new HTMLPurifier_Token_Start($wrapname); - $token = $this->insertBefore($newtoken); - $reprocess = true; - continue; - } - } - - $carryover = false; - if ($autoclose && $parent_def->formatting) { - $carryover = true; - } - - if ($autoclose) { - // check if this autoclose is doomed to fail - // (this rechecks $parent, which his harmless) - $autoclose_ok = isset($global_parent_allowed_elements[$token->name]); - if (!$autoclose_ok) { - foreach ($this->stack as $ancestor) { - $elements = $definition->info[$ancestor->name]->child->getAllowedElements($config); - if (isset($elements[$token->name])) { - $autoclose_ok = true; - break; - } - if ($definition->info[$token->name]->wrap) { - $wrapname = $definition->info[$token->name]->wrap; - $wrapdef = $definition->info[$wrapname]; - $wrap_elements = $wrapdef->child->getAllowedElements($config); - if (isset($wrap_elements[$token->name]) && isset($elements[$wrapname])) { - $autoclose_ok = true; - break; - } - } - } - } - if ($autoclose_ok) { - // errors need to be updated - $new_token = new HTMLPurifier_Token_End($parent->name); - $new_token->start = $parent; - // [TagClosedSuppress] - if ($e && !isset($parent->armor['MakeWellFormed_TagClosedError'])) { - if (!$carryover) { - $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag auto closed', $parent); - } else { - $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag carryover', $parent); - } - } - if ($carryover) { - $element = clone $parent; - // [TagClosedAuto] - $element->armor['MakeWellFormed_TagClosedError'] = true; - $element->carryover = true; - $token = $this->processToken(array($new_token, $token, $element)); - } else { - $token = $this->insertBefore($new_token); - } - } else { - $token = $this->remove(); - } - $reprocess = true; - continue; - } - - } - $ok = true; - } - - if ($ok) { - foreach ($this->injectors as $i => $injector) { - if (isset($token->skip[$i])) { - continue; - } - if ($token->rewind !== null && $token->rewind !== $i) { - continue; - } - $r = $token; - $injector->handleElement($r); - $token = $this->processToken($r, $i); - $reprocess = true; - break; - } - if (!$reprocess) { - // ah, nothing interesting happened; do normal processing - if ($token instanceof HTMLPurifier_Token_Start) { - $this->stack[] = $token; - } elseif ($token instanceof HTMLPurifier_Token_End) { - throw new HTMLPurifier_Exception( - 'Improper handling of end tag in start code; possible error in MakeWellFormed' - ); - } - } - continue; - } - - // sanity check: we should be dealing with a closing tag - if (!$token instanceof HTMLPurifier_Token_End) { - throw new HTMLPurifier_Exception('Unaccounted for tag token in input stream, bug in HTML Purifier'); - } - - // make sure that we have something open - if (empty($this->stack)) { - if ($escape_invalid_tags) { - if ($e) { - $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag to text'); - } - $token = new HTMLPurifier_Token_Text($generator->generateFromToken($token)); - } else { - if ($e) { - $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag removed'); - } - $token = $this->remove(); - } - $reprocess = true; - continue; - } - - // first, check for the simplest case: everything closes neatly. - // Eventually, everything passes through here; if there are problems - // we modify the input stream accordingly and then punt, so that - // the tokens get processed again. - $current_parent = array_pop($this->stack); - if ($current_parent->name == $token->name) { - $token->start = $current_parent; - foreach ($this->injectors as $i => $injector) { - if (isset($token->skip[$i])) { - continue; - } - if ($token->rewind !== null && $token->rewind !== $i) { - continue; - } - $r = $token; - $injector->handleEnd($r); - $token = $this->processToken($r, $i); - $this->stack[] = $current_parent; - $reprocess = true; - break; - } - continue; - } - - // okay, so we're trying to close the wrong tag - - // undo the pop previous pop - $this->stack[] = $current_parent; - - // scroll back the entire nest, trying to find our tag. - // (feature could be to specify how far you'd like to go) - $size = count($this->stack); - // -2 because -1 is the last element, but we already checked that - $skipped_tags = false; - for ($j = $size - 2; $j >= 0; $j--) { - if ($this->stack[$j]->name == $token->name) { - $skipped_tags = array_slice($this->stack, $j); - break; - } - } - - // we didn't find the tag, so remove - if ($skipped_tags === false) { - if ($escape_invalid_tags) { - if ($e) { - $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag to text'); - } - $token = new HTMLPurifier_Token_Text($generator->generateFromToken($token)); - } else { - if ($e) { - $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag removed'); - } - $token = $this->remove(); - } - $reprocess = true; - continue; - } - - // do errors, in REVERSE $j order: a,b,c with - $c = count($skipped_tags); - if ($e) { - for ($j = $c - 1; $j > 0; $j--) { - // notice we exclude $j == 0, i.e. the current ending tag, from - // the errors... [TagClosedSuppress] - if (!isset($skipped_tags[$j]->armor['MakeWellFormed_TagClosedError'])) { - $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by element end', $skipped_tags[$j]); - } - } - } - - // insert tags, in FORWARD $j order: c,b,a with - $replace = array($token); - for ($j = 1; $j < $c; $j++) { - // ...as well as from the insertions - $new_token = new HTMLPurifier_Token_End($skipped_tags[$j]->name); - $new_token->start = $skipped_tags[$j]; - array_unshift($replace, $new_token); - if (isset($definition->info[$new_token->name]) && $definition->info[$new_token->name]->formatting) { - // [TagClosedAuto] - $element = clone $skipped_tags[$j]; - $element->carryover = true; - $element->armor['MakeWellFormed_TagClosedError'] = true; - $replace[] = $element; - } - } - $token = $this->processToken($replace); - $reprocess = true; - continue; - } - - $context->destroy('CurrentToken'); - $context->destroy('CurrentNesting'); - $context->destroy('InputZipper'); - - unset($this->injectors, $this->stack, $this->tokens); - return $zipper->toArray($token); - } - - /** - * Processes arbitrary token values for complicated substitution patterns. - * In general: - * - * If $token is an array, it is a list of tokens to substitute for the - * current token. These tokens then get individually processed. If there - * is a leading integer in the list, that integer determines how many - * tokens from the stream should be removed. - * - * If $token is a regular token, it is swapped with the current token. - * - * If $token is false, the current token is deleted. - * - * If $token is an integer, that number of tokens (with the first token - * being the current one) will be deleted. - * - * @param HTMLPurifier_Token|array|int|bool $token Token substitution value - * @param HTMLPurifier_Injector|int $injector Injector that performed the substitution; default is if - * this is not an injector related operation. - * @throws HTMLPurifier_Exception - */ - protected function processToken($token, $injector = -1) - { - // normalize forms of token - if (is_object($token)) { - $token = array(1, $token); - } - if (is_int($token)) { - $token = array($token); - } - if ($token === false) { - $token = array(1); - } - if (!is_array($token)) { - throw new HTMLPurifier_Exception('Invalid token type from injector'); - } - if (!is_int($token[0])) { - array_unshift($token, 1); - } - if ($token[0] === 0) { - throw new HTMLPurifier_Exception('Deleting zero tokens is not valid'); - } - - // $token is now an array with the following form: - // array(number nodes to delete, new node 1, new node 2, ...) - - $delete = array_shift($token); - list($old, $r) = $this->zipper->splice($this->token, $delete, $token); - - if ($injector > -1) { - // determine appropriate skips - $oldskip = isset($old[0]) ? $old[0]->skip : array(); - foreach ($token as $object) { - $object->skip = $oldskip; - $object->skip[$injector] = true; - } - } - - return $r; - - } - - /** - * Inserts a token before the current token. Cursor now points to - * this token. You must reprocess after this. - * @param HTMLPurifier_Token $token - */ - private function insertBefore($token) - { - // NB not $this->zipper->insertBefore(), due to positioning - // differences - $splice = $this->zipper->splice($this->token, 0, array($token)); - - return $splice[1]; - } - - /** - * Removes current token. Cursor now points to new token occupying previously - * occupied space. You must reprocess after this. - */ - private function remove() - { - return $this->zipper->delete(); - } -} - - - - - -/** - * Removes all unrecognized tags from the list of tokens. - * - * This strategy iterates through all the tokens and removes unrecognized - * tokens. If a token is not recognized but a TagTransform is defined for - * that element, the element will be transformed accordingly. - */ - -class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy -{ - - /** - * @param HTMLPurifier_Token[] $tokens - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return array|HTMLPurifier_Token[] - */ - public function execute($tokens, $config, $context) - { - $definition = $config->getHTMLDefinition(); - $generator = new HTMLPurifier_Generator($config, $context); - $result = array(); - - $escape_invalid_tags = $config->get('Core.EscapeInvalidTags'); - $remove_invalid_img = $config->get('Core.RemoveInvalidImg'); - - // currently only used to determine if comments should be kept - $trusted = $config->get('HTML.Trusted'); - $comment_lookup = $config->get('HTML.AllowedComments'); - $comment_regexp = $config->get('HTML.AllowedCommentsRegexp'); - $check_comments = $comment_lookup !== array() || $comment_regexp !== null; - - $remove_script_contents = $config->get('Core.RemoveScriptContents'); - $hidden_elements = $config->get('Core.HiddenElements'); - - // remove script contents compatibility - if ($remove_script_contents === true) { - $hidden_elements['script'] = true; - } elseif ($remove_script_contents === false && isset($hidden_elements['script'])) { - unset($hidden_elements['script']); - } - - $attr_validator = new HTMLPurifier_AttrValidator(); - - // removes tokens until it reaches a closing tag with its value - $remove_until = false; - - // converts comments into text tokens when this is equal to a tag name - $textify_comments = false; - - $token = false; - $context->register('CurrentToken', $token); - - $e = false; - if ($config->get('Core.CollectErrors')) { - $e =& $context->get('ErrorCollector'); - } - - foreach ($tokens as $token) { - if ($remove_until) { - if (empty($token->is_tag) || $token->name !== $remove_until) { - continue; - } - } - if (!empty($token->is_tag)) { - // DEFINITION CALL - - // before any processing, try to transform the element - if (isset($definition->info_tag_transform[$token->name])) { - $original_name = $token->name; - // there is a transformation for this tag - // DEFINITION CALL - $token = $definition-> - info_tag_transform[$token->name]->transform($token, $config, $context); - if ($e) { - $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Tag transform', $original_name); - } - } - - if (isset($definition->info[$token->name])) { - // mostly everything's good, but - // we need to make sure required attributes are in order - if (($token instanceof HTMLPurifier_Token_Start || $token instanceof HTMLPurifier_Token_Empty) && - $definition->info[$token->name]->required_attr && - ($token->name != 'img' || $remove_invalid_img) // ensure config option still works - ) { - $attr_validator->validateToken($token, $config, $context); - $ok = true; - foreach ($definition->info[$token->name]->required_attr as $name) { - if (!isset($token->attr[$name])) { - $ok = false; - break; - } - } - if (!$ok) { - if ($e) { - $e->send( - E_ERROR, - 'Strategy_RemoveForeignElements: Missing required attribute', - $name - ); - } - continue; - } - $token->armor['ValidateAttributes'] = true; - } - - if (isset($hidden_elements[$token->name]) && $token instanceof HTMLPurifier_Token_Start) { - $textify_comments = $token->name; - } elseif ($token->name === $textify_comments && $token instanceof HTMLPurifier_Token_End) { - $textify_comments = false; - } - - } elseif ($escape_invalid_tags) { - // invalid tag, generate HTML representation and insert in - if ($e) { - $e->send(E_WARNING, 'Strategy_RemoveForeignElements: Foreign element to text'); - } - $token = new HTMLPurifier_Token_Text( - $generator->generateFromToken($token) - ); - } else { - // check if we need to destroy all of the tag's children - // CAN BE GENERICIZED - if (isset($hidden_elements[$token->name])) { - if ($token instanceof HTMLPurifier_Token_Start) { - $remove_until = $token->name; - } elseif ($token instanceof HTMLPurifier_Token_Empty) { - // do nothing: we're still looking - } else { - $remove_until = false; - } - if ($e) { - $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Foreign meta element removed'); - } - } else { - if ($e) { - $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Foreign element removed'); - } - } - continue; - } - } elseif ($token instanceof HTMLPurifier_Token_Comment) { - // textify comments in script tags when they are allowed - if ($textify_comments !== false) { - $data = $token->data; - $token = new HTMLPurifier_Token_Text($data); - } elseif ($trusted || $check_comments) { - // always cleanup comments - $trailing_hyphen = false; - if ($e) { - // perform check whether or not there's a trailing hyphen - if (substr($token->data, -1) == '-') { - $trailing_hyphen = true; - } - } - $token->data = rtrim($token->data, '-'); - $found_double_hyphen = false; - while (strpos($token->data, '--') !== false) { - $found_double_hyphen = true; - $token->data = str_replace('--', '-', $token->data); - } - if ($trusted || !empty($comment_lookup[trim($token->data)]) || - ($comment_regexp !== null && preg_match($comment_regexp, trim($token->data)))) { - // OK good - if ($e) { - if ($trailing_hyphen) { - $e->send( - E_NOTICE, - 'Strategy_RemoveForeignElements: Trailing hyphen in comment removed' - ); - } - if ($found_double_hyphen) { - $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Hyphens in comment collapsed'); - } - } - } else { - if ($e) { - $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Comment removed'); - } - continue; - } - } else { - // strip comments - if ($e) { - $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Comment removed'); - } - continue; - } - } elseif ($token instanceof HTMLPurifier_Token_Text) { - } else { - continue; - } - $result[] = $token; - } - if ($remove_until && $e) { - // we removed tokens until the end, throw error - $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Token removed to end', $remove_until); - } - $context->destroy('CurrentToken'); - return $result; - } -} - - - - - -/** - * Validate all attributes in the tokens. - */ - -class HTMLPurifier_Strategy_ValidateAttributes extends HTMLPurifier_Strategy -{ - - /** - * @param HTMLPurifier_Token[] $tokens - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return HTMLPurifier_Token[] - */ - public function execute($tokens, $config, $context) - { - // setup validator - $validator = new HTMLPurifier_AttrValidator(); - - $token = false; - $context->register('CurrentToken', $token); - - foreach ($tokens as $key => $token) { - - // only process tokens that have attributes, - // namely start and empty tags - if (!$token instanceof HTMLPurifier_Token_Start && !$token instanceof HTMLPurifier_Token_Empty) { - continue; - } - - // skip tokens that are armored - if (!empty($token->armor['ValidateAttributes'])) { - continue; - } - - // note that we have no facilities here for removing tokens - $validator->validateToken($token, $config, $context); - } - $context->destroy('CurrentToken'); - return $tokens; - } -} - - - - - -/** - * Transforms FONT tags to the proper form (SPAN with CSS styling) - * - * This transformation takes the three proprietary attributes of FONT and - * transforms them into their corresponding CSS attributes. These are color, - * face, and size. - * - * @note Size is an interesting case because it doesn't map cleanly to CSS. - * Thanks to - * http://style.cleverchimp.com/font_size_intervals/altintervals.html - * for reasonable mappings. - * @warning This doesn't work completely correctly; specifically, this - * TagTransform operates before well-formedness is enforced, so - * the "active formatting elements" algorithm doesn't get applied. - */ -class HTMLPurifier_TagTransform_Font extends HTMLPurifier_TagTransform -{ - /** - * @type string - */ - public $transform_to = 'span'; - - /** - * @type array - */ - protected $_size_lookup = array( - '0' => 'xx-small', - '1' => 'xx-small', - '2' => 'small', - '3' => 'medium', - '4' => 'large', - '5' => 'x-large', - '6' => 'xx-large', - '7' => '300%', - '-1' => 'smaller', - '-2' => '60%', - '+1' => 'larger', - '+2' => '150%', - '+3' => '200%', - '+4' => '300%' - ); - - /** - * @param HTMLPurifier_Token_Tag $tag - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return HTMLPurifier_Token_End|string - */ - public function transform($tag, $config, $context) - { - if ($tag instanceof HTMLPurifier_Token_End) { - $new_tag = clone $tag; - $new_tag->name = $this->transform_to; - return $new_tag; - } - - $attr = $tag->attr; - $prepend_style = ''; - - // handle color transform - if (isset($attr['color'])) { - $prepend_style .= 'color:' . $attr['color'] . ';'; - unset($attr['color']); - } - - // handle face transform - if (isset($attr['face'])) { - $prepend_style .= 'font-family:' . $attr['face'] . ';'; - unset($attr['face']); - } - - // handle size transform - if (isset($attr['size'])) { - // normalize large numbers - if ($attr['size'] !== '') { - if ($attr['size']{0} == '+' || $attr['size']{0} == '-') { - $size = (int)$attr['size']; - if ($size < -2) { - $attr['size'] = '-2'; - } - if ($size > 4) { - $attr['size'] = '+4'; - } - } else { - $size = (int)$attr['size']; - if ($size > 7) { - $attr['size'] = '7'; - } - } - } - if (isset($this->_size_lookup[$attr['size']])) { - $prepend_style .= 'font-size:' . - $this->_size_lookup[$attr['size']] . ';'; - } - unset($attr['size']); - } - - if ($prepend_style) { - $attr['style'] = isset($attr['style']) ? - $prepend_style . $attr['style'] : - $prepend_style; - } - - $new_tag = clone $tag; - $new_tag->name = $this->transform_to; - $new_tag->attr = $attr; - - return $new_tag; - } -} - - - - - -/** - * Simple transformation, just change tag name to something else, - * and possibly add some styling. This will cover most of the deprecated - * tag cases. - */ -class HTMLPurifier_TagTransform_Simple extends HTMLPurifier_TagTransform -{ - /** - * @type string - */ - protected $style; - - /** - * @param string $transform_to Tag name to transform to. - * @param string $style CSS style to add to the tag - */ - public function __construct($transform_to, $style = null) - { - $this->transform_to = $transform_to; - $this->style = $style; - } - - /** - * @param HTMLPurifier_Token_Tag $tag - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return string - */ - public function transform($tag, $config, $context) - { - $new_tag = clone $tag; - $new_tag->name = $this->transform_to; - if (!is_null($this->style) && - ($new_tag instanceof HTMLPurifier_Token_Start || $new_tag instanceof HTMLPurifier_Token_Empty) - ) { - $this->prependCSS($new_tag->attr, $this->style); - } - return $new_tag; - } -} - - - - - -/** - * Concrete comment token class. Generally will be ignored. - */ -class HTMLPurifier_Token_Comment extends HTMLPurifier_Token -{ - /** - * Character data within comment. - * @type string - */ - public $data; - - /** - * @type bool - */ - public $is_whitespace = true; - - /** - * Transparent constructor. - * - * @param string $data String comment data. - * @param int $line - * @param int $col - */ - public function __construct($data, $line = null, $col = null) - { - $this->data = $data; - $this->line = $line; - $this->col = $col; - } - - public function toNode() { - return new HTMLPurifier_Node_Comment($this->data, $this->line, $this->col); - } -} - - - - - -/** - * Abstract class of a tag token (start, end or empty), and its behavior. - */ -abstract class HTMLPurifier_Token_Tag extends HTMLPurifier_Token -{ - /** - * Static bool marker that indicates the class is a tag. - * - * This allows us to check objects with !empty($obj->is_tag) - * without having to use a function call is_a(). - * @type bool - */ - public $is_tag = true; - - /** - * The lower-case name of the tag, like 'a', 'b' or 'blockquote'. - * - * @note Strictly speaking, XML tags are case sensitive, so we shouldn't - * be lower-casing them, but these tokens cater to HTML tags, which are - * insensitive. - * @type string - */ - public $name; - - /** - * Associative array of the tag's attributes. - * @type array - */ - public $attr = array(); - - /** - * Non-overloaded constructor, which lower-cases passed tag name. - * - * @param string $name String name. - * @param array $attr Associative array of attributes. - * @param int $line - * @param int $col - * @param array $armor - */ - public function __construct($name, $attr = array(), $line = null, $col = null, $armor = array()) - { - $this->name = ctype_lower($name) ? $name : strtolower($name); - foreach ($attr as $key => $value) { - // normalization only necessary when key is not lowercase - if (!ctype_lower($key)) { - $new_key = strtolower($key); - if (!isset($attr[$new_key])) { - $attr[$new_key] = $attr[$key]; - } - if ($new_key !== $key) { - unset($attr[$key]); - } - } - } - $this->attr = $attr; - $this->line = $line; - $this->col = $col; - $this->armor = $armor; - } - - public function toNode() { - return new HTMLPurifier_Node_Element($this->name, $this->attr, $this->line, $this->col, $this->armor); - } -} - - - - - -/** - * Concrete empty token class. - */ -class HTMLPurifier_Token_Empty extends HTMLPurifier_Token_Tag -{ - public function toNode() { - $n = parent::toNode(); - $n->empty = true; - return $n; - } -} - - - - - -/** - * Concrete end token class. - * - * @warning This class accepts attributes even though end tags cannot. This - * is for optimization reasons, as under normal circumstances, the Lexers - * do not pass attributes. - */ -class HTMLPurifier_Token_End extends HTMLPurifier_Token_Tag -{ - /** - * Token that started this node. - * Added by MakeWellFormed. Please do not edit this! - * @type HTMLPurifier_Token - */ - public $start; - - public function toNode() { - throw new Exception("HTMLPurifier_Token_End->toNode not supported!"); - } -} - - - - - -/** - * Concrete start token class. - */ -class HTMLPurifier_Token_Start extends HTMLPurifier_Token_Tag -{ -} - - - - - -/** - * Concrete text token class. - * - * Text tokens comprise of regular parsed character data (PCDATA) and raw - * character data (from the CDATA sections). Internally, their - * data is parsed with all entities expanded. Surprisingly, the text token - * does have a "tag name" called #PCDATA, which is how the DTD represents it - * in permissible child nodes. - */ -class HTMLPurifier_Token_Text extends HTMLPurifier_Token -{ - - /** - * @type string - */ - public $name = '#PCDATA'; - /**< PCDATA tag name compatible with DTD. */ - - /** - * @type string - */ - public $data; - /**< Parsed character data of text. */ - - /** - * @type bool - */ - public $is_whitespace; - - /**< Bool indicating if node is whitespace. */ - - /** - * Constructor, accepts data and determines if it is whitespace. - * @param string $data String parsed character data. - * @param int $line - * @param int $col - */ - public function __construct($data, $line = null, $col = null) - { - $this->data = $data; - $this->is_whitespace = ctype_space($data); - $this->line = $line; - $this->col = $col; - } - - public function toNode() { - return new HTMLPurifier_Node_Text($this->data, $this->is_whitespace, $this->line, $this->col); - } -} - - - - - -class HTMLPurifier_URIFilter_DisableExternal extends HTMLPurifier_URIFilter -{ - /** - * @type string - */ - public $name = 'DisableExternal'; - - /** - * @type array - */ - protected $ourHostParts = false; - - /** - * @param HTMLPurifier_Config $config - * @return void - */ - public function prepare($config) - { - $our_host = $config->getDefinition('URI')->host; - if ($our_host !== null) { - $this->ourHostParts = array_reverse(explode('.', $our_host)); - } - } - - /** - * @param HTMLPurifier_URI $uri Reference - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool - */ - public function filter(&$uri, $config, $context) - { - if (is_null($uri->host)) { - return true; - } - if ($this->ourHostParts === false) { - return false; - } - $host_parts = array_reverse(explode('.', $uri->host)); - foreach ($this->ourHostParts as $i => $x) { - if (!isset($host_parts[$i])) { - return false; - } - if ($host_parts[$i] != $this->ourHostParts[$i]) { - return false; - } - } - return true; - } -} - - - - - -class HTMLPurifier_URIFilter_DisableExternalResources extends HTMLPurifier_URIFilter_DisableExternal -{ - /** - * @type string - */ - public $name = 'DisableExternalResources'; - - /** - * @param HTMLPurifier_URI $uri - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool - */ - public function filter(&$uri, $config, $context) - { - if (!$context->get('EmbeddedURI', true)) { - return true; - } - return parent::filter($uri, $config, $context); - } -} - - - - - -class HTMLPurifier_URIFilter_DisableResources extends HTMLPurifier_URIFilter -{ - /** - * @type string - */ - public $name = 'DisableResources'; - - /** - * @param HTMLPurifier_URI $uri - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool - */ - public function filter(&$uri, $config, $context) - { - return !$context->get('EmbeddedURI', true); - } -} - - - - - -// It's not clear to me whether or not Punycode means that hostnames -// do not have canonical forms anymore. As far as I can tell, it's -// not a problem (punycoding should be identity when no Unicode -// points are involved), but I'm not 100% sure -class HTMLPurifier_URIFilter_HostBlacklist extends HTMLPurifier_URIFilter -{ - /** - * @type string - */ - public $name = 'HostBlacklist'; - - /** - * @type array - */ - protected $blacklist = array(); - - /** - * @param HTMLPurifier_Config $config - * @return bool - */ - public function prepare($config) - { - $this->blacklist = $config->get('URI.HostBlacklist'); - return true; - } - - /** - * @param HTMLPurifier_URI $uri - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool - */ - public function filter(&$uri, $config, $context) - { - foreach ($this->blacklist as $blacklisted_host_fragment) { - if (strpos($uri->host, $blacklisted_host_fragment) !== false) { - return false; - } - } - return true; - } -} - - - - - -// does not support network paths - -class HTMLPurifier_URIFilter_MakeAbsolute extends HTMLPurifier_URIFilter -{ - /** - * @type string - */ - public $name = 'MakeAbsolute'; - - /** - * @type - */ - protected $base; - - /** - * @type array - */ - protected $basePathStack = array(); - - /** - * @param HTMLPurifier_Config $config - * @return bool - */ - public function prepare($config) - { - $def = $config->getDefinition('URI'); - $this->base = $def->base; - if (is_null($this->base)) { - trigger_error( - 'URI.MakeAbsolute is being ignored due to lack of ' . - 'value for URI.Base configuration', - E_USER_WARNING - ); - return false; - } - $this->base->fragment = null; // fragment is invalid for base URI - $stack = explode('/', $this->base->path); - array_pop($stack); // discard last segment - $stack = $this->_collapseStack($stack); // do pre-parsing - $this->basePathStack = $stack; - return true; - } - - /** - * @param HTMLPurifier_URI $uri - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool - */ - public function filter(&$uri, $config, $context) - { - if (is_null($this->base)) { - return true; - } // abort early - if ($uri->path === '' && is_null($uri->scheme) && - is_null($uri->host) && is_null($uri->query) && is_null($uri->fragment)) { - // reference to current document - $uri = clone $this->base; - return true; - } - if (!is_null($uri->scheme)) { - // absolute URI already: don't change - if (!is_null($uri->host)) { - return true; - } - $scheme_obj = $uri->getSchemeObj($config, $context); - if (!$scheme_obj) { - // scheme not recognized - return false; - } - if (!$scheme_obj->hierarchical) { - // non-hierarchal URI with explicit scheme, don't change - return true; - } - // special case: had a scheme but always is hierarchical and had no authority - } - if (!is_null($uri->host)) { - // network path, don't bother - return true; - } - if ($uri->path === '') { - $uri->path = $this->base->path; - } elseif ($uri->path[0] !== '/') { - // relative path, needs more complicated processing - $stack = explode('/', $uri->path); - $new_stack = array_merge($this->basePathStack, $stack); - if ($new_stack[0] !== '' && !is_null($this->base->host)) { - array_unshift($new_stack, ''); - } - $new_stack = $this->_collapseStack($new_stack); - $uri->path = implode('/', $new_stack); - } else { - // absolute path, but still we should collapse - $uri->path = implode('/', $this->_collapseStack(explode('/', $uri->path))); - } - // re-combine - $uri->scheme = $this->base->scheme; - if (is_null($uri->userinfo)) { - $uri->userinfo = $this->base->userinfo; - } - if (is_null($uri->host)) { - $uri->host = $this->base->host; - } - if (is_null($uri->port)) { - $uri->port = $this->base->port; - } - return true; - } - - /** - * Resolve dots and double-dots in a path stack - * @param array $stack - * @return array - */ - private function _collapseStack($stack) - { - $result = array(); - $is_folder = false; - for ($i = 0; isset($stack[$i]); $i++) { - $is_folder = false; - // absorb an internally duplicated slash - if ($stack[$i] == '' && $i && isset($stack[$i + 1])) { - continue; - } - if ($stack[$i] == '..') { - if (!empty($result)) { - $segment = array_pop($result); - if ($segment === '' && empty($result)) { - // error case: attempted to back out too far: - // restore the leading slash - $result[] = ''; - } elseif ($segment === '..') { - $result[] = '..'; // cannot remove .. with .. - } - } else { - // relative path, preserve the double-dots - $result[] = '..'; - } - $is_folder = true; - continue; - } - if ($stack[$i] == '.') { - // silently absorb - $is_folder = true; - continue; - } - $result[] = $stack[$i]; - } - if ($is_folder) { - $result[] = ''; - } - return $result; - } -} - - - - - -class HTMLPurifier_URIFilter_Munge extends HTMLPurifier_URIFilter -{ - /** - * @type string - */ - public $name = 'Munge'; - - /** - * @type bool - */ - public $post = true; - - /** - * @type string - */ - private $target; - - /** - * @type HTMLPurifier_URIParser - */ - private $parser; - - /** - * @type bool - */ - private $doEmbed; - - /** - * @type string - */ - private $secretKey; - - /** - * @type array - */ - protected $replace = array(); - - /** - * @param HTMLPurifier_Config $config - * @return bool - */ - public function prepare($config) - { - $this->target = $config->get('URI.' . $this->name); - $this->parser = new HTMLPurifier_URIParser(); - $this->doEmbed = $config->get('URI.MungeResources'); - $this->secretKey = $config->get('URI.MungeSecretKey'); - if ($this->secretKey && !function_exists('hash_hmac')) { - throw new Exception("Cannot use %URI.MungeSecretKey without hash_hmac support."); - } - return true; - } - - /** - * @param HTMLPurifier_URI $uri - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool - */ - public function filter(&$uri, $config, $context) - { - if ($context->get('EmbeddedURI', true) && !$this->doEmbed) { - return true; - } - - $scheme_obj = $uri->getSchemeObj($config, $context); - if (!$scheme_obj) { - return true; - } // ignore unknown schemes, maybe another postfilter did it - if (!$scheme_obj->browsable) { - return true; - } // ignore non-browseable schemes, since we can't munge those in a reasonable way - if ($uri->isBenign($config, $context)) { - return true; - } // don't redirect if a benign URL - - $this->makeReplace($uri, $config, $context); - $this->replace = array_map('rawurlencode', $this->replace); - - $new_uri = strtr($this->target, $this->replace); - $new_uri = $this->parser->parse($new_uri); - // don't redirect if the target host is the same as the - // starting host - if ($uri->host === $new_uri->host) { - return true; - } - $uri = $new_uri; // overwrite - return true; - } - - /** - * @param HTMLPurifier_URI $uri - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - */ - protected function makeReplace($uri, $config, $context) - { - $string = $uri->toString(); - // always available - $this->replace['%s'] = $string; - $this->replace['%r'] = $context->get('EmbeddedURI', true); - $token = $context->get('CurrentToken', true); - $this->replace['%n'] = $token ? $token->name : null; - $this->replace['%m'] = $context->get('CurrentAttr', true); - $this->replace['%p'] = $context->get('CurrentCSSProperty', true); - // not always available - if ($this->secretKey) { - $this->replace['%t'] = hash_hmac("sha256", $string, $this->secretKey); - } - } -} - - - - - -/** - * Implements safety checks for safe iframes. - * - * @warning This filter is *critical* for ensuring that %HTML.SafeIframe - * works safely. - */ -class HTMLPurifier_URIFilter_SafeIframe extends HTMLPurifier_URIFilter -{ - /** - * @type string - */ - public $name = 'SafeIframe'; - - /** - * @type bool - */ - public $always_load = true; - - /** - * @type string - */ - protected $regexp = null; - - // XXX: The not so good bit about how this is all set up now is we - // can't check HTML.SafeIframe in the 'prepare' step: we have to - // defer till the actual filtering. - /** - * @param HTMLPurifier_Config $config - * @return bool - */ - public function prepare($config) - { - $this->regexp = $config->get('URI.SafeIframeRegexp'); - return true; - } - - /** - * @param HTMLPurifier_URI $uri - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool - */ - public function filter(&$uri, $config, $context) - { - // check if filter not applicable - if (!$config->get('HTML.SafeIframe')) { - return true; - } - // check if the filter should actually trigger - if (!$context->get('EmbeddedURI', true)) { - return true; - } - $token = $context->get('CurrentToken', true); - if (!($token && $token->name == 'iframe')) { - return true; - } - // check if we actually have some whitelists enabled - if ($this->regexp === null) { - return false; - } - // actually check the whitelists - return preg_match($this->regexp, $uri->toString()); - } -} - - - - - -/** - * Implements data: URI for base64 encoded images supported by GD. - */ -class HTMLPurifier_URIScheme_data extends HTMLPurifier_URIScheme -{ - /** - * @type bool - */ - public $browsable = true; - - /** - * @type array - */ - public $allowed_types = array( - // you better write validation code for other types if you - // decide to allow them - 'image/jpeg' => true, - 'image/gif' => true, - 'image/png' => true, - ); - // this is actually irrelevant since we only write out the path - // component - /** - * @type bool - */ - public $may_omit_host = true; - - /** - * @param HTMLPurifier_URI $uri - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool - */ - public function doValidate(&$uri, $config, $context) - { - $result = explode(',', $uri->path, 2); - $is_base64 = false; - $charset = null; - $content_type = null; - if (count($result) == 2) { - list($metadata, $data) = $result; - // do some legwork on the metadata - $metas = explode(';', $metadata); - while (!empty($metas)) { - $cur = array_shift($metas); - if ($cur == 'base64') { - $is_base64 = true; - break; - } - if (substr($cur, 0, 8) == 'charset=') { - // doesn't match if there are arbitrary spaces, but - // whatever dude - if ($charset !== null) { - continue; - } // garbage - $charset = substr($cur, 8); // not used - } else { - if ($content_type !== null) { - continue; - } // garbage - $content_type = $cur; - } - } - } else { - $data = $result[0]; - } - if ($content_type !== null && empty($this->allowed_types[$content_type])) { - return false; - } - if ($charset !== null) { - // error; we don't allow plaintext stuff - $charset = null; - } - $data = rawurldecode($data); - if ($is_base64) { - $raw_data = base64_decode($data); - } else { - $raw_data = $data; - } - // XXX probably want to refactor this into a general mechanism - // for filtering arbitrary content types - $file = tempnam("/tmp", ""); - file_put_contents($file, $raw_data); - if (function_exists('exif_imagetype')) { - $image_code = exif_imagetype($file); - unlink($file); - } elseif (function_exists('getimagesize')) { - set_error_handler(array($this, 'muteErrorHandler')); - $info = getimagesize($file); - restore_error_handler(); - unlink($file); - if ($info == false) { - return false; - } - $image_code = $info[2]; - } else { - trigger_error("could not find exif_imagetype or getimagesize functions", E_USER_ERROR); - } - $real_content_type = image_type_to_mime_type($image_code); - if ($real_content_type != $content_type) { - // we're nice guys; if the content type is something else we - // support, change it over - if (empty($this->allowed_types[$real_content_type])) { - return false; - } - $content_type = $real_content_type; - } - // ok, it's kosher, rewrite what we need - $uri->userinfo = null; - $uri->host = null; - $uri->port = null; - $uri->fragment = null; - $uri->query = null; - $uri->path = "$content_type;base64," . base64_encode($raw_data); - return true; - } - - /** - * @param int $errno - * @param string $errstr - */ - public function muteErrorHandler($errno, $errstr) - { - } -} - - - -/** - * Validates file as defined by RFC 1630 and RFC 1738. - */ -class HTMLPurifier_URIScheme_file extends HTMLPurifier_URIScheme -{ - /** - * Generally file:// URLs are not accessible from most - * machines, so placing them as an img src is incorrect. - * @type bool - */ - public $browsable = false; - - /** - * Basically the *only* URI scheme for which this is true, since - * accessing files on the local machine is very common. In fact, - * browsers on some operating systems don't understand the - * authority, though I hear it is used on Windows to refer to - * network shares. - * @type bool - */ - public $may_omit_host = true; - - /** - * @param HTMLPurifier_URI $uri - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool - */ - public function doValidate(&$uri, $config, $context) - { - // Authentication method is not supported - $uri->userinfo = null; - // file:// makes no provisions for accessing the resource - $uri->port = null; - // While it seems to work on Firefox, the querystring has - // no possible effect and is thus stripped. - $uri->query = null; - return true; - } -} - - - - - -/** - * Validates ftp (File Transfer Protocol) URIs as defined by generic RFC 1738. - */ -class HTMLPurifier_URIScheme_ftp extends HTMLPurifier_URIScheme -{ - /** - * @type int - */ - public $default_port = 21; - - /** - * @type bool - */ - public $browsable = true; // usually - - /** - * @type bool - */ - public $hierarchical = true; - - /** - * @param HTMLPurifier_URI $uri - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool - */ - public function doValidate(&$uri, $config, $context) - { - $uri->query = null; - - // typecode check - $semicolon_pos = strrpos($uri->path, ';'); // reverse - if ($semicolon_pos !== false) { - $type = substr($uri->path, $semicolon_pos + 1); // no semicolon - $uri->path = substr($uri->path, 0, $semicolon_pos); - $type_ret = ''; - if (strpos($type, '=') !== false) { - // figure out whether or not the declaration is correct - list($key, $typecode) = explode('=', $type, 2); - if ($key !== 'type') { - // invalid key, tack it back on encoded - $uri->path .= '%3B' . $type; - } elseif ($typecode === 'a' || $typecode === 'i' || $typecode === 'd') { - $type_ret = ";type=$typecode"; - } - } else { - $uri->path .= '%3B' . $type; - } - $uri->path = str_replace(';', '%3B', $uri->path); - $uri->path .= $type_ret; - } - return true; - } -} - - - - - -/** - * Validates http (HyperText Transfer Protocol) as defined by RFC 2616 - */ -class HTMLPurifier_URIScheme_http extends HTMLPurifier_URIScheme -{ - /** - * @type int - */ - public $default_port = 80; - - /** - * @type bool - */ - public $browsable = true; - - /** - * @type bool - */ - public $hierarchical = true; - - /** - * @param HTMLPurifier_URI $uri - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool - */ - public function doValidate(&$uri, $config, $context) - { - $uri->userinfo = null; - return true; - } -} - - - - - -/** - * Validates https (Secure HTTP) according to http scheme. - */ -class HTMLPurifier_URIScheme_https extends HTMLPurifier_URIScheme_http -{ - /** - * @type int - */ - public $default_port = 443; - /** - * @type bool - */ - public $secure = true; -} - - - - - -// VERY RELAXED! Shouldn't cause problems, not even Firefox checks if the -// email is valid, but be careful! - -/** - * Validates mailto (for E-mail) according to RFC 2368 - * @todo Validate the email address - * @todo Filter allowed query parameters - */ - -class HTMLPurifier_URIScheme_mailto extends HTMLPurifier_URIScheme -{ - /** - * @type bool - */ - public $browsable = false; - - /** - * @type bool - */ - public $may_omit_host = true; - - /** - * @param HTMLPurifier_URI $uri - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool - */ - public function doValidate(&$uri, $config, $context) - { - $uri->userinfo = null; - $uri->host = null; - $uri->port = null; - // we need to validate path against RFC 2368's addr-spec - return true; - } -} - - - - - -/** - * Validates news (Usenet) as defined by generic RFC 1738 - */ -class HTMLPurifier_URIScheme_news extends HTMLPurifier_URIScheme -{ - /** - * @type bool - */ - public $browsable = false; - - /** - * @type bool - */ - public $may_omit_host = true; - - /** - * @param HTMLPurifier_URI $uri - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool - */ - public function doValidate(&$uri, $config, $context) - { - $uri->userinfo = null; - $uri->host = null; - $uri->port = null; - $uri->query = null; - // typecode check needed on path - return true; - } -} - - - - - -/** - * Validates nntp (Network News Transfer Protocol) as defined by generic RFC 1738 - */ -class HTMLPurifier_URIScheme_nntp extends HTMLPurifier_URIScheme -{ - /** - * @type int - */ - public $default_port = 119; - - /** - * @type bool - */ - public $browsable = false; - - /** - * @param HTMLPurifier_URI $uri - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool - */ - public function doValidate(&$uri, $config, $context) - { - $uri->userinfo = null; - $uri->query = null; - return true; - } -} - - - - - -/** - * Performs safe variable parsing based on types which can be used by - * users. This may not be able to represent all possible data inputs, - * however. - */ -class HTMLPurifier_VarParser_Flexible extends HTMLPurifier_VarParser -{ - /** - * @param mixed $var - * @param int $type - * @param bool $allow_null - * @return array|bool|float|int|mixed|null|string - * @throws HTMLPurifier_VarParserException - */ - protected function parseImplementation($var, $type, $allow_null) - { - if ($allow_null && $var === null) { - return null; - } - switch ($type) { - // Note: if code "breaks" from the switch, it triggers a generic - // exception to be thrown. Specific errors can be specifically - // done here. - case self::MIXED: - case self::ISTRING: - case self::STRING: - case self::TEXT: - case self::ITEXT: - return $var; - case self::INT: - if (is_string($var) && ctype_digit($var)) { - $var = (int)$var; - } - return $var; - case self::FLOAT: - if ((is_string($var) && is_numeric($var)) || is_int($var)) { - $var = (float)$var; - } - return $var; - case self::BOOL: - if (is_int($var) && ($var === 0 || $var === 1)) { - $var = (bool)$var; - } elseif (is_string($var)) { - if ($var == 'on' || $var == 'true' || $var == '1') { - $var = true; - } elseif ($var == 'off' || $var == 'false' || $var == '0') { - $var = false; - } else { - throw new HTMLPurifier_VarParserException("Unrecognized value '$var' for $type"); - } - } - return $var; - case self::ALIST: - case self::HASH: - case self::LOOKUP: - if (is_string($var)) { - // special case: technically, this is an array with - // a single empty string item, but having an empty - // array is more intuitive - if ($var == '') { - return array(); - } - if (strpos($var, "\n") === false && strpos($var, "\r") === false) { - // simplistic string to array method that only works - // for simple lists of tag names or alphanumeric characters - $var = explode(',', $var); - } else { - $var = preg_split('/(,|[\n\r]+)/', $var); - } - // remove spaces - foreach ($var as $i => $j) { - $var[$i] = trim($j); - } - if ($type === self::HASH) { - // key:value,key2:value2 - $nvar = array(); - foreach ($var as $keypair) { - $c = explode(':', $keypair, 2); - if (!isset($c[1])) { - continue; - } - $nvar[trim($c[0])] = trim($c[1]); - } - $var = $nvar; - } - } - if (!is_array($var)) { - break; - } - $keys = array_keys($var); - if ($keys === array_keys($keys)) { - if ($type == self::ALIST) { - return $var; - } elseif ($type == self::LOOKUP) { - $new = array(); - foreach ($var as $key) { - $new[$key] = true; - } - return $new; - } else { - break; - } - } - if ($type === self::ALIST) { - trigger_error("Array list did not have consecutive integer indexes", E_USER_WARNING); - return array_values($var); - } - if ($type === self::LOOKUP) { - foreach ($var as $key => $value) { - if ($value !== true) { - trigger_error( - "Lookup array has non-true value at key '$key'; " . - "maybe your input array was not indexed numerically", - E_USER_WARNING - ); - } - $var[$key] = true; - } - } - return $var; - default: - $this->errorInconsistent(__CLASS__, $type); - } - $this->errorGeneric($var, $type); - } -} - - - - - -/** - * This variable parser uses PHP's internal code engine. Because it does - * this, it can represent all inputs; however, it is dangerous and cannot - * be used by users. - */ -class HTMLPurifier_VarParser_Native extends HTMLPurifier_VarParser -{ - - /** - * @param mixed $var - * @param int $type - * @param bool $allow_null - * @return null|string - */ - protected function parseImplementation($var, $type, $allow_null) - { - return $this->evalExpression($var); - } - - /** - * @param string $expr - * @return mixed - * @throws HTMLPurifier_VarParserException - */ - protected function evalExpression($expr) - { - $var = null; - $result = eval("\$var = $expr;"); - if ($result === false) { - throw new HTMLPurifier_VarParserException("Fatal error in evaluated code"); - } - return $var; - } -} - +config = HTMLPurifier_Config::create($config); + $this->strategy = new HTMLPurifier_Strategy_Core(); + } + + /** + * Adds a filter to process the output. First come first serve + * + * @param HTMLPurifier_Filter $filter HTMLPurifier_Filter object + */ + public function addFilter($filter) + { + trigger_error( + 'HTMLPurifier->addFilter() is deprecated, use configuration directives' . + ' in the Filter namespace or Filter.Custom', + E_USER_WARNING + ); + $this->filters[] = $filter; + } + + /** + * Filters an HTML snippet/document to be XSS-free and standards-compliant. + * + * @param string $html String of HTML to purify + * @param HTMLPurifier_Config $config Config object for this operation, + * if omitted, defaults to the config object specified during this + * object's construction. The parameter can also be any type + * that HTMLPurifier_Config::create() supports. + * + * @return string Purified HTML + */ + public function purify($html, $config = null) + { + // :TODO: make the config merge in, instead of replace + $config = $config ? HTMLPurifier_Config::create($config) : $this->config; + + // implementation is partially environment dependant, partially + // configuration dependant + $lexer = HTMLPurifier_Lexer::create($config); + + $context = new HTMLPurifier_Context(); + + // setup HTML generator + $this->generator = new HTMLPurifier_Generator($config, $context); + $context->register('Generator', $this->generator); + + // set up global context variables + if ($config->get('Core.CollectErrors')) { + // may get moved out if other facilities use it + $language_factory = HTMLPurifier_LanguageFactory::instance(); + $language = $language_factory->create($config, $context); + $context->register('Locale', $language); + + $error_collector = new HTMLPurifier_ErrorCollector($context); + $context->register('ErrorCollector', $error_collector); + } + + // setup id_accumulator context, necessary due to the fact that + // AttrValidator can be called from many places + $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context); + $context->register('IDAccumulator', $id_accumulator); + + $html = HTMLPurifier_Encoder::convertToUTF8($html, $config, $context); + + // setup filters + $filter_flags = $config->getBatch('Filter'); + $custom_filters = $filter_flags['Custom']; + unset($filter_flags['Custom']); + $filters = array(); + foreach ($filter_flags as $filter => $flag) { + if (!$flag) { + continue; + } + if (strpos($filter, '.') !== false) { + continue; + } + $class = "HTMLPurifier_Filter_$filter"; + $filters[] = new $class; + } + foreach ($custom_filters as $filter) { + // maybe "HTMLPurifier_Filter_$filter", but be consistent with AutoFormat + $filters[] = $filter; + } + $filters = array_merge($filters, $this->filters); + // maybe prepare(), but later + + for ($i = 0, $filter_size = count($filters); $i < $filter_size; $i++) { + $html = $filters[$i]->preFilter($html, $config, $context); + } + + // purified HTML + $html = + $this->generator->generateFromTokens( + // list of tokens + $this->strategy->execute( + // list of un-purified tokens + $lexer->tokenizeHTML( + // un-purified HTML + $html, + $config, + $context + ), + $config, + $context + ) + ); + + for ($i = $filter_size - 1; $i >= 0; $i--) { + $html = $filters[$i]->postFilter($html, $config, $context); + } + + $html = HTMLPurifier_Encoder::convertFromUTF8($html, $config, $context); + $this->context =& $context; + return $html; + } + + /** + * Filters an array of HTML snippets + * + * @param string[] $array_of_html Array of html snippets + * @param HTMLPurifier_Config $config Optional config object for this operation. + * See HTMLPurifier::purify() for more details. + * + * @return string[] Array of purified HTML + */ + public function purifyArray($array_of_html, $config = null) + { + $context_array = array(); + foreach ($array_of_html as $key => $html) { + $array_of_html[$key] = $this->purify($html, $config); + $context_array[$key] = $this->context; + } + $this->context = $context_array; + return $array_of_html; + } + + /** + * Singleton for enforcing just one HTML Purifier in your system + * + * @param HTMLPurifier|HTMLPurifier_Config $prototype Optional prototype + * HTMLPurifier instance to overload singleton with, + * or HTMLPurifier_Config instance to configure the + * generated version with. + * + * @return HTMLPurifier + */ + public static function instance($prototype = null) + { + if (!self::$instance || $prototype) { + if ($prototype instanceof HTMLPurifier) { + self::$instance = $prototype; + } elseif ($prototype) { + self::$instance = new HTMLPurifier($prototype); + } else { + self::$instance = new HTMLPurifier(); + } + } + return self::$instance; + } + + /** + * Singleton for enforcing just one HTML Purifier in your system + * + * @param HTMLPurifier|HTMLPurifier_Config $prototype Optional prototype + * HTMLPurifier instance to overload singleton with, + * or HTMLPurifier_Config instance to configure the + * generated version with. + * + * @return HTMLPurifier + * @note Backwards compatibility, see instance() + */ + public static function getInstance($prototype = null) + { + return HTMLPurifier::instance($prototype); + } +} + + + + + +/** + * Converts a stream of HTMLPurifier_Token into an HTMLPurifier_Node, + * and back again. + * + * @note This transformation is not an equivalence. We mutate the input + * token stream to make it so; see all [MUT] markers in code. + */ +class HTMLPurifier_Arborize +{ + public static function arborize($tokens, $config, $context) { + $definition = $config->getHTMLDefinition(); + $parent = new HTMLPurifier_Token_Start($definition->info_parent); + $stack = array($parent->toNode()); + foreach ($tokens as $token) { + $token->skip = null; // [MUT] + $token->carryover = null; // [MUT] + if ($token instanceof HTMLPurifier_Token_End) { + $token->start = null; // [MUT] + $r = array_pop($stack); + assert($r->name === $token->name); + assert(empty($token->attr)); + $r->endCol = $token->col; + $r->endLine = $token->line; + $r->endArmor = $token->armor; + continue; + } + $node = $token->toNode(); + $stack[count($stack)-1]->children[] = $node; + if ($token instanceof HTMLPurifier_Token_Start) { + $stack[] = $node; + } + } + assert(count($stack) == 1); + return $stack[0]; + } + + public static function flatten($node, $config, $context) { + $level = 0; + $nodes = array($level => new HTMLPurifier_Queue(array($node))); + $closingTokens = array(); + $tokens = array(); + do { + while (!$nodes[$level]->isEmpty()) { + $node = $nodes[$level]->shift(); // FIFO + list($start, $end) = $node->toTokenPair(); + if ($level > 0) { + $tokens[] = $start; + } + if ($end !== NULL) { + $closingTokens[$level][] = $end; + } + if ($node instanceof HTMLPurifier_Node_Element) { + $level++; + $nodes[$level] = new HTMLPurifier_Queue(); + foreach ($node->children as $childNode) { + $nodes[$level]->push($childNode); + } + } + } + $level--; + if ($level && isset($closingTokens[$level])) { + while ($token = array_pop($closingTokens[$level])) { + $tokens[] = $token; + } + } + } while ($level > 0); + return $tokens; + } +} + + + +/** + * Defines common attribute collections that modules reference + */ + +class HTMLPurifier_AttrCollections +{ + + /** + * Associative array of attribute collections, indexed by name. + * @type array + */ + public $info = array(); + + /** + * Performs all expansions on internal data for use by other inclusions + * It also collects all attribute collection extensions from + * modules + * @param HTMLPurifier_AttrTypes $attr_types HTMLPurifier_AttrTypes instance + * @param HTMLPurifier_HTMLModule[] $modules Hash array of HTMLPurifier_HTMLModule members + */ + public function __construct($attr_types, $modules) + { + // load extensions from the modules + foreach ($modules as $module) { + foreach ($module->attr_collections as $coll_i => $coll) { + if (!isset($this->info[$coll_i])) { + $this->info[$coll_i] = array(); + } + foreach ($coll as $attr_i => $attr) { + if ($attr_i === 0 && isset($this->info[$coll_i][$attr_i])) { + // merge in includes + $this->info[$coll_i][$attr_i] = array_merge( + $this->info[$coll_i][$attr_i], + $attr + ); + continue; + } + $this->info[$coll_i][$attr_i] = $attr; + } + } + } + // perform internal expansions and inclusions + foreach ($this->info as $name => $attr) { + // merge attribute collections that include others + $this->performInclusions($this->info[$name]); + // replace string identifiers with actual attribute objects + $this->expandIdentifiers($this->info[$name], $attr_types); + } + } + + /** + * Takes a reference to an attribute associative array and performs + * all inclusions specified by the zero index. + * @param array &$attr Reference to attribute array + */ + public function performInclusions(&$attr) + { + if (!isset($attr[0])) { + return; + } + $merge = $attr[0]; + $seen = array(); // recursion guard + // loop through all the inclusions + for ($i = 0; isset($merge[$i]); $i++) { + if (isset($seen[$merge[$i]])) { + continue; + } + $seen[$merge[$i]] = true; + // foreach attribute of the inclusion, copy it over + if (!isset($this->info[$merge[$i]])) { + continue; + } + foreach ($this->info[$merge[$i]] as $key => $value) { + if (isset($attr[$key])) { + continue; + } // also catches more inclusions + $attr[$key] = $value; + } + if (isset($this->info[$merge[$i]][0])) { + // recursion + $merge = array_merge($merge, $this->info[$merge[$i]][0]); + } + } + unset($attr[0]); + } + + /** + * Expands all string identifiers in an attribute array by replacing + * them with the appropriate values inside HTMLPurifier_AttrTypes + * @param array &$attr Reference to attribute array + * @param HTMLPurifier_AttrTypes $attr_types HTMLPurifier_AttrTypes instance + */ + public function expandIdentifiers(&$attr, $attr_types) + { + // because foreach will process new elements we add, make sure we + // skip duplicates + $processed = array(); + + foreach ($attr as $def_i => $def) { + // skip inclusions + if ($def_i === 0) { + continue; + } + + if (isset($processed[$def_i])) { + continue; + } + + // determine whether or not attribute is required + if ($required = (strpos($def_i, '*') !== false)) { + // rename the definition + unset($attr[$def_i]); + $def_i = trim($def_i, '*'); + $attr[$def_i] = $def; + } + + $processed[$def_i] = true; + + // if we've already got a literal object, move on + if (is_object($def)) { + // preserve previous required + $attr[$def_i]->required = ($required || $attr[$def_i]->required); + continue; + } + + if ($def === false) { + unset($attr[$def_i]); + continue; + } + + if ($t = $attr_types->get($def)) { + $attr[$def_i] = $t; + $attr[$def_i]->required = $required; + } else { + unset($attr[$def_i]); + } + } + } +} + + + + + +/** + * Base class for all validating attribute definitions. + * + * This family of classes forms the core for not only HTML attribute validation, + * but also any sort of string that needs to be validated or cleaned (which + * means CSS properties and composite definitions are defined here too). + * Besides defining (through code) what precisely makes the string valid, + * subclasses are also responsible for cleaning the code if possible. + */ + +abstract class HTMLPurifier_AttrDef +{ + + /** + * Tells us whether or not an HTML attribute is minimized. + * Has no meaning in other contexts. + * @type bool + */ + public $minimized = false; + + /** + * Tells us whether or not an HTML attribute is required. + * Has no meaning in other contexts + * @type bool + */ + public $required = false; + + /** + * Validates and cleans passed string according to a definition. + * + * @param string $string String to be validated and cleaned. + * @param HTMLPurifier_Config $config Mandatory HTMLPurifier_Config object. + * @param HTMLPurifier_Context $context Mandatory HTMLPurifier_Context object. + */ + abstract public function validate($string, $config, $context); + + /** + * Convenience method that parses a string as if it were CDATA. + * + * This method process a string in the manner specified at + * by removing + * leading and trailing whitespace, ignoring line feeds, and replacing + * carriage returns and tabs with spaces. While most useful for HTML + * attributes specified as CDATA, it can also be applied to most CSS + * values. + * + * @note This method is not entirely standards compliant, as trim() removes + * more types of whitespace than specified in the spec. In practice, + * this is rarely a problem, as those extra characters usually have + * already been removed by HTMLPurifier_Encoder. + * + * @warning This processing is inconsistent with XML's whitespace handling + * as specified by section 3.3.3 and referenced XHTML 1.0 section + * 4.7. However, note that we are NOT necessarily + * parsing XML, thus, this behavior may still be correct. We + * assume that newlines have been normalized. + */ + public function parseCDATA($string) + { + $string = trim($string); + $string = str_replace(array("\n", "\t", "\r"), ' ', $string); + return $string; + } + + /** + * Factory method for creating this class from a string. + * @param string $string String construction info + * @return HTMLPurifier_AttrDef Created AttrDef object corresponding to $string + */ + public function make($string) + { + // default implementation, return a flyweight of this object. + // If $string has an effect on the returned object (i.e. you + // need to overload this method), it is best + // to clone or instantiate new copies. (Instantiation is safer.) + return $this; + } + + /** + * Removes spaces from rgb(0, 0, 0) so that shorthand CSS properties work + * properly. THIS IS A HACK! + * @param string $string a CSS colour definition + * @return string + */ + protected function mungeRgb($string) + { + return preg_replace('/rgb\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\)/', 'rgb(\1,\2,\3)', $string); + } + + /** + * Parses a possibly escaped CSS string and returns the "pure" + * version of it. + */ + protected function expandCSSEscape($string) + { + // flexibly parse it + $ret = ''; + for ($i = 0, $c = strlen($string); $i < $c; $i++) { + if ($string[$i] === '\\') { + $i++; + if ($i >= $c) { + $ret .= '\\'; + break; + } + if (ctype_xdigit($string[$i])) { + $code = $string[$i]; + for ($a = 1, $i++; $i < $c && $a < 6; $i++, $a++) { + if (!ctype_xdigit($string[$i])) { + break; + } + $code .= $string[$i]; + } + // We have to be extremely careful when adding + // new characters, to make sure we're not breaking + // the encoding. + $char = HTMLPurifier_Encoder::unichr(hexdec($code)); + if (HTMLPurifier_Encoder::cleanUTF8($char) === '') { + continue; + } + $ret .= $char; + if ($i < $c && trim($string[$i]) !== '') { + $i--; + } + continue; + } + if ($string[$i] === "\n") { + continue; + } + } + $ret .= $string[$i]; + } + return $ret; + } +} + + + + + +/** + * Processes an entire attribute array for corrections needing multiple values. + * + * Occasionally, a certain attribute will need to be removed and popped onto + * another value. Instead of creating a complex return syntax for + * HTMLPurifier_AttrDef, we just pass the whole attribute array to a + * specialized object and have that do the special work. That is the + * family of HTMLPurifier_AttrTransform. + * + * An attribute transformation can be assigned to run before or after + * HTMLPurifier_AttrDef validation. See HTMLPurifier_HTMLDefinition for + * more details. + */ + +abstract class HTMLPurifier_AttrTransform +{ + + /** + * Abstract: makes changes to the attributes dependent on multiple values. + * + * @param array $attr Assoc array of attributes, usually from + * HTMLPurifier_Token_Tag::$attr + * @param HTMLPurifier_Config $config Mandatory HTMLPurifier_Config object. + * @param HTMLPurifier_Context $context Mandatory HTMLPurifier_Context object + * @return array Processed attribute array. + */ + abstract public function transform($attr, $config, $context); + + /** + * Prepends CSS properties to the style attribute, creating the + * attribute if it doesn't exist. + * @param array &$attr Attribute array to process (passed by reference) + * @param string $css CSS to prepend + */ + public function prependCSS(&$attr, $css) + { + $attr['style'] = isset($attr['style']) ? $attr['style'] : ''; + $attr['style'] = $css . $attr['style']; + } + + /** + * Retrieves and removes an attribute + * @param array &$attr Attribute array to process (passed by reference) + * @param mixed $key Key of attribute to confiscate + * @return mixed + */ + public function confiscateAttr(&$attr, $key) + { + if (!isset($attr[$key])) { + return null; + } + $value = $attr[$key]; + unset($attr[$key]); + return $value; + } +} + + + + + +/** + * Provides lookup array of attribute types to HTMLPurifier_AttrDef objects + */ +class HTMLPurifier_AttrTypes +{ + /** + * Lookup array of attribute string identifiers to concrete implementations. + * @type HTMLPurifier_AttrDef[] + */ + protected $info = array(); + + /** + * Constructs the info array, supplying default implementations for attribute + * types. + */ + public function __construct() + { + // XXX This is kind of poor, since we don't actually /clone/ + // instances; instead, we use the supplied make() attribute. So, + // the underlying class must know how to deal with arguments. + // With the old implementation of Enum, that ignored its + // arguments when handling a make dispatch, the IAlign + // definition wouldn't work. + + // pseudo-types, must be instantiated via shorthand + $this->info['Enum'] = new HTMLPurifier_AttrDef_Enum(); + $this->info['Bool'] = new HTMLPurifier_AttrDef_HTML_Bool(); + + $this->info['CDATA'] = new HTMLPurifier_AttrDef_Text(); + $this->info['ID'] = new HTMLPurifier_AttrDef_HTML_ID(); + $this->info['Length'] = new HTMLPurifier_AttrDef_HTML_Length(); + $this->info['MultiLength'] = new HTMLPurifier_AttrDef_HTML_MultiLength(); + $this->info['NMTOKENS'] = new HTMLPurifier_AttrDef_HTML_Nmtokens(); + $this->info['Pixels'] = new HTMLPurifier_AttrDef_HTML_Pixels(); + $this->info['Text'] = new HTMLPurifier_AttrDef_Text(); + $this->info['URI'] = new HTMLPurifier_AttrDef_URI(); + $this->info['LanguageCode'] = new HTMLPurifier_AttrDef_Lang(); + $this->info['Color'] = new HTMLPurifier_AttrDef_HTML_Color(); + $this->info['IAlign'] = self::makeEnum('top,middle,bottom,left,right'); + $this->info['LAlign'] = self::makeEnum('top,bottom,left,right'); + $this->info['FrameTarget'] = new HTMLPurifier_AttrDef_HTML_FrameTarget(); + + // unimplemented aliases + $this->info['ContentType'] = new HTMLPurifier_AttrDef_Text(); + $this->info['ContentTypes'] = new HTMLPurifier_AttrDef_Text(); + $this->info['Charsets'] = new HTMLPurifier_AttrDef_Text(); + $this->info['Character'] = new HTMLPurifier_AttrDef_Text(); + + // "proprietary" types + $this->info['Class'] = new HTMLPurifier_AttrDef_HTML_Class(); + + // number is really a positive integer (one or more digits) + // FIXME: ^^ not always, see start and value of list items + $this->info['Number'] = new HTMLPurifier_AttrDef_Integer(false, false, true); + } + + private static function makeEnum($in) + { + return new HTMLPurifier_AttrDef_Clone(new HTMLPurifier_AttrDef_Enum(explode(',', $in))); + } + + /** + * Retrieves a type + * @param string $type String type name + * @return HTMLPurifier_AttrDef Object AttrDef for type + */ + public function get($type) + { + // determine if there is any extra info tacked on + if (strpos($type, '#') !== false) { + list($type, $string) = explode('#', $type, 2); + } else { + $string = ''; + } + + if (!isset($this->info[$type])) { + trigger_error('Cannot retrieve undefined attribute type ' . $type, E_USER_ERROR); + return; + } + return $this->info[$type]->make($string); + } + + /** + * Sets a new implementation for a type + * @param string $type String type name + * @param HTMLPurifier_AttrDef $impl Object AttrDef for type + */ + public function set($type, $impl) + { + $this->info[$type] = $impl; + } +} + + + + + +/** + * Validates the attributes of a token. Doesn't manage required attributes + * very well. The only reason we factored this out was because RemoveForeignElements + * also needed it besides ValidateAttributes. + */ +class HTMLPurifier_AttrValidator +{ + + /** + * Validates the attributes of a token, mutating it as necessary. + * that has valid tokens + * @param HTMLPurifier_Token $token Token to validate. + * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config + * @param HTMLPurifier_Context $context Instance of HTMLPurifier_Context + */ + public function validateToken($token, $config, $context) + { + $definition = $config->getHTMLDefinition(); + $e =& $context->get('ErrorCollector', true); + + // initialize IDAccumulator if necessary + $ok =& $context->get('IDAccumulator', true); + if (!$ok) { + $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context); + $context->register('IDAccumulator', $id_accumulator); + } + + // initialize CurrentToken if necessary + $current_token =& $context->get('CurrentToken', true); + if (!$current_token) { + $context->register('CurrentToken', $token); + } + + if (!$token instanceof HTMLPurifier_Token_Start && + !$token instanceof HTMLPurifier_Token_Empty + ) { + return; + } + + // create alias to global definition array, see also $defs + // DEFINITION CALL + $d_defs = $definition->info_global_attr; + + // don't update token until the very end, to ensure an atomic update + $attr = $token->attr; + + // do global transformations (pre) + // nothing currently utilizes this + foreach ($definition->info_attr_transform_pre as $transform) { + $attr = $transform->transform($o = $attr, $config, $context); + if ($e) { + if ($attr != $o) { + $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } + } + } + + // do local transformations only applicable to this element (pre) + // ex.

          to

          + foreach ($definition->info[$token->name]->attr_transform_pre as $transform) { + $attr = $transform->transform($o = $attr, $config, $context); + if ($e) { + if ($attr != $o) { + $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } + } + } + + // create alias to this element's attribute definition array, see + // also $d_defs (global attribute definition array) + // DEFINITION CALL + $defs = $definition->info[$token->name]->attr; + + $attr_key = false; + $context->register('CurrentAttr', $attr_key); + + // iterate through all the attribute keypairs + // Watch out for name collisions: $key has previously been used + foreach ($attr as $attr_key => $value) { + + // call the definition + if (isset($defs[$attr_key])) { + // there is a local definition defined + if ($defs[$attr_key] === false) { + // We've explicitly been told not to allow this element. + // This is usually when there's a global definition + // that must be overridden. + // Theoretically speaking, we could have a + // AttrDef_DenyAll, but this is faster! + $result = false; + } else { + // validate according to the element's definition + $result = $defs[$attr_key]->validate( + $value, + $config, + $context + ); + } + } elseif (isset($d_defs[$attr_key])) { + // there is a global definition defined, validate according + // to the global definition + $result = $d_defs[$attr_key]->validate( + $value, + $config, + $context + ); + } else { + // system never heard of the attribute? DELETE! + $result = false; + } + + // put the results into effect + if ($result === false || $result === null) { + // this is a generic error message that should replaced + // with more specific ones when possible + if ($e) { + $e->send(E_ERROR, 'AttrValidator: Attribute removed'); + } + + // remove the attribute + unset($attr[$attr_key]); + } elseif (is_string($result)) { + // generally, if a substitution is happening, there + // was some sort of implicit correction going on. We'll + // delegate it to the attribute classes to say exactly what. + + // simple substitution + $attr[$attr_key] = $result; + } else { + // nothing happens + } + + // we'd also want slightly more complicated substitution + // involving an array as the return value, + // although we're not sure how colliding attributes would + // resolve (certain ones would be completely overriden, + // others would prepend themselves). + } + + $context->destroy('CurrentAttr'); + + // post transforms + + // global (error reporting untested) + foreach ($definition->info_attr_transform_post as $transform) { + $attr = $transform->transform($o = $attr, $config, $context); + if ($e) { + if ($attr != $o) { + $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } + } + } + + // local (error reporting untested) + foreach ($definition->info[$token->name]->attr_transform_post as $transform) { + $attr = $transform->transform($o = $attr, $config, $context); + if ($e) { + if ($attr != $o) { + $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } + } + } + + $token->attr = $attr; + + // destroy CurrentToken if we made it ourselves + if (!$current_token) { + $context->destroy('CurrentToken'); + } + + } + + +} + + + + + +// constants are slow, so we use as few as possible +if (!defined('HTMLPURIFIER_PREFIX')) { + define('HTMLPURIFIER_PREFIX', dirname(__FILE__) . '/standalone'); + set_include_path(HTMLPURIFIER_PREFIX . PATH_SEPARATOR . get_include_path()); +} + +// accomodations for versions earlier than 5.0.2 +// borrowed from PHP_Compat, LGPL licensed, by Aidan Lister +if (!defined('PHP_EOL')) { + switch (strtoupper(substr(PHP_OS, 0, 3))) { + case 'WIN': + define('PHP_EOL', "\r\n"); + break; + case 'DAR': + define('PHP_EOL', "\r"); + break; + default: + define('PHP_EOL', "\n"); + } +} + +/** + * Bootstrap class that contains meta-functionality for HTML Purifier such as + * the autoload function. + * + * @note + * This class may be used without any other files from HTML Purifier. + */ +class HTMLPurifier_Bootstrap +{ + + /** + * Autoload function for HTML Purifier + * @param string $class Class to load + * @return bool + */ + public static function autoload($class) + { + $file = HTMLPurifier_Bootstrap::getPath($class); + if (!$file) { + return false; + } + // Technically speaking, it should be ok and more efficient to + // just do 'require', but Antonio Parraga reports that with + // Zend extensions such as Zend debugger and APC, this invariant + // may be broken. Since we have efficient alternatives, pay + // the cost here and avoid the bug. + require_once HTMLPURIFIER_PREFIX . '/' . $file; + return true; + } + + /** + * Returns the path for a specific class. + * @param string $class Class path to get + * @return string + */ + public static function getPath($class) + { + if (strncmp('HTMLPurifier', $class, 12) !== 0) { + return false; + } + // Custom implementations + if (strncmp('HTMLPurifier_Language_', $class, 22) === 0) { + $code = str_replace('_', '-', substr($class, 22)); + $file = 'HTMLPurifier/Language/classes/' . $code . '.php'; + } else { + $file = str_replace('_', '/', $class) . '.php'; + } + if (!file_exists(HTMLPURIFIER_PREFIX . '/' . $file)) { + return false; + } + return $file; + } + + /** + * "Pre-registers" our autoloader on the SPL stack. + */ + public static function registerAutoload() + { + $autoload = array('HTMLPurifier_Bootstrap', 'autoload'); + if (($funcs = spl_autoload_functions()) === false) { + spl_autoload_register($autoload); + } elseif (function_exists('spl_autoload_unregister')) { + if (version_compare(PHP_VERSION, '5.3.0', '>=')) { + // prepend flag exists, no need for shenanigans + spl_autoload_register($autoload, true, true); + } else { + $buggy = version_compare(PHP_VERSION, '5.2.11', '<'); + $compat = version_compare(PHP_VERSION, '5.1.2', '<=') && + version_compare(PHP_VERSION, '5.1.0', '>='); + foreach ($funcs as $func) { + if ($buggy && is_array($func)) { + // :TRICKY: There are some compatibility issues and some + // places where we need to error out + $reflector = new ReflectionMethod($func[0], $func[1]); + if (!$reflector->isStatic()) { + throw new Exception( + 'HTML Purifier autoloader registrar is not compatible + with non-static object methods due to PHP Bug #44144; + Please do not use HTMLPurifier.autoload.php (or any + file that includes this file); instead, place the code: + spl_autoload_register(array(\'HTMLPurifier_Bootstrap\', \'autoload\')) + after your own autoloaders.' + ); + } + // Suprisingly, spl_autoload_register supports the + // Class::staticMethod callback format, although call_user_func doesn't + if ($compat) { + $func = implode('::', $func); + } + } + spl_autoload_unregister($func); + } + spl_autoload_register($autoload); + foreach ($funcs as $func) { + spl_autoload_register($func); + } + } + } + } +} + + + + + +/** + * Super-class for definition datatype objects, implements serialization + * functions for the class. + */ +abstract class HTMLPurifier_Definition +{ + + /** + * Has setup() been called yet? + * @type bool + */ + public $setup = false; + + /** + * If true, write out the final definition object to the cache after + * setup. This will be true only if all invocations to get a raw + * definition object are also optimized. This does not cause file + * system thrashing because on subsequent calls the cached object + * is used and any writes to the raw definition object are short + * circuited. See enduser-customize.html for the high-level + * picture. + * @type bool + */ + public $optimized = null; + + /** + * What type of definition is it? + * @type string + */ + public $type; + + /** + * Sets up the definition object into the final form, something + * not done by the constructor + * @param HTMLPurifier_Config $config + */ + abstract protected function doSetup($config); + + /** + * Setup function that aborts if already setup + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + if ($this->setup) { + return; + } + $this->setup = true; + $this->doSetup($config); + } +} + + + + + +/** + * Defines allowed CSS attributes and what their values are. + * @see HTMLPurifier_HTMLDefinition + */ +class HTMLPurifier_CSSDefinition extends HTMLPurifier_Definition +{ + + public $type = 'CSS'; + + /** + * Assoc array of attribute name to definition object. + * @type HTMLPurifier_AttrDef[] + */ + public $info = array(); + + /** + * Constructs the info array. The meat of this class. + * @param HTMLPurifier_Config $config + */ + protected function doSetup($config) + { + $this->info['text-align'] = new HTMLPurifier_AttrDef_Enum( + array('left', 'right', 'center', 'justify'), + false + ); + + $border_style = + $this->info['border-bottom-style'] = + $this->info['border-right-style'] = + $this->info['border-left-style'] = + $this->info['border-top-style'] = new HTMLPurifier_AttrDef_Enum( + array( + 'none', + 'hidden', + 'dotted', + 'dashed', + 'solid', + 'double', + 'groove', + 'ridge', + 'inset', + 'outset' + ), + false + ); + + $this->info['border-style'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_style); + + $this->info['clear'] = new HTMLPurifier_AttrDef_Enum( + array('none', 'left', 'right', 'both'), + false + ); + $this->info['float'] = new HTMLPurifier_AttrDef_Enum( + array('none', 'left', 'right'), + false + ); + $this->info['font-style'] = new HTMLPurifier_AttrDef_Enum( + array('normal', 'italic', 'oblique'), + false + ); + $this->info['font-variant'] = new HTMLPurifier_AttrDef_Enum( + array('normal', 'small-caps'), + false + ); + + $uri_or_none = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('none')), + new HTMLPurifier_AttrDef_CSS_URI() + ) + ); + + $this->info['list-style-position'] = new HTMLPurifier_AttrDef_Enum( + array('inside', 'outside'), + false + ); + $this->info['list-style-type'] = new HTMLPurifier_AttrDef_Enum( + array( + 'disc', + 'circle', + 'square', + 'decimal', + 'lower-roman', + 'upper-roman', + 'lower-alpha', + 'upper-alpha', + 'none' + ), + false + ); + $this->info['list-style-image'] = $uri_or_none; + + $this->info['list-style'] = new HTMLPurifier_AttrDef_CSS_ListStyle($config); + + $this->info['text-transform'] = new HTMLPurifier_AttrDef_Enum( + array('capitalize', 'uppercase', 'lowercase', 'none'), + false + ); + $this->info['color'] = new HTMLPurifier_AttrDef_CSS_Color(); + + $this->info['background-image'] = $uri_or_none; + $this->info['background-repeat'] = new HTMLPurifier_AttrDef_Enum( + array('repeat', 'repeat-x', 'repeat-y', 'no-repeat') + ); + $this->info['background-attachment'] = new HTMLPurifier_AttrDef_Enum( + array('scroll', 'fixed') + ); + $this->info['background-position'] = new HTMLPurifier_AttrDef_CSS_BackgroundPosition(); + + $border_color = + $this->info['border-top-color'] = + $this->info['border-bottom-color'] = + $this->info['border-left-color'] = + $this->info['border-right-color'] = + $this->info['background-color'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('transparent')), + new HTMLPurifier_AttrDef_CSS_Color() + ) + ); + + $this->info['background'] = new HTMLPurifier_AttrDef_CSS_Background($config); + + $this->info['border-color'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_color); + + $border_width = + $this->info['border-top-width'] = + $this->info['border-bottom-width'] = + $this->info['border-left-width'] = + $this->info['border-right-width'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('thin', 'medium', 'thick')), + new HTMLPurifier_AttrDef_CSS_Length('0') //disallow negative + ) + ); + + $this->info['border-width'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_width); + + $this->info['letter-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('normal')), + new HTMLPurifier_AttrDef_CSS_Length() + ) + ); + + $this->info['word-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('normal')), + new HTMLPurifier_AttrDef_CSS_Length() + ) + ); + + $this->info['font-size'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum( + array( + 'xx-small', + 'x-small', + 'small', + 'medium', + 'large', + 'x-large', + 'xx-large', + 'larger', + 'smaller' + ) + ), + new HTMLPurifier_AttrDef_CSS_Percentage(), + new HTMLPurifier_AttrDef_CSS_Length() + ) + ); + + $this->info['line-height'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('normal')), + new HTMLPurifier_AttrDef_CSS_Number(true), // no negatives + new HTMLPurifier_AttrDef_CSS_Length('0'), + new HTMLPurifier_AttrDef_CSS_Percentage(true) + ) + ); + + $margin = + $this->info['margin-top'] = + $this->info['margin-bottom'] = + $this->info['margin-left'] = + $this->info['margin-right'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length(), + new HTMLPurifier_AttrDef_CSS_Percentage(), + new HTMLPurifier_AttrDef_Enum(array('auto')) + ) + ); + + $this->info['margin'] = new HTMLPurifier_AttrDef_CSS_Multiple($margin); + + // non-negative + $padding = + $this->info['padding-top'] = + $this->info['padding-bottom'] = + $this->info['padding-left'] = + $this->info['padding-right'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length('0'), + new HTMLPurifier_AttrDef_CSS_Percentage(true) + ) + ); + + $this->info['padding'] = new HTMLPurifier_AttrDef_CSS_Multiple($padding); + + $this->info['text-indent'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length(), + new HTMLPurifier_AttrDef_CSS_Percentage() + ) + ); + + $trusted_wh = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length('0'), + new HTMLPurifier_AttrDef_CSS_Percentage(true), + new HTMLPurifier_AttrDef_Enum(array('auto')) + ) + ); + $max = $config->get('CSS.MaxImgLength'); + + $this->info['width'] = + $this->info['height'] = + $max === null ? + $trusted_wh : + new HTMLPurifier_AttrDef_Switch( + 'img', + // For img tags: + new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length('0', $max), + new HTMLPurifier_AttrDef_Enum(array('auto')) + ) + ), + // For everyone else: + $trusted_wh + ); + + $this->info['text-decoration'] = new HTMLPurifier_AttrDef_CSS_TextDecoration(); + + $this->info['font-family'] = new HTMLPurifier_AttrDef_CSS_FontFamily(); + + // this could use specialized code + $this->info['font-weight'] = new HTMLPurifier_AttrDef_Enum( + array( + 'normal', + 'bold', + 'bolder', + 'lighter', + '100', + '200', + '300', + '400', + '500', + '600', + '700', + '800', + '900' + ), + false + ); + + // MUST be called after other font properties, as it references + // a CSSDefinition object + $this->info['font'] = new HTMLPurifier_AttrDef_CSS_Font($config); + + // same here + $this->info['border'] = + $this->info['border-bottom'] = + $this->info['border-top'] = + $this->info['border-left'] = + $this->info['border-right'] = new HTMLPurifier_AttrDef_CSS_Border($config); + + $this->info['border-collapse'] = new HTMLPurifier_AttrDef_Enum( + array('collapse', 'separate') + ); + + $this->info['caption-side'] = new HTMLPurifier_AttrDef_Enum( + array('top', 'bottom') + ); + + $this->info['table-layout'] = new HTMLPurifier_AttrDef_Enum( + array('auto', 'fixed') + ); + + $this->info['vertical-align'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum( + array( + 'baseline', + 'sub', + 'super', + 'top', + 'text-top', + 'middle', + 'bottom', + 'text-bottom' + ) + ), + new HTMLPurifier_AttrDef_CSS_Length(), + new HTMLPurifier_AttrDef_CSS_Percentage() + ) + ); + + $this->info['border-spacing'] = new HTMLPurifier_AttrDef_CSS_Multiple(new HTMLPurifier_AttrDef_CSS_Length(), 2); + + // These CSS properties don't work on many browsers, but we live + // in THE FUTURE! + $this->info['white-space'] = new HTMLPurifier_AttrDef_Enum( + array('nowrap', 'normal', 'pre', 'pre-wrap', 'pre-line') + ); + + if ($config->get('CSS.Proprietary')) { + $this->doSetupProprietary($config); + } + + if ($config->get('CSS.AllowTricky')) { + $this->doSetupTricky($config); + } + + if ($config->get('CSS.Trusted')) { + $this->doSetupTrusted($config); + } + + $allow_important = $config->get('CSS.AllowImportant'); + // wrap all attr-defs with decorator that handles !important + foreach ($this->info as $k => $v) { + $this->info[$k] = new HTMLPurifier_AttrDef_CSS_ImportantDecorator($v, $allow_important); + } + + $this->setupConfigStuff($config); + } + + /** + * @param HTMLPurifier_Config $config + */ + protected function doSetupProprietary($config) + { + // Internet Explorer only scrollbar colors + $this->info['scrollbar-arrow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-base-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-darkshadow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-face-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-highlight-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-shadow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + + // technically not proprietary, but CSS3, and no one supports it + $this->info['opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + $this->info['-moz-opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + $this->info['-khtml-opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + + // only opacity, for now + $this->info['filter'] = new HTMLPurifier_AttrDef_CSS_Filter(); + + // more CSS3 + $this->info['page-break-after'] = + $this->info['page-break-before'] = new HTMLPurifier_AttrDef_Enum( + array( + 'auto', + 'always', + 'avoid', + 'left', + 'right' + ) + ); + $this->info['page-break-inside'] = new HTMLPurifier_AttrDef_Enum(array('auto', 'avoid')); + + } + + /** + * @param HTMLPurifier_Config $config + */ + protected function doSetupTricky($config) + { + $this->info['display'] = new HTMLPurifier_AttrDef_Enum( + array( + 'inline', + 'block', + 'list-item', + 'run-in', + 'compact', + 'marker', + 'table', + 'inline-block', + 'inline-table', + 'table-row-group', + 'table-header-group', + 'table-footer-group', + 'table-row', + 'table-column-group', + 'table-column', + 'table-cell', + 'table-caption', + 'none' + ) + ); + $this->info['visibility'] = new HTMLPurifier_AttrDef_Enum( + array('visible', 'hidden', 'collapse') + ); + $this->info['overflow'] = new HTMLPurifier_AttrDef_Enum(array('visible', 'hidden', 'auto', 'scroll')); + } + + /** + * @param HTMLPurifier_Config $config + */ + protected function doSetupTrusted($config) + { + $this->info['position'] = new HTMLPurifier_AttrDef_Enum( + array('static', 'relative', 'absolute', 'fixed') + ); + $this->info['top'] = + $this->info['left'] = + $this->info['right'] = + $this->info['bottom'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length(), + new HTMLPurifier_AttrDef_CSS_Percentage(), + new HTMLPurifier_AttrDef_Enum(array('auto')), + ) + ); + $this->info['z-index'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Integer(), + new HTMLPurifier_AttrDef_Enum(array('auto')), + ) + ); + } + + /** + * Performs extra config-based processing. Based off of + * HTMLPurifier_HTMLDefinition. + * @param HTMLPurifier_Config $config + * @todo Refactor duplicate elements into common class (probably using + * composition, not inheritance). + */ + protected function setupConfigStuff($config) + { + // setup allowed elements + $support = "(for information on implementing this, see the " . + "support forums) "; + $allowed_properties = $config->get('CSS.AllowedProperties'); + if ($allowed_properties !== null) { + foreach ($this->info as $name => $d) { + if (!isset($allowed_properties[$name])) { + unset($this->info[$name]); + } + unset($allowed_properties[$name]); + } + // emit errors + foreach ($allowed_properties as $name => $d) { + // :TODO: Is this htmlspecialchars() call really necessary? + $name = htmlspecialchars($name); + trigger_error("Style attribute '$name' is not supported $support", E_USER_WARNING); + } + } + + $forbidden_properties = $config->get('CSS.ForbiddenProperties'); + if ($forbidden_properties !== null) { + foreach ($this->info as $name => $d) { + if (isset($forbidden_properties[$name])) { + unset($this->info[$name]); + } + } + } + } +} + + + + + +/** + * Defines allowed child nodes and validates nodes against it. + */ +abstract class HTMLPurifier_ChildDef +{ + /** + * Type of child definition, usually right-most part of class name lowercase. + * Used occasionally in terms of context. + * @type string + */ + public $type; + + /** + * Indicates whether or not an empty array of children is okay. + * + * This is necessary for redundant checking when changes affecting + * a child node may cause a parent node to now be disallowed. + * @type bool + */ + public $allow_empty; + + /** + * Lookup array of all elements that this definition could possibly allow. + * @type array + */ + public $elements = array(); + + /** + * Get lookup of tag names that should not close this element automatically. + * All other elements will do so. + * @param HTMLPurifier_Config $config HTMLPurifier_Config object + * @return array + */ + public function getAllowedElements($config) + { + return $this->elements; + } + + /** + * Validates nodes according to definition and returns modification. + * + * @param HTMLPurifier_Node[] $children Array of HTMLPurifier_Node + * @param HTMLPurifier_Config $config HTMLPurifier_Config object + * @param HTMLPurifier_Context $context HTMLPurifier_Context object + * @return bool|array true to leave nodes as is, false to remove parent node, array of replacement children + */ + abstract public function validateChildren($children, $config, $context); +} + + + + + +/** + * Configuration object that triggers customizable behavior. + * + * @warning This class is strongly defined: that means that the class + * will fail if an undefined directive is retrieved or set. + * + * @note Many classes that could (although many times don't) use the + * configuration object make it a mandatory parameter. This is + * because a configuration object should always be forwarded, + * otherwise, you run the risk of missing a parameter and then + * being stumped when a configuration directive doesn't work. + * + * @todo Reconsider some of the public member variables + */ +class HTMLPurifier_Config +{ + + /** + * HTML Purifier's version + * @type string + */ + public $version = '4.6.0'; + + /** + * Whether or not to automatically finalize + * the object if a read operation is done. + * @type bool + */ + public $autoFinalize = true; + + // protected member variables + + /** + * Namespace indexed array of serials for specific namespaces. + * @see getSerial() for more info. + * @type string[] + */ + protected $serials = array(); + + /** + * Serial for entire configuration object. + * @type string + */ + protected $serial; + + /** + * Parser for variables. + * @type HTMLPurifier_VarParser_Flexible + */ + protected $parser = null; + + /** + * Reference HTMLPurifier_ConfigSchema for value checking. + * @type HTMLPurifier_ConfigSchema + * @note This is public for introspective purposes. Please don't + * abuse! + */ + public $def; + + /** + * Indexed array of definitions. + * @type HTMLPurifier_Definition[] + */ + protected $definitions; + + /** + * Whether or not config is finalized. + * @type bool + */ + protected $finalized = false; + + /** + * Property list containing configuration directives. + * @type array + */ + protected $plist; + + /** + * Whether or not a set is taking place due to an alias lookup. + * @type bool + */ + private $aliasMode; + + /** + * Set to false if you do not want line and file numbers in errors. + * (useful when unit testing). This will also compress some errors + * and exceptions. + * @type bool + */ + public $chatty = true; + + /** + * Current lock; only gets to this namespace are allowed. + * @type string + */ + private $lock; + + /** + * Constructor + * @param HTMLPurifier_ConfigSchema $definition ConfigSchema that defines + * what directives are allowed. + * @param HTMLPurifier_PropertyList $parent + */ + public function __construct($definition, $parent = null) + { + $parent = $parent ? $parent : $definition->defaultPlist; + $this->plist = new HTMLPurifier_PropertyList($parent); + $this->def = $definition; // keep a copy around for checking + $this->parser = new HTMLPurifier_VarParser_Flexible(); + } + + /** + * Convenience constructor that creates a config object based on a mixed var + * @param mixed $config Variable that defines the state of the config + * object. Can be: a HTMLPurifier_Config() object, + * an array of directives based on loadArray(), + * or a string filename of an ini file. + * @param HTMLPurifier_ConfigSchema $schema Schema object + * @return HTMLPurifier_Config Configured object + */ + public static function create($config, $schema = null) + { + if ($config instanceof HTMLPurifier_Config) { + // pass-through + return $config; + } + if (!$schema) { + $ret = HTMLPurifier_Config::createDefault(); + } else { + $ret = new HTMLPurifier_Config($schema); + } + if (is_string($config)) { + $ret->loadIni($config); + } elseif (is_array($config)) $ret->loadArray($config); + return $ret; + } + + /** + * Creates a new config object that inherits from a previous one. + * @param HTMLPurifier_Config $config Configuration object to inherit from. + * @return HTMLPurifier_Config object with $config as its parent. + */ + public static function inherit(HTMLPurifier_Config $config) + { + return new HTMLPurifier_Config($config->def, $config->plist); + } + + /** + * Convenience constructor that creates a default configuration object. + * @return HTMLPurifier_Config default object. + */ + public static function createDefault() + { + $definition = HTMLPurifier_ConfigSchema::instance(); + $config = new HTMLPurifier_Config($definition); + return $config; + } + + /** + * Retrieves a value from the configuration. + * + * @param string $key String key + * @param mixed $a + * + * @return mixed + */ + public function get($key, $a = null) + { + if ($a !== null) { + $this->triggerError( + "Using deprecated API: use \$config->get('$key.$a') instead", + E_USER_WARNING + ); + $key = "$key.$a"; + } + if (!$this->finalized) { + $this->autoFinalize(); + } + if (!isset($this->def->info[$key])) { + // can't add % due to SimpleTest bug + $this->triggerError( + 'Cannot retrieve value of undefined directive ' . htmlspecialchars($key), + E_USER_WARNING + ); + return; + } + if (isset($this->def->info[$key]->isAlias)) { + $d = $this->def->info[$key]; + $this->triggerError( + 'Cannot get value from aliased directive, use real name ' . $d->key, + E_USER_ERROR + ); + return; + } + if ($this->lock) { + list($ns) = explode('.', $key); + if ($ns !== $this->lock) { + $this->triggerError( + 'Cannot get value of namespace ' . $ns . ' when lock for ' . + $this->lock . + ' is active, this probably indicates a Definition setup method ' . + 'is accessing directives that are not within its namespace', + E_USER_ERROR + ); + return; + } + } + return $this->plist->get($key); + } + + /** + * Retrieves an array of directives to values from a given namespace + * + * @param string $namespace String namespace + * + * @return array + */ + public function getBatch($namespace) + { + if (!$this->finalized) { + $this->autoFinalize(); + } + $full = $this->getAll(); + if (!isset($full[$namespace])) { + $this->triggerError( + 'Cannot retrieve undefined namespace ' . + htmlspecialchars($namespace), + E_USER_WARNING + ); + return; + } + return $full[$namespace]; + } + + /** + * Returns a SHA-1 signature of a segment of the configuration object + * that uniquely identifies that particular configuration + * + * @param string $namespace Namespace to get serial for + * + * @return string + * @note Revision is handled specially and is removed from the batch + * before processing! + */ + public function getBatchSerial($namespace) + { + if (empty($this->serials[$namespace])) { + $batch = $this->getBatch($namespace); + unset($batch['DefinitionRev']); + $this->serials[$namespace] = sha1(serialize($batch)); + } + return $this->serials[$namespace]; + } + + /** + * Returns a SHA-1 signature for the entire configuration object + * that uniquely identifies that particular configuration + * + * @return string + */ + public function getSerial() + { + if (empty($this->serial)) { + $this->serial = sha1(serialize($this->getAll())); + } + return $this->serial; + } + + /** + * Retrieves all directives, organized by namespace + * + * @warning This is a pretty inefficient function, avoid if you can + */ + public function getAll() + { + if (!$this->finalized) { + $this->autoFinalize(); + } + $ret = array(); + foreach ($this->plist->squash() as $name => $value) { + list($ns, $key) = explode('.', $name, 2); + $ret[$ns][$key] = $value; + } + return $ret; + } + + /** + * Sets a value to configuration. + * + * @param string $key key + * @param mixed $value value + * @param mixed $a + */ + public function set($key, $value, $a = null) + { + if (strpos($key, '.') === false) { + $namespace = $key; + $directive = $value; + $value = $a; + $key = "$key.$directive"; + $this->triggerError("Using deprecated API: use \$config->set('$key', ...) instead", E_USER_NOTICE); + } else { + list($namespace) = explode('.', $key); + } + if ($this->isFinalized('Cannot set directive after finalization')) { + return; + } + if (!isset($this->def->info[$key])) { + $this->triggerError( + 'Cannot set undefined directive ' . htmlspecialchars($key) . ' to value', + E_USER_WARNING + ); + return; + } + $def = $this->def->info[$key]; + + if (isset($def->isAlias)) { + if ($this->aliasMode) { + $this->triggerError( + 'Double-aliases not allowed, please fix '. + 'ConfigSchema bug with' . $key, + E_USER_ERROR + ); + return; + } + $this->aliasMode = true; + $this->set($def->key, $value); + $this->aliasMode = false; + $this->triggerError("$key is an alias, preferred directive name is {$def->key}", E_USER_NOTICE); + return; + } + + // Raw type might be negative when using the fully optimized form + // of stdclass, which indicates allow_null == true + $rtype = is_int($def) ? $def : $def->type; + if ($rtype < 0) { + $type = -$rtype; + $allow_null = true; + } else { + $type = $rtype; + $allow_null = isset($def->allow_null); + } + + try { + $value = $this->parser->parse($value, $type, $allow_null); + } catch (HTMLPurifier_VarParserException $e) { + $this->triggerError( + 'Value for ' . $key . ' is of invalid type, should be ' . + HTMLPurifier_VarParser::getTypeName($type), + E_USER_WARNING + ); + return; + } + if (is_string($value) && is_object($def)) { + // resolve value alias if defined + if (isset($def->aliases[$value])) { + $value = $def->aliases[$value]; + } + // check to see if the value is allowed + if (isset($def->allowed) && !isset($def->allowed[$value])) { + $this->triggerError( + 'Value not supported, valid values are: ' . + $this->_listify($def->allowed), + E_USER_WARNING + ); + return; + } + } + $this->plist->set($key, $value); + + // reset definitions if the directives they depend on changed + // this is a very costly process, so it's discouraged + // with finalization + if ($namespace == 'HTML' || $namespace == 'CSS' || $namespace == 'URI') { + $this->definitions[$namespace] = null; + } + + $this->serials[$namespace] = false; + } + + /** + * Convenience function for error reporting + * + * @param array $lookup + * + * @return string + */ + private function _listify($lookup) + { + $list = array(); + foreach ($lookup as $name => $b) { + $list[] = $name; + } + return implode(', ', $list); + } + + /** + * Retrieves object reference to the HTML definition. + * + * @param bool $raw Return a copy that has not been setup yet. Must be + * called before it's been setup, otherwise won't work. + * @param bool $optimized If true, this method may return null, to + * indicate that a cached version of the modified + * definition object is available and no further edits + * are necessary. Consider using + * maybeGetRawHTMLDefinition, which is more explicitly + * named, instead. + * + * @return HTMLPurifier_HTMLDefinition + */ + public function getHTMLDefinition($raw = false, $optimized = false) + { + return $this->getDefinition('HTML', $raw, $optimized); + } + + /** + * Retrieves object reference to the CSS definition + * + * @param bool $raw Return a copy that has not been setup yet. Must be + * called before it's been setup, otherwise won't work. + * @param bool $optimized If true, this method may return null, to + * indicate that a cached version of the modified + * definition object is available and no further edits + * are necessary. Consider using + * maybeGetRawCSSDefinition, which is more explicitly + * named, instead. + * + * @return HTMLPurifier_CSSDefinition + */ + public function getCSSDefinition($raw = false, $optimized = false) + { + return $this->getDefinition('CSS', $raw, $optimized); + } + + /** + * Retrieves object reference to the URI definition + * + * @param bool $raw Return a copy that has not been setup yet. Must be + * called before it's been setup, otherwise won't work. + * @param bool $optimized If true, this method may return null, to + * indicate that a cached version of the modified + * definition object is available and no further edits + * are necessary. Consider using + * maybeGetRawURIDefinition, which is more explicitly + * named, instead. + * + * @return HTMLPurifier_URIDefinition + */ + public function getURIDefinition($raw = false, $optimized = false) + { + return $this->getDefinition('URI', $raw, $optimized); + } + + /** + * Retrieves a definition + * + * @param string $type Type of definition: HTML, CSS, etc + * @param bool $raw Whether or not definition should be returned raw + * @param bool $optimized Only has an effect when $raw is true. Whether + * or not to return null if the result is already present in + * the cache. This is off by default for backwards + * compatibility reasons, but you need to do things this + * way in order to ensure that caching is done properly. + * Check out enduser-customize.html for more details. + * We probably won't ever change this default, as much as the + * maybe semantics is the "right thing to do." + * + * @throws HTMLPurifier_Exception + * @return HTMLPurifier_Definition + */ + public function getDefinition($type, $raw = false, $optimized = false) + { + if ($optimized && !$raw) { + throw new HTMLPurifier_Exception("Cannot set optimized = true when raw = false"); + } + if (!$this->finalized) { + $this->autoFinalize(); + } + // temporarily suspend locks, so we can handle recursive definition calls + $lock = $this->lock; + $this->lock = null; + $factory = HTMLPurifier_DefinitionCacheFactory::instance(); + $cache = $factory->create($type, $this); + $this->lock = $lock; + if (!$raw) { + // full definition + // --------------- + // check if definition is in memory + if (!empty($this->definitions[$type])) { + $def = $this->definitions[$type]; + // check if the definition is setup + if ($def->setup) { + return $def; + } else { + $def->setup($this); + if ($def->optimized) { + $cache->add($def, $this); + } + return $def; + } + } + // check if definition is in cache + $def = $cache->get($this); + if ($def) { + // definition in cache, save to memory and return it + $this->definitions[$type] = $def; + return $def; + } + // initialize it + $def = $this->initDefinition($type); + // set it up + $this->lock = $type; + $def->setup($this); + $this->lock = null; + // save in cache + $cache->add($def, $this); + // return it + return $def; + } else { + // raw definition + // -------------- + // check preconditions + $def = null; + if ($optimized) { + if (is_null($this->get($type . '.DefinitionID'))) { + // fatally error out if definition ID not set + throw new HTMLPurifier_Exception( + "Cannot retrieve raw version without specifying %$type.DefinitionID" + ); + } + } + if (!empty($this->definitions[$type])) { + $def = $this->definitions[$type]; + if ($def->setup && !$optimized) { + $extra = $this->chatty ? + " (try moving this code block earlier in your initialization)" : + ""; + throw new HTMLPurifier_Exception( + "Cannot retrieve raw definition after it has already been setup" . + $extra + ); + } + if ($def->optimized === null) { + $extra = $this->chatty ? " (try flushing your cache)" : ""; + throw new HTMLPurifier_Exception( + "Optimization status of definition is unknown" . $extra + ); + } + if ($def->optimized !== $optimized) { + $msg = $optimized ? "optimized" : "unoptimized"; + $extra = $this->chatty ? + " (this backtrace is for the first inconsistent call, which was for a $msg raw definition)" + : ""; + throw new HTMLPurifier_Exception( + "Inconsistent use of optimized and unoptimized raw definition retrievals" . $extra + ); + } + } + // check if definition was in memory + if ($def) { + if ($def->setup) { + // invariant: $optimized === true (checked above) + return null; + } else { + return $def; + } + } + // if optimized, check if definition was in cache + // (because we do the memory check first, this formulation + // is prone to cache slamming, but I think + // guaranteeing that either /all/ of the raw + // setup code or /none/ of it is run is more important.) + if ($optimized) { + // This code path only gets run once; once we put + // something in $definitions (which is guaranteed by the + // trailing code), we always short-circuit above. + $def = $cache->get($this); + if ($def) { + // save the full definition for later, but don't + // return it yet + $this->definitions[$type] = $def; + return null; + } + } + // check invariants for creation + if (!$optimized) { + if (!is_null($this->get($type . '.DefinitionID'))) { + if ($this->chatty) { + $this->triggerError( + 'Due to a documentation error in previous version of HTML Purifier, your ' . + 'definitions are not being cached. If this is OK, you can remove the ' . + '%$type.DefinitionRev and %$type.DefinitionID declaration. Otherwise, ' . + 'modify your code to use maybeGetRawDefinition, and test if the returned ' . + 'value is null before making any edits (if it is null, that means that a ' . + 'cached version is available, and no raw operations are necessary). See ' . + '' . + 'Customize for more details', + E_USER_WARNING + ); + } else { + $this->triggerError( + "Useless DefinitionID declaration", + E_USER_WARNING + ); + } + } + } + // initialize it + $def = $this->initDefinition($type); + $def->optimized = $optimized; + return $def; + } + throw new HTMLPurifier_Exception("The impossible happened!"); + } + + /** + * Initialise definition + * + * @param string $type What type of definition to create + * + * @return HTMLPurifier_CSSDefinition|HTMLPurifier_HTMLDefinition|HTMLPurifier_URIDefinition + * @throws HTMLPurifier_Exception + */ + private function initDefinition($type) + { + // quick checks failed, let's create the object + if ($type == 'HTML') { + $def = new HTMLPurifier_HTMLDefinition(); + } elseif ($type == 'CSS') { + $def = new HTMLPurifier_CSSDefinition(); + } elseif ($type == 'URI') { + $def = new HTMLPurifier_URIDefinition(); + } else { + throw new HTMLPurifier_Exception( + "Definition of $type type not supported" + ); + } + $this->definitions[$type] = $def; + return $def; + } + + public function maybeGetRawDefinition($name) + { + return $this->getDefinition($name, true, true); + } + + public function maybeGetRawHTMLDefinition() + { + return $this->getDefinition('HTML', true, true); + } + + public function maybeGetRawCSSDefinition() + { + return $this->getDefinition('CSS', true, true); + } + + public function maybeGetRawURIDefinition() + { + return $this->getDefinition('URI', true, true); + } + + /** + * Loads configuration values from an array with the following structure: + * Namespace.Directive => Value + * + * @param array $config_array Configuration associative array + */ + public function loadArray($config_array) + { + if ($this->isFinalized('Cannot load directives after finalization')) { + return; + } + foreach ($config_array as $key => $value) { + $key = str_replace('_', '.', $key); + if (strpos($key, '.') !== false) { + $this->set($key, $value); + } else { + $namespace = $key; + $namespace_values = $value; + foreach ($namespace_values as $directive => $value2) { + $this->set($namespace .'.'. $directive, $value2); + } + } + } + } + + /** + * Returns a list of array(namespace, directive) for all directives + * that are allowed in a web-form context as per an allowed + * namespaces/directives list. + * + * @param array $allowed List of allowed namespaces/directives + * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy + * + * @return array + */ + public static function getAllowedDirectivesForForm($allowed, $schema = null) + { + if (!$schema) { + $schema = HTMLPurifier_ConfigSchema::instance(); + } + if ($allowed !== true) { + if (is_string($allowed)) { + $allowed = array($allowed); + } + $allowed_ns = array(); + $allowed_directives = array(); + $blacklisted_directives = array(); + foreach ($allowed as $ns_or_directive) { + if (strpos($ns_or_directive, '.') !== false) { + // directive + if ($ns_or_directive[0] == '-') { + $blacklisted_directives[substr($ns_or_directive, 1)] = true; + } else { + $allowed_directives[$ns_or_directive] = true; + } + } else { + // namespace + $allowed_ns[$ns_or_directive] = true; + } + } + } + $ret = array(); + foreach ($schema->info as $key => $def) { + list($ns, $directive) = explode('.', $key, 2); + if ($allowed !== true) { + if (isset($blacklisted_directives["$ns.$directive"])) { + continue; + } + if (!isset($allowed_directives["$ns.$directive"]) && !isset($allowed_ns[$ns])) { + continue; + } + } + if (isset($def->isAlias)) { + continue; + } + if ($directive == 'DefinitionID' || $directive == 'DefinitionRev') { + continue; + } + $ret[] = array($ns, $directive); + } + return $ret; + } + + /** + * Loads configuration values from $_GET/$_POST that were posted + * via ConfigForm + * + * @param array $array $_GET or $_POST array to import + * @param string|bool $index Index/name that the config variables are in + * @param array|bool $allowed List of allowed namespaces/directives + * @param bool $mq_fix Boolean whether or not to enable magic quotes fix + * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy + * + * @return mixed + */ + public static function loadArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) + { + $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $schema); + $config = HTMLPurifier_Config::create($ret, $schema); + return $config; + } + + /** + * Merges in configuration values from $_GET/$_POST to object. NOT STATIC. + * + * @param array $array $_GET or $_POST array to import + * @param string|bool $index Index/name that the config variables are in + * @param array|bool $allowed List of allowed namespaces/directives + * @param bool $mq_fix Boolean whether or not to enable magic quotes fix + */ + public function mergeArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true) + { + $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $this->def); + $this->loadArray($ret); + } + + /** + * Prepares an array from a form into something usable for the more + * strict parts of HTMLPurifier_Config + * + * @param array $array $_GET or $_POST array to import + * @param string|bool $index Index/name that the config variables are in + * @param array|bool $allowed List of allowed namespaces/directives + * @param bool $mq_fix Boolean whether or not to enable magic quotes fix + * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy + * + * @return array + */ + public static function prepareArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) + { + if ($index !== false) { + $array = (isset($array[$index]) && is_array($array[$index])) ? $array[$index] : array(); + } + $mq = $mq_fix && function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc(); + + $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $schema); + $ret = array(); + foreach ($allowed as $key) { + list($ns, $directive) = $key; + $skey = "$ns.$directive"; + if (!empty($array["Null_$skey"])) { + $ret[$ns][$directive] = null; + continue; + } + if (!isset($array[$skey])) { + continue; + } + $value = $mq ? stripslashes($array[$skey]) : $array[$skey]; + $ret[$ns][$directive] = $value; + } + return $ret; + } + + /** + * Loads configuration values from an ini file + * + * @param string $filename Name of ini file + */ + public function loadIni($filename) + { + if ($this->isFinalized('Cannot load directives after finalization')) { + return; + } + $array = parse_ini_file($filename, true); + $this->loadArray($array); + } + + /** + * Checks whether or not the configuration object is finalized. + * + * @param string|bool $error String error message, or false for no error + * + * @return bool + */ + public function isFinalized($error = false) + { + if ($this->finalized && $error) { + $this->triggerError($error, E_USER_ERROR); + } + return $this->finalized; + } + + /** + * Finalizes configuration only if auto finalize is on and not + * already finalized + */ + public function autoFinalize() + { + if ($this->autoFinalize) { + $this->finalize(); + } else { + $this->plist->squash(true); + } + } + + /** + * Finalizes a configuration object, prohibiting further change + */ + public function finalize() + { + $this->finalized = true; + $this->parser = null; + } + + /** + * Produces a nicely formatted error message by supplying the + * stack frame information OUTSIDE of HTMLPurifier_Config. + * + * @param string $msg An error message + * @param int $no An error number + */ + protected function triggerError($msg, $no) + { + // determine previous stack frame + $extra = ''; + if ($this->chatty) { + $trace = debug_backtrace(); + // zip(tail(trace), trace) -- but PHP is not Haskell har har + for ($i = 0, $c = count($trace); $i < $c - 1; $i++) { + // XXX this is not correct on some versions of HTML Purifier + if ($trace[$i + 1]['class'] === 'HTMLPurifier_Config') { + continue; + } + $frame = $trace[$i]; + $extra = " invoked on line {$frame['line']} in file {$frame['file']}"; + break; + } + } + trigger_error($msg . $extra, $no); + } + + /** + * Returns a serialized form of the configuration object that can + * be reconstituted. + * + * @return string + */ + public function serialize() + { + $this->getDefinition('HTML'); + $this->getDefinition('CSS'); + $this->getDefinition('URI'); + return serialize($this); + } + +} + + + + + +/** + * Configuration definition, defines directives and their defaults. + */ +class HTMLPurifier_ConfigSchema +{ + /** + * Defaults of the directives and namespaces. + * @type array + * @note This shares the exact same structure as HTMLPurifier_Config::$conf + */ + public $defaults = array(); + + /** + * The default property list. Do not edit this property list. + * @type array + */ + public $defaultPlist; + + /** + * Definition of the directives. + * The structure of this is: + * + * array( + * 'Namespace' => array( + * 'Directive' => new stdclass(), + * ) + * ) + * + * The stdclass may have the following properties: + * + * - If isAlias isn't set: + * - type: Integer type of directive, see HTMLPurifier_VarParser for definitions + * - allow_null: If set, this directive allows null values + * - aliases: If set, an associative array of value aliases to real values + * - allowed: If set, a lookup array of allowed (string) values + * - If isAlias is set: + * - namespace: Namespace this directive aliases to + * - name: Directive name this directive aliases to + * + * In certain degenerate cases, stdclass will actually be an integer. In + * that case, the value is equivalent to an stdclass with the type + * property set to the integer. If the integer is negative, type is + * equal to the absolute value of integer, and allow_null is true. + * + * This class is friendly with HTMLPurifier_Config. If you need introspection + * about the schema, you're better of using the ConfigSchema_Interchange, + * which uses more memory but has much richer information. + * @type array + */ + public $info = array(); + + /** + * Application-wide singleton + * @type HTMLPurifier_ConfigSchema + */ + protected static $singleton; + + public function __construct() + { + $this->defaultPlist = new HTMLPurifier_PropertyList(); + } + + /** + * Unserializes the default ConfigSchema. + * @return HTMLPurifier_ConfigSchema + */ + public static function makeFromSerial() + { + $contents = file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema.ser'); + $r = unserialize($contents); + if (!$r) { + $hash = sha1($contents); + trigger_error("Unserialization of configuration schema failed, sha1 of file was $hash", E_USER_ERROR); + } + return $r; + } + + /** + * Retrieves an instance of the application-wide configuration definition. + * @param HTMLPurifier_ConfigSchema $prototype + * @return HTMLPurifier_ConfigSchema + */ + public static function instance($prototype = null) + { + if ($prototype !== null) { + HTMLPurifier_ConfigSchema::$singleton = $prototype; + } elseif (HTMLPurifier_ConfigSchema::$singleton === null || $prototype === true) { + HTMLPurifier_ConfigSchema::$singleton = HTMLPurifier_ConfigSchema::makeFromSerial(); + } + return HTMLPurifier_ConfigSchema::$singleton; + } + + /** + * Defines a directive for configuration + * @warning Will fail of directive's namespace is defined. + * @warning This method's signature is slightly different from the legacy + * define() static method! Beware! + * @param string $key Name of directive + * @param mixed $default Default value of directive + * @param string $type Allowed type of the directive. See + * HTMLPurifier_DirectiveDef::$type for allowed values + * @param bool $allow_null Whether or not to allow null values + */ + public function add($key, $default, $type, $allow_null) + { + $obj = new stdclass(); + $obj->type = is_int($type) ? $type : HTMLPurifier_VarParser::$types[$type]; + if ($allow_null) { + $obj->allow_null = true; + } + $this->info[$key] = $obj; + $this->defaults[$key] = $default; + $this->defaultPlist->set($key, $default); + } + + /** + * Defines a directive value alias. + * + * Directive value aliases are convenient for developers because it lets + * them set a directive to several values and get the same result. + * @param string $key Name of Directive + * @param array $aliases Hash of aliased values to the real alias + */ + public function addValueAliases($key, $aliases) + { + if (!isset($this->info[$key]->aliases)) { + $this->info[$key]->aliases = array(); + } + foreach ($aliases as $alias => $real) { + $this->info[$key]->aliases[$alias] = $real; + } + } + + /** + * Defines a set of allowed values for a directive. + * @warning This is slightly different from the corresponding static + * method definition. + * @param string $key Name of directive + * @param array $allowed Lookup array of allowed values + */ + public function addAllowedValues($key, $allowed) + { + $this->info[$key]->allowed = $allowed; + } + + /** + * Defines a directive alias for backwards compatibility + * @param string $key Directive that will be aliased + * @param string $new_key Directive that the alias will be to + */ + public function addAlias($key, $new_key) + { + $obj = new stdclass; + $obj->key = $new_key; + $obj->isAlias = true; + $this->info[$key] = $obj; + } + + /** + * Replaces any stdclass that only has the type property with type integer. + */ + public function postProcess() + { + foreach ($this->info as $key => $v) { + if (count((array) $v) == 1) { + $this->info[$key] = $v->type; + } elseif (count((array) $v) == 2 && isset($v->allow_null)) { + $this->info[$key] = -$v->type; + } + } + } +} + + + + + +/** + * @todo Unit test + */ +class HTMLPurifier_ContentSets +{ + + /** + * List of content set strings (pipe separators) indexed by name. + * @type array + */ + public $info = array(); + + /** + * List of content set lookups (element => true) indexed by name. + * @type array + * @note This is in HTMLPurifier_HTMLDefinition->info_content_sets + */ + public $lookup = array(); + + /** + * Synchronized list of defined content sets (keys of info). + * @type array + */ + protected $keys = array(); + /** + * Synchronized list of defined content values (values of info). + * @type array + */ + protected $values = array(); + + /** + * Merges in module's content sets, expands identifiers in the content + * sets and populates the keys, values and lookup member variables. + * @param HTMLPurifier_HTMLModule[] $modules List of HTMLPurifier_HTMLModule + */ + public function __construct($modules) + { + if (!is_array($modules)) { + $modules = array($modules); + } + // populate content_sets based on module hints + // sorry, no way of overloading + foreach ($modules as $module) { + foreach ($module->content_sets as $key => $value) { + $temp = $this->convertToLookup($value); + if (isset($this->lookup[$key])) { + // add it into the existing content set + $this->lookup[$key] = array_merge($this->lookup[$key], $temp); + } else { + $this->lookup[$key] = $temp; + } + } + } + $old_lookup = false; + while ($old_lookup !== $this->lookup) { + $old_lookup = $this->lookup; + foreach ($this->lookup as $i => $set) { + $add = array(); + foreach ($set as $element => $x) { + if (isset($this->lookup[$element])) { + $add += $this->lookup[$element]; + unset($this->lookup[$i][$element]); + } + } + $this->lookup[$i] += $add; + } + } + + foreach ($this->lookup as $key => $lookup) { + $this->info[$key] = implode(' | ', array_keys($lookup)); + } + $this->keys = array_keys($this->info); + $this->values = array_values($this->info); + } + + /** + * Accepts a definition; generates and assigns a ChildDef for it + * @param HTMLPurifier_ElementDef $def HTMLPurifier_ElementDef reference + * @param HTMLPurifier_HTMLModule $module Module that defined the ElementDef + */ + public function generateChildDef(&$def, $module) + { + if (!empty($def->child)) { // already done! + return; + } + $content_model = $def->content_model; + if (is_string($content_model)) { + // Assume that $this->keys is alphanumeric + $def->content_model = preg_replace_callback( + '/\b(' . implode('|', $this->keys) . ')\b/', + array($this, 'generateChildDefCallback'), + $content_model + ); + //$def->content_model = str_replace( + // $this->keys, $this->values, $content_model); + } + $def->child = $this->getChildDef($def, $module); + } + + public function generateChildDefCallback($matches) + { + return $this->info[$matches[0]]; + } + + /** + * Instantiates a ChildDef based on content_model and content_model_type + * member variables in HTMLPurifier_ElementDef + * @note This will also defer to modules for custom HTMLPurifier_ChildDef + * subclasses that need content set expansion + * @param HTMLPurifier_ElementDef $def HTMLPurifier_ElementDef to have ChildDef extracted + * @param HTMLPurifier_HTMLModule $module Module that defined the ElementDef + * @return HTMLPurifier_ChildDef corresponding to ElementDef + */ + public function getChildDef($def, $module) + { + $value = $def->content_model; + if (is_object($value)) { + trigger_error( + 'Literal object child definitions should be stored in '. + 'ElementDef->child not ElementDef->content_model', + E_USER_NOTICE + ); + return $value; + } + switch ($def->content_model_type) { + case 'required': + return new HTMLPurifier_ChildDef_Required($value); + case 'optional': + return new HTMLPurifier_ChildDef_Optional($value); + case 'empty': + return new HTMLPurifier_ChildDef_Empty(); + case 'custom': + return new HTMLPurifier_ChildDef_Custom($value); + } + // defer to its module + $return = false; + if ($module->defines_child_def) { // save a func call + $return = $module->getChildDef($def); + } + if ($return !== false) { + return $return; + } + // error-out + trigger_error( + 'Could not determine which ChildDef class to instantiate', + E_USER_ERROR + ); + return false; + } + + /** + * Converts a string list of elements separated by pipes into + * a lookup array. + * @param string $string List of elements + * @return array Lookup array of elements + */ + protected function convertToLookup($string) + { + $array = explode('|', str_replace(' ', '', $string)); + $ret = array(); + foreach ($array as $k) { + $ret[$k] = true; + } + return $ret; + } +} + + + + + +/** + * Registry object that contains information about the current context. + * @warning Is a bit buggy when variables are set to null: it thinks + * they don't exist! So use false instead, please. + * @note Since the variables Context deals with may not be objects, + * references are very important here! Do not remove! + */ +class HTMLPurifier_Context +{ + + /** + * Private array that stores the references. + * @type array + */ + private $_storage = array(); + + /** + * Registers a variable into the context. + * @param string $name String name + * @param mixed $ref Reference to variable to be registered + */ + public function register($name, &$ref) + { + if (array_key_exists($name, $this->_storage)) { + trigger_error( + "Name $name produces collision, cannot re-register", + E_USER_ERROR + ); + return; + } + $this->_storage[$name] =& $ref; + } + + /** + * Retrieves a variable reference from the context. + * @param string $name String name + * @param bool $ignore_error Boolean whether or not to ignore error + * @return mixed + */ + public function &get($name, $ignore_error = false) + { + if (!array_key_exists($name, $this->_storage)) { + if (!$ignore_error) { + trigger_error( + "Attempted to retrieve non-existent variable $name", + E_USER_ERROR + ); + } + $var = null; // so we can return by reference + return $var; + } + return $this->_storage[$name]; + } + + /** + * Destroys a variable in the context. + * @param string $name String name + */ + public function destroy($name) + { + if (!array_key_exists($name, $this->_storage)) { + trigger_error( + "Attempted to destroy non-existent variable $name", + E_USER_ERROR + ); + return; + } + unset($this->_storage[$name]); + } + + /** + * Checks whether or not the variable exists. + * @param string $name String name + * @return bool + */ + public function exists($name) + { + return array_key_exists($name, $this->_storage); + } + + /** + * Loads a series of variables from an associative array + * @param array $context_array Assoc array of variables to load + */ + public function loadArray($context_array) + { + foreach ($context_array as $key => $discard) { + $this->register($key, $context_array[$key]); + } + } +} + + + + + +/** + * Abstract class representing Definition cache managers that implements + * useful common methods and is a factory. + * @todo Create a separate maintenance file advanced users can use to + * cache their custom HTMLDefinition, which can be loaded + * via a configuration directive + * @todo Implement memcached + */ +abstract class HTMLPurifier_DefinitionCache +{ + /** + * @type string + */ + public $type; + + /** + * @param string $type Type of definition objects this instance of the + * cache will handle. + */ + public function __construct($type) + { + $this->type = $type; + } + + /** + * Generates a unique identifier for a particular configuration + * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config + * @return string + */ + public function generateKey($config) + { + return $config->version . ',' . // possibly replace with function calls + $config->getBatchSerial($this->type) . ',' . + $config->get($this->type . '.DefinitionRev'); + } + + /** + * Tests whether or not a key is old with respect to the configuration's + * version and revision number. + * @param string $key Key to test + * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config to test against + * @return bool + */ + public function isOld($key, $config) + { + if (substr_count($key, ',') < 2) { + return true; + } + list($version, $hash, $revision) = explode(',', $key, 3); + $compare = version_compare($version, $config->version); + // version mismatch, is always old + if ($compare != 0) { + return true; + } + // versions match, ids match, check revision number + if ($hash == $config->getBatchSerial($this->type) && + $revision < $config->get($this->type . '.DefinitionRev')) { + return true; + } + return false; + } + + /** + * Checks if a definition's type jives with the cache's type + * @note Throws an error on failure + * @param HTMLPurifier_Definition $def Definition object to check + * @return bool true if good, false if not + */ + public function checkDefType($def) + { + if ($def->type !== $this->type) { + trigger_error("Cannot use definition of type {$def->type} in cache for {$this->type}"); + return false; + } + return true; + } + + /** + * Adds a definition object to the cache + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + */ + abstract public function add($def, $config); + + /** + * Unconditionally saves a definition object to the cache + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + */ + abstract public function set($def, $config); + + /** + * Replace an object in the cache + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + */ + abstract public function replace($def, $config); + + /** + * Retrieves a definition object from the cache + * @param HTMLPurifier_Config $config + */ + abstract public function get($config); + + /** + * Removes a definition object to the cache + * @param HTMLPurifier_Config $config + */ + abstract public function remove($config); + + /** + * Clears all objects from cache + * @param HTMLPurifier_Config $config + */ + abstract public function flush($config); + + /** + * Clears all expired (older version or revision) objects from cache + * @note Be carefuly implementing this method as flush. Flush must + * not interfere with other Definition types, and cleanup() + * should not be repeatedly called by userland code. + * @param HTMLPurifier_Config $config + */ + abstract public function cleanup($config); +} + + + + + +/** + * Responsible for creating definition caches. + */ +class HTMLPurifier_DefinitionCacheFactory +{ + /** + * @type array + */ + protected $caches = array('Serializer' => array()); + + /** + * @type array + */ + protected $implementations = array(); + + /** + * @type HTMLPurifier_DefinitionCache_Decorator[] + */ + protected $decorators = array(); + + /** + * Initialize default decorators + */ + public function setup() + { + $this->addDecorator('Cleanup'); + } + + /** + * Retrieves an instance of global definition cache factory. + * @param HTMLPurifier_DefinitionCacheFactory $prototype + * @return HTMLPurifier_DefinitionCacheFactory + */ + public static function instance($prototype = null) + { + static $instance; + if ($prototype !== null) { + $instance = $prototype; + } elseif ($instance === null || $prototype === true) { + $instance = new HTMLPurifier_DefinitionCacheFactory(); + $instance->setup(); + } + return $instance; + } + + /** + * Registers a new definition cache object + * @param string $short Short name of cache object, for reference + * @param string $long Full class name of cache object, for construction + */ + public function register($short, $long) + { + $this->implementations[$short] = $long; + } + + /** + * Factory method that creates a cache object based on configuration + * @param string $type Name of definitions handled by cache + * @param HTMLPurifier_Config $config Config instance + * @return mixed + */ + public function create($type, $config) + { + $method = $config->get('Cache.DefinitionImpl'); + if ($method === null) { + return new HTMLPurifier_DefinitionCache_Null($type); + } + if (!empty($this->caches[$method][$type])) { + return $this->caches[$method][$type]; + } + if (isset($this->implementations[$method]) && + class_exists($class = $this->implementations[$method], false)) { + $cache = new $class($type); + } else { + if ($method != 'Serializer') { + trigger_error("Unrecognized DefinitionCache $method, using Serializer instead", E_USER_WARNING); + } + $cache = new HTMLPurifier_DefinitionCache_Serializer($type); + } + foreach ($this->decorators as $decorator) { + $new_cache = $decorator->decorate($cache); + // prevent infinite recursion in PHP 4 + unset($cache); + $cache = $new_cache; + } + $this->caches[$method][$type] = $cache; + return $this->caches[$method][$type]; + } + + /** + * Registers a decorator to add to all new cache objects + * @param HTMLPurifier_DefinitionCache_Decorator|string $decorator An instance or the name of a decorator + */ + public function addDecorator($decorator) + { + if (is_string($decorator)) { + $class = "HTMLPurifier_DefinitionCache_Decorator_$decorator"; + $decorator = new $class; + } + $this->decorators[$decorator->name] = $decorator; + } +} + + + + + +/** + * Represents a document type, contains information on which modules + * need to be loaded. + * @note This class is inspected by Printer_HTMLDefinition->renderDoctype. + * If structure changes, please update that function. + */ +class HTMLPurifier_Doctype +{ + /** + * Full name of doctype + * @type string + */ + public $name; + + /** + * List of standard modules (string identifiers or literal objects) + * that this doctype uses + * @type array + */ + public $modules = array(); + + /** + * List of modules to use for tidying up code + * @type array + */ + public $tidyModules = array(); + + /** + * Is the language derived from XML (i.e. XHTML)? + * @type bool + */ + public $xml = true; + + /** + * List of aliases for this doctype + * @type array + */ + public $aliases = array(); + + /** + * Public DTD identifier + * @type string + */ + public $dtdPublic; + + /** + * System DTD identifier + * @type string + */ + public $dtdSystem; + + public function __construct( + $name = null, + $xml = true, + $modules = array(), + $tidyModules = array(), + $aliases = array(), + $dtd_public = null, + $dtd_system = null + ) { + $this->name = $name; + $this->xml = $xml; + $this->modules = $modules; + $this->tidyModules = $tidyModules; + $this->aliases = $aliases; + $this->dtdPublic = $dtd_public; + $this->dtdSystem = $dtd_system; + } +} + + + + + +class HTMLPurifier_DoctypeRegistry +{ + + /** + * Hash of doctype names to doctype objects. + * @type array + */ + protected $doctypes; + + /** + * Lookup table of aliases to real doctype names. + * @type array + */ + protected $aliases; + + /** + * Registers a doctype to the registry + * @note Accepts a fully-formed doctype object, or the + * parameters for constructing a doctype object + * @param string $doctype Name of doctype or literal doctype object + * @param bool $xml + * @param array $modules Modules doctype will load + * @param array $tidy_modules Modules doctype will load for certain modes + * @param array $aliases Alias names for doctype + * @param string $dtd_public + * @param string $dtd_system + * @return HTMLPurifier_Doctype Editable registered doctype + */ + public function register( + $doctype, + $xml = true, + $modules = array(), + $tidy_modules = array(), + $aliases = array(), + $dtd_public = null, + $dtd_system = null + ) { + if (!is_array($modules)) { + $modules = array($modules); + } + if (!is_array($tidy_modules)) { + $tidy_modules = array($tidy_modules); + } + if (!is_array($aliases)) { + $aliases = array($aliases); + } + if (!is_object($doctype)) { + $doctype = new HTMLPurifier_Doctype( + $doctype, + $xml, + $modules, + $tidy_modules, + $aliases, + $dtd_public, + $dtd_system + ); + } + $this->doctypes[$doctype->name] = $doctype; + $name = $doctype->name; + // hookup aliases + foreach ($doctype->aliases as $alias) { + if (isset($this->doctypes[$alias])) { + continue; + } + $this->aliases[$alias] = $name; + } + // remove old aliases + if (isset($this->aliases[$name])) { + unset($this->aliases[$name]); + } + return $doctype; + } + + /** + * Retrieves reference to a doctype of a certain name + * @note This function resolves aliases + * @note When possible, use the more fully-featured make() + * @param string $doctype Name of doctype + * @return HTMLPurifier_Doctype Editable doctype object + */ + public function get($doctype) + { + if (isset($this->aliases[$doctype])) { + $doctype = $this->aliases[$doctype]; + } + if (!isset($this->doctypes[$doctype])) { + trigger_error('Doctype ' . htmlspecialchars($doctype) . ' does not exist', E_USER_ERROR); + $anon = new HTMLPurifier_Doctype($doctype); + return $anon; + } + return $this->doctypes[$doctype]; + } + + /** + * Creates a doctype based on a configuration object, + * will perform initialization on the doctype + * @note Use this function to get a copy of doctype that config + * can hold on to (this is necessary in order to tell + * Generator whether or not the current document is XML + * based or not). + * @param HTMLPurifier_Config $config + * @return HTMLPurifier_Doctype + */ + public function make($config) + { + return clone $this->get($this->getDoctypeFromConfig($config)); + } + + /** + * Retrieves the doctype from the configuration object + * @param HTMLPurifier_Config $config + * @return string + */ + public function getDoctypeFromConfig($config) + { + // recommended test + $doctype = $config->get('HTML.Doctype'); + if (!empty($doctype)) { + return $doctype; + } + $doctype = $config->get('HTML.CustomDoctype'); + if (!empty($doctype)) { + return $doctype; + } + // backwards-compatibility + if ($config->get('HTML.XHTML')) { + $doctype = 'XHTML 1.0'; + } else { + $doctype = 'HTML 4.01'; + } + if ($config->get('HTML.Strict')) { + $doctype .= ' Strict'; + } else { + $doctype .= ' Transitional'; + } + return $doctype; + } +} + + + + + +/** + * Structure that stores an HTML element definition. Used by + * HTMLPurifier_HTMLDefinition and HTMLPurifier_HTMLModule. + * @note This class is inspected by HTMLPurifier_Printer_HTMLDefinition. + * Please update that class too. + * @warning If you add new properties to this class, you MUST update + * the mergeIn() method. + */ +class HTMLPurifier_ElementDef +{ + /** + * Does the definition work by itself, or is it created solely + * for the purpose of merging into another definition? + * @type bool + */ + public $standalone = true; + + /** + * Associative array of attribute name to HTMLPurifier_AttrDef. + * @type array + * @note Before being processed by HTMLPurifier_AttrCollections + * when modules are finalized during + * HTMLPurifier_HTMLDefinition->setup(), this array may also + * contain an array at index 0 that indicates which attribute + * collections to load into the full array. It may also + * contain string indentifiers in lieu of HTMLPurifier_AttrDef, + * see HTMLPurifier_AttrTypes on how they are expanded during + * HTMLPurifier_HTMLDefinition->setup() processing. + */ + public $attr = array(); + + // XXX: Design note: currently, it's not possible to override + // previously defined AttrTransforms without messing around with + // the final generated config. This is by design; a previous version + // used an associated list of attr_transform, but it was extremely + // easy to accidentally override other attribute transforms by + // forgetting to specify an index (and just using 0.) While we + // could check this by checking the index number and complaining, + // there is a second problem which is that it is not at all easy to + // tell when something is getting overridden. Combine this with a + // codebase where this isn't really being used, and it's perfect for + // nuking. + + /** + * List of tags HTMLPurifier_AttrTransform to be done before validation. + * @type array + */ + public $attr_transform_pre = array(); + + /** + * List of tags HTMLPurifier_AttrTransform to be done after validation. + * @type array + */ + public $attr_transform_post = array(); + + /** + * HTMLPurifier_ChildDef of this tag. + * @type HTMLPurifier_ChildDef + */ + public $child; + + /** + * Abstract string representation of internal ChildDef rules. + * @see HTMLPurifier_ContentSets for how this is parsed and then transformed + * into an HTMLPurifier_ChildDef. + * @warning This is a temporary variable that is not available after + * being processed by HTMLDefinition + * @type string + */ + public $content_model; + + /** + * Value of $child->type, used to determine which ChildDef to use, + * used in combination with $content_model. + * @warning This must be lowercase + * @warning This is a temporary variable that is not available after + * being processed by HTMLDefinition + * @type string + */ + public $content_model_type; + + /** + * Does the element have a content model (#PCDATA | Inline)*? This + * is important for chameleon ins and del processing in + * HTMLPurifier_ChildDef_Chameleon. Dynamically set: modules don't + * have to worry about this one. + * @type bool + */ + public $descendants_are_inline = false; + + /** + * List of the names of required attributes this element has. + * Dynamically populated by HTMLPurifier_HTMLDefinition::getElement() + * @type array + */ + public $required_attr = array(); + + /** + * Lookup table of tags excluded from all descendants of this tag. + * @type array + * @note SGML permits exclusions for all descendants, but this is + * not possible with DTDs or XML Schemas. W3C has elected to + * use complicated compositions of content_models to simulate + * exclusion for children, but we go the simpler, SGML-style + * route of flat-out exclusions, which correctly apply to + * all descendants and not just children. Note that the XHTML + * Modularization Abstract Modules are blithely unaware of such + * distinctions. + */ + public $excludes = array(); + + /** + * This tag is explicitly auto-closed by the following tags. + * @type array + */ + public $autoclose = array(); + + /** + * If a foreign element is found in this element, test if it is + * allowed by this sub-element; if it is, instead of closing the + * current element, place it inside this element. + * @type string + */ + public $wrap; + + /** + * Whether or not this is a formatting element affected by the + * "Active Formatting Elements" algorithm. + * @type bool + */ + public $formatting; + + /** + * Low-level factory constructor for creating new standalone element defs + */ + public static function create($content_model, $content_model_type, $attr) + { + $def = new HTMLPurifier_ElementDef(); + $def->content_model = $content_model; + $def->content_model_type = $content_model_type; + $def->attr = $attr; + return $def; + } + + /** + * Merges the values of another element definition into this one. + * Values from the new element def take precedence if a value is + * not mergeable. + * @param HTMLPurifier_ElementDef $def + */ + public function mergeIn($def) + { + // later keys takes precedence + foreach ($def->attr as $k => $v) { + if ($k === 0) { + // merge in the includes + // sorry, no way to override an include + foreach ($v as $v2) { + $this->attr[0][] = $v2; + } + continue; + } + if ($v === false) { + if (isset($this->attr[$k])) { + unset($this->attr[$k]); + } + continue; + } + $this->attr[$k] = $v; + } + $this->_mergeAssocArray($this->excludes, $def->excludes); + $this->attr_transform_pre = array_merge($this->attr_transform_pre, $def->attr_transform_pre); + $this->attr_transform_post = array_merge($this->attr_transform_post, $def->attr_transform_post); + + if (!empty($def->content_model)) { + $this->content_model = + str_replace("#SUPER", $this->content_model, $def->content_model); + $this->child = false; + } + if (!empty($def->content_model_type)) { + $this->content_model_type = $def->content_model_type; + $this->child = false; + } + if (!is_null($def->child)) { + $this->child = $def->child; + } + if (!is_null($def->formatting)) { + $this->formatting = $def->formatting; + } + if ($def->descendants_are_inline) { + $this->descendants_are_inline = $def->descendants_are_inline; + } + } + + /** + * Merges one array into another, removes values which equal false + * @param $a1 Array by reference that is merged into + * @param $a2 Array that merges into $a1 + */ + private function _mergeAssocArray(&$a1, $a2) + { + foreach ($a2 as $k => $v) { + if ($v === false) { + if (isset($a1[$k])) { + unset($a1[$k]); + } + continue; + } + $a1[$k] = $v; + } + } +} + + + + + +/** + * A UTF-8 specific character encoder that handles cleaning and transforming. + * @note All functions in this class should be static. + */ +class HTMLPurifier_Encoder +{ + + /** + * Constructor throws fatal error if you attempt to instantiate class + */ + private function __construct() + { + trigger_error('Cannot instantiate encoder, call methods statically', E_USER_ERROR); + } + + /** + * Error-handler that mutes errors, alternative to shut-up operator. + */ + public static function muteErrorHandler() + { + } + + /** + * iconv wrapper which mutes errors, but doesn't work around bugs. + * @param string $in Input encoding + * @param string $out Output encoding + * @param string $text The text to convert + * @return string + */ + public static function unsafeIconv($in, $out, $text) + { + set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler')); + $r = iconv($in, $out, $text); + restore_error_handler(); + return $r; + } + + /** + * iconv wrapper which mutes errors and works around bugs. + * @param string $in Input encoding + * @param string $out Output encoding + * @param string $text The text to convert + * @param int $max_chunk_size + * @return string + */ + public static function iconv($in, $out, $text, $max_chunk_size = 8000) + { + $code = self::testIconvTruncateBug(); + if ($code == self::ICONV_OK) { + return self::unsafeIconv($in, $out, $text); + } elseif ($code == self::ICONV_TRUNCATES) { + // we can only work around this if the input character set + // is utf-8 + if ($in == 'utf-8') { + if ($max_chunk_size < 4) { + trigger_error('max_chunk_size is too small', E_USER_WARNING); + return false; + } + // split into 8000 byte chunks, but be careful to handle + // multibyte boundaries properly + if (($c = strlen($text)) <= $max_chunk_size) { + return self::unsafeIconv($in, $out, $text); + } + $r = ''; + $i = 0; + while (true) { + if ($i + $max_chunk_size >= $c) { + $r .= self::unsafeIconv($in, $out, substr($text, $i)); + break; + } + // wibble the boundary + if (0x80 != (0xC0 & ord($text[$i + $max_chunk_size]))) { + $chunk_size = $max_chunk_size; + } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 1]))) { + $chunk_size = $max_chunk_size - 1; + } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 2]))) { + $chunk_size = $max_chunk_size - 2; + } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 3]))) { + $chunk_size = $max_chunk_size - 3; + } else { + return false; // rather confusing UTF-8... + } + $chunk = substr($text, $i, $chunk_size); // substr doesn't mind overlong lengths + $r .= self::unsafeIconv($in, $out, $chunk); + $i += $chunk_size; + } + return $r; + } else { + return false; + } + } else { + return false; + } + } + + /** + * Cleans a UTF-8 string for well-formedness and SGML validity + * + * It will parse according to UTF-8 and return a valid UTF8 string, with + * non-SGML codepoints excluded. + * + * @param string $str The string to clean + * @param bool $force_php + * @return string + * + * @note Just for reference, the non-SGML code points are 0 to 31 and + * 127 to 159, inclusive. However, we allow code points 9, 10 + * and 13, which are the tab, line feed and carriage return + * respectively. 128 and above the code points map to multibyte + * UTF-8 representations. + * + * @note Fallback code adapted from utf8ToUnicode by Henri Sivonen and + * hsivonen@iki.fi at under the + * LGPL license. Notes on what changed are inside, but in general, + * the original code transformed UTF-8 text into an array of integer + * Unicode codepoints. Understandably, transforming that back to + * a string would be somewhat expensive, so the function was modded to + * directly operate on the string. However, this discourages code + * reuse, and the logic enumerated here would be useful for any + * function that needs to be able to understand UTF-8 characters. + * As of right now, only smart lossless character encoding converters + * would need that, and I'm probably not going to implement them. + * Once again, PHP 6 should solve all our problems. + */ + public static function cleanUTF8($str, $force_php = false) + { + // UTF-8 validity is checked since PHP 4.3.5 + // This is an optimization: if the string is already valid UTF-8, no + // need to do PHP stuff. 99% of the time, this will be the case. + // The regexp matches the XML char production, as well as well as excluding + // non-SGML codepoints U+007F to U+009F + if (preg_match( + '/^[\x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]*$/Du', + $str + )) { + return $str; + } + + $mState = 0; // cached expected number of octets after the current octet + // until the beginning of the next UTF8 character sequence + $mUcs4 = 0; // cached Unicode character + $mBytes = 1; // cached expected number of octets in the current sequence + + // original code involved an $out that was an array of Unicode + // codepoints. Instead of having to convert back into UTF-8, we've + // decided to directly append valid UTF-8 characters onto a string + // $out once they're done. $char accumulates raw bytes, while $mUcs4 + // turns into the Unicode code point, so there's some redundancy. + + $out = ''; + $char = ''; + + $len = strlen($str); + for ($i = 0; $i < $len; $i++) { + $in = ord($str{$i}); + $char .= $str[$i]; // append byte to char + if (0 == $mState) { + // When mState is zero we expect either a US-ASCII character + // or a multi-octet sequence. + if (0 == (0x80 & ($in))) { + // US-ASCII, pass straight through. + if (($in <= 31 || $in == 127) && + !($in == 9 || $in == 13 || $in == 10) // save \r\t\n + ) { + // control characters, remove + } else { + $out .= $char; + } + // reset + $char = ''; + $mBytes = 1; + } elseif (0xC0 == (0xE0 & ($in))) { + // First octet of 2 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x1F) << 6; + $mState = 1; + $mBytes = 2; + } elseif (0xE0 == (0xF0 & ($in))) { + // First octet of 3 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x0F) << 12; + $mState = 2; + $mBytes = 3; + } elseif (0xF0 == (0xF8 & ($in))) { + // First octet of 4 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x07) << 18; + $mState = 3; + $mBytes = 4; + } elseif (0xF8 == (0xFC & ($in))) { + // First octet of 5 octet sequence. + // + // This is illegal because the encoded codepoint must be + // either: + // (a) not the shortest form or + // (b) outside the Unicode range of 0-0x10FFFF. + // Rather than trying to resynchronize, we will carry on + // until the end of the sequence and let the later error + // handling code catch it. + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x03) << 24; + $mState = 4; + $mBytes = 5; + } elseif (0xFC == (0xFE & ($in))) { + // First octet of 6 octet sequence, see comments for 5 + // octet sequence. + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 1) << 30; + $mState = 5; + $mBytes = 6; + } else { + // Current octet is neither in the US-ASCII range nor a + // legal first octet of a multi-octet sequence. + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + $char = ''; + } + } else { + // When mState is non-zero, we expect a continuation of the + // multi-octet sequence + if (0x80 == (0xC0 & ($in))) { + // Legal continuation. + $shift = ($mState - 1) * 6; + $tmp = $in; + $tmp = ($tmp & 0x0000003F) << $shift; + $mUcs4 |= $tmp; + + if (0 == --$mState) { + // End of the multi-octet sequence. mUcs4 now contains + // the final Unicode codepoint to be output + + // Check for illegal sequences and codepoints. + + // From Unicode 3.1, non-shortest form is illegal + if (((2 == $mBytes) && ($mUcs4 < 0x0080)) || + ((3 == $mBytes) && ($mUcs4 < 0x0800)) || + ((4 == $mBytes) && ($mUcs4 < 0x10000)) || + (4 < $mBytes) || + // From Unicode 3.2, surrogate characters = illegal + (($mUcs4 & 0xFFFFF800) == 0xD800) || + // Codepoints outside the Unicode range are illegal + ($mUcs4 > 0x10FFFF) + ) { + + } elseif (0xFEFF != $mUcs4 && // omit BOM + // check for valid Char unicode codepoints + ( + 0x9 == $mUcs4 || + 0xA == $mUcs4 || + 0xD == $mUcs4 || + (0x20 <= $mUcs4 && 0x7E >= $mUcs4) || + // 7F-9F is not strictly prohibited by XML, + // but it is non-SGML, and thus we don't allow it + (0xA0 <= $mUcs4 && 0xD7FF >= $mUcs4) || + (0x10000 <= $mUcs4 && 0x10FFFF >= $mUcs4) + ) + ) { + $out .= $char; + } + // initialize UTF8 cache (reset) + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + $char = ''; + } + } else { + // ((0xC0 & (*in) != 0x80) && (mState != 0)) + // Incomplete multi-octet sequence. + // used to result in complete fail, but we'll reset + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + $char =''; + } + } + } + return $out; + } + + /** + * Translates a Unicode codepoint into its corresponding UTF-8 character. + * @note Based on Feyd's function at + * , + * which is in public domain. + * @note While we're going to do code point parsing anyway, a good + * optimization would be to refuse to translate code points that + * are non-SGML characters. However, this could lead to duplication. + * @note This is very similar to the unichr function in + * maintenance/generate-entity-file.php (although this is superior, + * due to its sanity checks). + */ + + // +----------+----------+----------+----------+ + // | 33222222 | 22221111 | 111111 | | + // | 10987654 | 32109876 | 54321098 | 76543210 | bit + // +----------+----------+----------+----------+ + // | | | | 0xxxxxxx | 1 byte 0x00000000..0x0000007F + // | | | 110yyyyy | 10xxxxxx | 2 byte 0x00000080..0x000007FF + // | | 1110zzzz | 10yyyyyy | 10xxxxxx | 3 byte 0x00000800..0x0000FFFF + // | 11110www | 10wwzzzz | 10yyyyyy | 10xxxxxx | 4 byte 0x00010000..0x0010FFFF + // +----------+----------+----------+----------+ + // | 00000000 | 00011111 | 11111111 | 11111111 | Theoretical upper limit of legal scalars: 2097151 (0x001FFFFF) + // | 00000000 | 00010000 | 11111111 | 11111111 | Defined upper limit of legal scalar codes + // +----------+----------+----------+----------+ + + public static function unichr($code) + { + if ($code > 1114111 or $code < 0 or + ($code >= 55296 and $code <= 57343) ) { + // bits are set outside the "valid" range as defined + // by UNICODE 4.1.0 + return ''; + } + + $x = $y = $z = $w = 0; + if ($code < 128) { + // regular ASCII character + $x = $code; + } else { + // set up bits for UTF-8 + $x = ($code & 63) | 128; + if ($code < 2048) { + $y = (($code & 2047) >> 6) | 192; + } else { + $y = (($code & 4032) >> 6) | 128; + if ($code < 65536) { + $z = (($code >> 12) & 15) | 224; + } else { + $z = (($code >> 12) & 63) | 128; + $w = (($code >> 18) & 7) | 240; + } + } + } + // set up the actual character + $ret = ''; + if ($w) { + $ret .= chr($w); + } + if ($z) { + $ret .= chr($z); + } + if ($y) { + $ret .= chr($y); + } + $ret .= chr($x); + + return $ret; + } + + /** + * @return bool + */ + public static function iconvAvailable() + { + static $iconv = null; + if ($iconv === null) { + $iconv = function_exists('iconv') && self::testIconvTruncateBug() != self::ICONV_UNUSABLE; + } + return $iconv; + } + + /** + * Convert a string to UTF-8 based on configuration. + * @param string $str The string to convert + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public static function convertToUTF8($str, $config, $context) + { + $encoding = $config->get('Core.Encoding'); + if ($encoding === 'utf-8') { + return $str; + } + static $iconv = null; + if ($iconv === null) { + $iconv = self::iconvAvailable(); + } + if ($iconv && !$config->get('Test.ForceNoIconv')) { + // unaffected by bugs, since UTF-8 support all characters + $str = self::unsafeIconv($encoding, 'utf-8//IGNORE', $str); + if ($str === false) { + // $encoding is not a valid encoding + trigger_error('Invalid encoding ' . $encoding, E_USER_ERROR); + return ''; + } + // If the string is bjorked by Shift_JIS or a similar encoding + // that doesn't support all of ASCII, convert the naughty + // characters to their true byte-wise ASCII/UTF-8 equivalents. + $str = strtr($str, self::testEncodingSupportsASCII($encoding)); + return $str; + } elseif ($encoding === 'iso-8859-1') { + $str = utf8_encode($str); + return $str; + } + $bug = HTMLPurifier_Encoder::testIconvTruncateBug(); + if ($bug == self::ICONV_OK) { + trigger_error('Encoding not supported, please install iconv', E_USER_ERROR); + } else { + trigger_error( + 'You have a buggy version of iconv, see https://bugs.php.net/bug.php?id=48147 ' . + 'and http://sourceware.org/bugzilla/show_bug.cgi?id=13541', + E_USER_ERROR + ); + } + } + + /** + * Converts a string from UTF-8 based on configuration. + * @param string $str The string to convert + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + * @note Currently, this is a lossy conversion, with unexpressable + * characters being omitted. + */ + public static function convertFromUTF8($str, $config, $context) + { + $encoding = $config->get('Core.Encoding'); + if ($escape = $config->get('Core.EscapeNonASCIICharacters')) { + $str = self::convertToASCIIDumbLossless($str); + } + if ($encoding === 'utf-8') { + return $str; + } + static $iconv = null; + if ($iconv === null) { + $iconv = self::iconvAvailable(); + } + if ($iconv && !$config->get('Test.ForceNoIconv')) { + // Undo our previous fix in convertToUTF8, otherwise iconv will barf + $ascii_fix = self::testEncodingSupportsASCII($encoding); + if (!$escape && !empty($ascii_fix)) { + $clear_fix = array(); + foreach ($ascii_fix as $utf8 => $native) { + $clear_fix[$utf8] = ''; + } + $str = strtr($str, $clear_fix); + } + $str = strtr($str, array_flip($ascii_fix)); + // Normal stuff + $str = self::iconv('utf-8', $encoding . '//IGNORE', $str); + return $str; + } elseif ($encoding === 'iso-8859-1') { + $str = utf8_decode($str); + return $str; + } + trigger_error('Encoding not supported', E_USER_ERROR); + // You might be tempted to assume that the ASCII representation + // might be OK, however, this is *not* universally true over all + // encodings. So we take the conservative route here, rather + // than forcibly turn on %Core.EscapeNonASCIICharacters + } + + /** + * Lossless (character-wise) conversion of HTML to ASCII + * @param string $str UTF-8 string to be converted to ASCII + * @return string ASCII encoded string with non-ASCII character entity-ized + * @warning Adapted from MediaWiki, claiming fair use: this is a common + * algorithm. If you disagree with this license fudgery, + * implement it yourself. + * @note Uses decimal numeric entities since they are best supported. + * @note This is a DUMB function: it has no concept of keeping + * character entities that the projected character encoding + * can allow. We could possibly implement a smart version + * but that would require it to also know which Unicode + * codepoints the charset supported (not an easy task). + * @note Sort of with cleanUTF8() but it assumes that $str is + * well-formed UTF-8 + */ + public static function convertToASCIIDumbLossless($str) + { + $bytesleft = 0; + $result = ''; + $working = 0; + $len = strlen($str); + for ($i = 0; $i < $len; $i++) { + $bytevalue = ord($str[$i]); + if ($bytevalue <= 0x7F) { //0xxx xxxx + $result .= chr($bytevalue); + $bytesleft = 0; + } elseif ($bytevalue <= 0xBF) { //10xx xxxx + $working = $working << 6; + $working += ($bytevalue & 0x3F); + $bytesleft--; + if ($bytesleft <= 0) { + $result .= "&#" . $working . ";"; + } + } elseif ($bytevalue <= 0xDF) { //110x xxxx + $working = $bytevalue & 0x1F; + $bytesleft = 1; + } elseif ($bytevalue <= 0xEF) { //1110 xxxx + $working = $bytevalue & 0x0F; + $bytesleft = 2; + } else { //1111 0xxx + $working = $bytevalue & 0x07; + $bytesleft = 3; + } + } + return $result; + } + + /** No bugs detected in iconv. */ + const ICONV_OK = 0; + + /** Iconv truncates output if converting from UTF-8 to another + * character set with //IGNORE, and a non-encodable character is found */ + const ICONV_TRUNCATES = 1; + + /** Iconv does not support //IGNORE, making it unusable for + * transcoding purposes */ + const ICONV_UNUSABLE = 2; + + /** + * glibc iconv has a known bug where it doesn't handle the magic + * //IGNORE stanza correctly. In particular, rather than ignore + * characters, it will return an EILSEQ after consuming some number + * of characters, and expect you to restart iconv as if it were + * an E2BIG. Old versions of PHP did not respect the errno, and + * returned the fragment, so as a result you would see iconv + * mysteriously truncating output. We can work around this by + * manually chopping our input into segments of about 8000 + * characters, as long as PHP ignores the error code. If PHP starts + * paying attention to the error code, iconv becomes unusable. + * + * @return int Error code indicating severity of bug. + */ + public static function testIconvTruncateBug() + { + static $code = null; + if ($code === null) { + // better not use iconv, otherwise infinite loop! + $r = self::unsafeIconv('utf-8', 'ascii//IGNORE', "\xCE\xB1" . str_repeat('a', 9000)); + if ($r === false) { + $code = self::ICONV_UNUSABLE; + } elseif (($c = strlen($r)) < 9000) { + $code = self::ICONV_TRUNCATES; + } elseif ($c > 9000) { + trigger_error( + 'Your copy of iconv is extremely buggy. Please notify HTML Purifier maintainers: ' . + 'include your iconv version as per phpversion()', + E_USER_ERROR + ); + } else { + $code = self::ICONV_OK; + } + } + return $code; + } + + /** + * This expensive function tests whether or not a given character + * encoding supports ASCII. 7/8-bit encodings like Shift_JIS will + * fail this test, and require special processing. Variable width + * encodings shouldn't ever fail. + * + * @param string $encoding Encoding name to test, as per iconv format + * @param bool $bypass Whether or not to bypass the precompiled arrays. + * @return Array of UTF-8 characters to their corresponding ASCII, + * which can be used to "undo" any overzealous iconv action. + */ + public static function testEncodingSupportsASCII($encoding, $bypass = false) + { + // All calls to iconv here are unsafe, proof by case analysis: + // If ICONV_OK, no difference. + // If ICONV_TRUNCATE, all calls involve one character inputs, + // so bug is not triggered. + // If ICONV_UNUSABLE, this call is irrelevant + static $encodings = array(); + if (!$bypass) { + if (isset($encodings[$encoding])) { + return $encodings[$encoding]; + } + $lenc = strtolower($encoding); + switch ($lenc) { + case 'shift_jis': + return array("\xC2\xA5" => '\\', "\xE2\x80\xBE" => '~'); + case 'johab': + return array("\xE2\x82\xA9" => '\\'); + } + if (strpos($lenc, 'iso-8859-') === 0) { + return array(); + } + } + $ret = array(); + if (self::unsafeIconv('UTF-8', $encoding, 'a') === false) { + return false; + } + for ($i = 0x20; $i <= 0x7E; $i++) { // all printable ASCII chars + $c = chr($i); // UTF-8 char + $r = self::unsafeIconv('UTF-8', "$encoding//IGNORE", $c); // initial conversion + if ($r === '' || + // This line is needed for iconv implementations that do not + // omit characters that do not exist in the target character set + ($r === $c && self::unsafeIconv($encoding, 'UTF-8//IGNORE', $r) !== $c) + ) { + // Reverse engineer: what's the UTF-8 equiv of this byte + // sequence? This assumes that there's no variable width + // encoding that doesn't support ASCII. + $ret[self::unsafeIconv($encoding, 'UTF-8//IGNORE', $c)] = $c; + } + } + $encodings[$encoding] = $ret; + return $ret; + } +} + + + + + +/** + * Object that provides entity lookup table from entity name to character + */ +class HTMLPurifier_EntityLookup +{ + /** + * Assoc array of entity name to character represented. + * @type array + */ + public $table; + + /** + * Sets up the entity lookup table from the serialized file contents. + * @param bool $file + * @note The serialized contents are versioned, but were generated + * using the maintenance script generate_entity_file.php + * @warning This is not in constructor to help enforce the Singleton + */ + public function setup($file = false) + { + if (!$file) { + $file = HTMLPURIFIER_PREFIX . '/HTMLPurifier/EntityLookup/entities.ser'; + } + $this->table = unserialize(file_get_contents($file)); + } + + /** + * Retrieves sole instance of the object. + * @param bool|HTMLPurifier_EntityLookup $prototype Optional prototype of custom lookup table to overload with. + * @return HTMLPurifier_EntityLookup + */ + public static function instance($prototype = false) + { + // no references, since PHP doesn't copy unless modified + static $instance = null; + if ($prototype) { + $instance = $prototype; + } elseif (!$instance) { + $instance = new HTMLPurifier_EntityLookup(); + $instance->setup(); + } + return $instance; + } +} + + + + + +// if want to implement error collecting here, we'll need to use some sort +// of global data (probably trigger_error) because it's impossible to pass +// $config or $context to the callback functions. + +/** + * Handles referencing and derefencing character entities + */ +class HTMLPurifier_EntityParser +{ + + /** + * Reference to entity lookup table. + * @type HTMLPurifier_EntityLookup + */ + protected $_entity_lookup; + + /** + * Callback regex string for parsing entities. + * @type string + */ + protected $_substituteEntitiesRegex = + '/&(?:[#]x([a-fA-F0-9]+)|[#]0*(\d+)|([A-Za-z_:][A-Za-z0-9.\-_:]*));?/'; + // 1. hex 2. dec 3. string (XML style) + + /** + * Decimal to parsed string conversion table for special entities. + * @type array + */ + protected $_special_dec2str = + array( + 34 => '"', + 38 => '&', + 39 => "'", + 60 => '<', + 62 => '>' + ); + + /** + * Stripped entity names to decimal conversion table for special entities. + * @type array + */ + protected $_special_ent2dec = + array( + 'quot' => 34, + 'amp' => 38, + 'lt' => 60, + 'gt' => 62 + ); + + /** + * Substitutes non-special entities with their parsed equivalents. Since + * running this whenever you have parsed character is t3h 5uck, we run + * it before everything else. + * + * @param string $string String to have non-special entities parsed. + * @return string Parsed string. + */ + public function substituteNonSpecialEntities($string) + { + // it will try to detect missing semicolons, but don't rely on it + return preg_replace_callback( + $this->_substituteEntitiesRegex, + array($this, 'nonSpecialEntityCallback'), + $string + ); + } + + /** + * Callback function for substituteNonSpecialEntities() that does the work. + * + * @param array $matches PCRE matches array, with 0 the entire match, and + * either index 1, 2 or 3 set with a hex value, dec value, + * or string (respectively). + * @return string Replacement string. + */ + + protected function nonSpecialEntityCallback($matches) + { + // replaces all but big five + $entity = $matches[0]; + $is_num = (@$matches[0][1] === '#'); + if ($is_num) { + $is_hex = (@$entity[2] === 'x'); + $code = $is_hex ? hexdec($matches[1]) : (int) $matches[2]; + // abort for special characters + if (isset($this->_special_dec2str[$code])) { + return $entity; + } + return HTMLPurifier_Encoder::unichr($code); + } else { + if (isset($this->_special_ent2dec[$matches[3]])) { + return $entity; + } + if (!$this->_entity_lookup) { + $this->_entity_lookup = HTMLPurifier_EntityLookup::instance(); + } + if (isset($this->_entity_lookup->table[$matches[3]])) { + return $this->_entity_lookup->table[$matches[3]]; + } else { + return $entity; + } + } + } + + /** + * Substitutes only special entities with their parsed equivalents. + * + * @notice We try to avoid calling this function because otherwise, it + * would have to be called a lot (for every parsed section). + * + * @param string $string String to have non-special entities parsed. + * @return string Parsed string. + */ + public function substituteSpecialEntities($string) + { + return preg_replace_callback( + $this->_substituteEntitiesRegex, + array($this, 'specialEntityCallback'), + $string + ); + } + + /** + * Callback function for substituteSpecialEntities() that does the work. + * + * This callback has same syntax as nonSpecialEntityCallback(). + * + * @param array $matches PCRE-style matches array, with 0 the entire match, and + * either index 1, 2 or 3 set with a hex value, dec value, + * or string (respectively). + * @return string Replacement string. + */ + protected function specialEntityCallback($matches) + { + $entity = $matches[0]; + $is_num = (@$matches[0][1] === '#'); + if ($is_num) { + $is_hex = (@$entity[2] === 'x'); + $int = $is_hex ? hexdec($matches[1]) : (int) $matches[2]; + return isset($this->_special_dec2str[$int]) ? + $this->_special_dec2str[$int] : + $entity; + } else { + return isset($this->_special_ent2dec[$matches[3]]) ? + $this->_special_ent2dec[$matches[3]] : + $entity; + } + } +} + + + + + +/** + * Error collection class that enables HTML Purifier to report HTML + * problems back to the user + */ +class HTMLPurifier_ErrorCollector +{ + + /** + * Identifiers for the returned error array. These are purposely numeric + * so list() can be used. + */ + const LINENO = 0; + const SEVERITY = 1; + const MESSAGE = 2; + const CHILDREN = 3; + + /** + * @type array + */ + protected $errors; + + /** + * @type array + */ + protected $_current; + + /** + * @type array + */ + protected $_stacks = array(array()); + + /** + * @type HTMLPurifier_Language + */ + protected $locale; + + /** + * @type HTMLPurifier_Generator + */ + protected $generator; + + /** + * @type HTMLPurifier_Context + */ + protected $context; + + /** + * @type array + */ + protected $lines = array(); + + /** + * @param HTMLPurifier_Context $context + */ + public function __construct($context) + { + $this->locale =& $context->get('Locale'); + $this->context = $context; + $this->_current =& $this->_stacks[0]; + $this->errors =& $this->_stacks[0]; + } + + /** + * Sends an error message to the collector for later use + * @param int $severity Error severity, PHP error style (don't use E_USER_) + * @param string $msg Error message text + */ + public function send($severity, $msg) + { + $args = array(); + if (func_num_args() > 2) { + $args = func_get_args(); + array_shift($args); + unset($args[0]); + } + + $token = $this->context->get('CurrentToken', true); + $line = $token ? $token->line : $this->context->get('CurrentLine', true); + $col = $token ? $token->col : $this->context->get('CurrentCol', true); + $attr = $this->context->get('CurrentAttr', true); + + // perform special substitutions, also add custom parameters + $subst = array(); + if (!is_null($token)) { + $args['CurrentToken'] = $token; + } + if (!is_null($attr)) { + $subst['$CurrentAttr.Name'] = $attr; + if (isset($token->attr[$attr])) { + $subst['$CurrentAttr.Value'] = $token->attr[$attr]; + } + } + + if (empty($args)) { + $msg = $this->locale->getMessage($msg); + } else { + $msg = $this->locale->formatMessage($msg, $args); + } + + if (!empty($subst)) { + $msg = strtr($msg, $subst); + } + + // (numerically indexed) + $error = array( + self::LINENO => $line, + self::SEVERITY => $severity, + self::MESSAGE => $msg, + self::CHILDREN => array() + ); + $this->_current[] = $error; + + // NEW CODE BELOW ... + // Top-level errors are either: + // TOKEN type, if $value is set appropriately, or + // "syntax" type, if $value is null + $new_struct = new HTMLPurifier_ErrorStruct(); + $new_struct->type = HTMLPurifier_ErrorStruct::TOKEN; + if ($token) { + $new_struct->value = clone $token; + } + if (is_int($line) && is_int($col)) { + if (isset($this->lines[$line][$col])) { + $struct = $this->lines[$line][$col]; + } else { + $struct = $this->lines[$line][$col] = $new_struct; + } + // These ksorts may present a performance problem + ksort($this->lines[$line], SORT_NUMERIC); + } else { + if (isset($this->lines[-1])) { + $struct = $this->lines[-1]; + } else { + $struct = $this->lines[-1] = $new_struct; + } + } + ksort($this->lines, SORT_NUMERIC); + + // Now, check if we need to operate on a lower structure + if (!empty($attr)) { + $struct = $struct->getChild(HTMLPurifier_ErrorStruct::ATTR, $attr); + if (!$struct->value) { + $struct->value = array($attr, 'PUT VALUE HERE'); + } + } + if (!empty($cssprop)) { + $struct = $struct->getChild(HTMLPurifier_ErrorStruct::CSSPROP, $cssprop); + if (!$struct->value) { + // if we tokenize CSS this might be a little more difficult to do + $struct->value = array($cssprop, 'PUT VALUE HERE'); + } + } + + // Ok, structs are all setup, now time to register the error + $struct->addError($severity, $msg); + } + + /** + * Retrieves raw error data for custom formatter to use + */ + public function getRaw() + { + return $this->errors; + } + + /** + * Default HTML formatting implementation for error messages + * @param HTMLPurifier_Config $config Configuration, vital for HTML output nature + * @param array $errors Errors array to display; used for recursion. + * @return string + */ + public function getHTMLFormatted($config, $errors = null) + { + $ret = array(); + + $this->generator = new HTMLPurifier_Generator($config, $this->context); + if ($errors === null) { + $errors = $this->errors; + } + + // 'At line' message needs to be removed + + // generation code for new structure goes here. It needs to be recursive. + foreach ($this->lines as $line => $col_array) { + if ($line == -1) { + continue; + } + foreach ($col_array as $col => $struct) { + $this->_renderStruct($ret, $struct, $line, $col); + } + } + if (isset($this->lines[-1])) { + $this->_renderStruct($ret, $this->lines[-1]); + } + + if (empty($errors)) { + return '

          ' . $this->locale->getMessage('ErrorCollector: No errors') . '

          '; + } else { + return '
          • ' . implode('
          • ', $ret) . '
          '; + } + + } + + private function _renderStruct(&$ret, $struct, $line = null, $col = null) + { + $stack = array($struct); + $context_stack = array(array()); + while ($current = array_pop($stack)) { + $context = array_pop($context_stack); + foreach ($current->errors as $error) { + list($severity, $msg) = $error; + $string = ''; + $string .= '
          '; + // W3C uses an icon to indicate the severity of the error. + $error = $this->locale->getErrorName($severity); + $string .= "$error "; + if (!is_null($line) && !is_null($col)) { + $string .= "Line $line, Column $col: "; + } else { + $string .= 'End of Document: '; + } + $string .= '' . $this->generator->escape($msg) . ' '; + $string .= '
          '; + // Here, have a marker for the character on the column appropriate. + // Be sure to clip extremely long lines. + //$string .= '
          ';
          +                //$string .= '';
          +                //$string .= '
          '; + $ret[] = $string; + } + foreach ($current->children as $array) { + $context[] = $current; + $stack = array_merge($stack, array_reverse($array, true)); + for ($i = count($array); $i > 0; $i--) { + $context_stack[] = $context; + } + } + } + } +} + + + + + +/** + * Records errors for particular segments of an HTML document such as tokens, + * attributes or CSS properties. They can contain error structs (which apply + * to components of what they represent), but their main purpose is to hold + * errors applying to whatever struct is being used. + */ +class HTMLPurifier_ErrorStruct +{ + + /** + * Possible values for $children first-key. Note that top-level structures + * are automatically token-level. + */ + const TOKEN = 0; + const ATTR = 1; + const CSSPROP = 2; + + /** + * Type of this struct. + * @type string + */ + public $type; + + /** + * Value of the struct we are recording errors for. There are various + * values for this: + * - TOKEN: Instance of HTMLPurifier_Token + * - ATTR: array('attr-name', 'value') + * - CSSPROP: array('prop-name', 'value') + * @type mixed + */ + public $value; + + /** + * Errors registered for this structure. + * @type array + */ + public $errors = array(); + + /** + * Child ErrorStructs that are from this structure. For example, a TOKEN + * ErrorStruct would contain ATTR ErrorStructs. This is a multi-dimensional + * array in structure: [TYPE]['identifier'] + * @type array + */ + public $children = array(); + + /** + * @param string $type + * @param string $id + * @return mixed + */ + public function getChild($type, $id) + { + if (!isset($this->children[$type][$id])) { + $this->children[$type][$id] = new HTMLPurifier_ErrorStruct(); + $this->children[$type][$id]->type = $type; + } + return $this->children[$type][$id]; + } + + /** + * @param int $severity + * @param string $message + */ + public function addError($severity, $message) + { + $this->errors[] = array($severity, $message); + } +} + + + + + +/** + * Global exception class for HTML Purifier; any exceptions we throw + * are from here. + */ +class HTMLPurifier_Exception extends Exception +{ + +} + + + + + +/** + * Represents a pre or post processing filter on HTML Purifier's output + * + * Sometimes, a little ad-hoc fixing of HTML has to be done before + * it gets sent through HTML Purifier: you can use filters to acheive + * this effect. For instance, YouTube videos can be preserved using + * this manner. You could have used a decorator for this task, but + * PHP's support for them is not terribly robust, so we're going + * to just loop through the filters. + * + * Filters should be exited first in, last out. If there are three filters, + * named 1, 2 and 3, the order of execution should go 1->preFilter, + * 2->preFilter, 3->preFilter, purify, 3->postFilter, 2->postFilter, + * 1->postFilter. + * + * @note Methods are not declared abstract as it is perfectly legitimate + * for an implementation not to want anything to happen on a step + */ + +class HTMLPurifier_Filter +{ + + /** + * Name of the filter for identification purposes. + * @type string + */ + public $name; + + /** + * Pre-processor function, handles HTML before HTML Purifier + * @param string $html + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function preFilter($html, $config, $context) + { + return $html; + } + + /** + * Post-processor function, handles HTML after HTML Purifier + * @param string $html + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function postFilter($html, $config, $context) + { + return $html; + } +} + + + + + +/** + * Generates HTML from tokens. + * @todo Refactor interface so that configuration/context is determined + * upon instantiation, no need for messy generateFromTokens() calls + * @todo Make some of the more internal functions protected, and have + * unit tests work around that + */ +class HTMLPurifier_Generator +{ + + /** + * Whether or not generator should produce XML output. + * @type bool + */ + private $_xhtml = true; + + /** + * :HACK: Whether or not generator should comment the insides of )#si', + array($this, 'scriptCallback'), + $html + ); + } + + $html = $this->normalize($html, $config, $context); + + $cursor = 0; // our location in the text + $inside_tag = false; // whether or not we're parsing the inside of a tag + $array = array(); // result array + + // This is also treated to mean maintain *column* numbers too + $maintain_line_numbers = $config->get('Core.MaintainLineNumbers'); + + if ($maintain_line_numbers === null) { + // automatically determine line numbering by checking + // if error collection is on + $maintain_line_numbers = $config->get('Core.CollectErrors'); + } + + if ($maintain_line_numbers) { + $current_line = 1; + $current_col = 0; + $length = strlen($html); + } else { + $current_line = false; + $current_col = false; + $length = false; + } + $context->register('CurrentLine', $current_line); + $context->register('CurrentCol', $current_col); + $nl = "\n"; + // how often to manually recalculate. This will ALWAYS be right, + // but it's pretty wasteful. Set to 0 to turn off + $synchronize_interval = $config->get('Core.DirectLexLineNumberSyncInterval'); + + $e = false; + if ($config->get('Core.CollectErrors')) { + $e =& $context->get('ErrorCollector'); + } + + // for testing synchronization + $loops = 0; + + while (++$loops) { + // $cursor is either at the start of a token, or inside of + // a tag (i.e. there was a < immediately before it), as indicated + // by $inside_tag + + if ($maintain_line_numbers) { + // $rcursor, however, is always at the start of a token. + $rcursor = $cursor - (int)$inside_tag; + + // Column number is cheap, so we calculate it every round. + // We're interested at the *end* of the newline string, so + // we need to add strlen($nl) == 1 to $nl_pos before subtracting it + // from our "rcursor" position. + $nl_pos = strrpos($html, $nl, $rcursor - $length); + $current_col = $rcursor - (is_bool($nl_pos) ? 0 : $nl_pos + 1); + + // recalculate lines + if ($synchronize_interval && // synchronization is on + $cursor > 0 && // cursor is further than zero + $loops % $synchronize_interval === 0) { // time to synchronize! + $current_line = 1 + $this->substrCount($html, $nl, 0, $cursor); + } + } + + $position_next_lt = strpos($html, '<', $cursor); + $position_next_gt = strpos($html, '>', $cursor); + + // triggers on "asdf" but not "asdf " + // special case to set up context + if ($position_next_lt === $cursor) { + $inside_tag = true; + $cursor++; + } + + if (!$inside_tag && $position_next_lt !== false) { + // We are not inside tag and there still is another tag to parse + $token = new + HTMLPurifier_Token_Text( + $this->parseData( + substr( + $html, + $cursor, + $position_next_lt - $cursor + ) + ) + ); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_lt - $cursor); + } + $array[] = $token; + $cursor = $position_next_lt + 1; + $inside_tag = true; + continue; + } elseif (!$inside_tag) { + // We are not inside tag but there are no more tags + // If we're already at the end, break + if ($cursor === strlen($html)) { + break; + } + // Create Text of rest of string + $token = new + HTMLPurifier_Token_Text( + $this->parseData( + substr( + $html, + $cursor + ) + ) + ); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + } + $array[] = $token; + break; + } elseif ($inside_tag && $position_next_gt !== false) { + // We are in tag and it is well formed + // Grab the internals of the tag + $strlen_segment = $position_next_gt - $cursor; + + if ($strlen_segment < 1) { + // there's nothing to process! + $token = new HTMLPurifier_Token_Text('<'); + $cursor++; + continue; + } + + $segment = substr($html, $cursor, $strlen_segment); + + if ($segment === false) { + // somehow, we attempted to access beyond the end of + // the string, defense-in-depth, reported by Nate Abele + break; + } + + // Check if it's a comment + if (substr($segment, 0, 3) === '!--') { + // re-determine segment length, looking for --> + $position_comment_end = strpos($html, '-->', $cursor); + if ($position_comment_end === false) { + // uh oh, we have a comment that extends to + // infinity. Can't be helped: set comment + // end position to end of string + if ($e) { + $e->send(E_WARNING, 'Lexer: Unclosed comment'); + } + $position_comment_end = strlen($html); + $end = true; + } else { + $end = false; + } + $strlen_segment = $position_comment_end - $cursor; + $segment = substr($html, $cursor, $strlen_segment); + $token = new + HTMLPurifier_Token_Comment( + substr( + $segment, + 3, + $strlen_segment - 3 + ) + ); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $strlen_segment); + } + $array[] = $token; + $cursor = $end ? $position_comment_end : $position_comment_end + 3; + $inside_tag = false; + continue; + } + + // Check if it's an end tag + $is_end_tag = (strpos($segment, '/') === 0); + if ($is_end_tag) { + $type = substr($segment, 1); + $token = new HTMLPurifier_Token_End($type); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; + $inside_tag = false; + $cursor = $position_next_gt + 1; + continue; + } + + // Check leading character is alnum, if not, we may + // have accidently grabbed an emoticon. Translate into + // text and go our merry way + if (!ctype_alpha($segment[0])) { + // XML: $segment[0] !== '_' && $segment[0] !== ':' + if ($e) { + $e->send(E_NOTICE, 'Lexer: Unescaped lt'); + } + $token = new HTMLPurifier_Token_Text('<'); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; + $inside_tag = false; + continue; + } + + // Check if it is explicitly self closing, if so, remove + // trailing slash. Remember, we could have a tag like
          , so + // any later token processing scripts must convert improperly + // classified EmptyTags from StartTags. + $is_self_closing = (strrpos($segment, '/') === $strlen_segment - 1); + if ($is_self_closing) { + $strlen_segment--; + $segment = substr($segment, 0, $strlen_segment); + } + + // Check if there are any attributes + $position_first_space = strcspn($segment, $this->_whitespace); + + if ($position_first_space >= $strlen_segment) { + if ($is_self_closing) { + $token = new HTMLPurifier_Token_Empty($segment); + } else { + $token = new HTMLPurifier_Token_Start($segment); + } + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; + $inside_tag = false; + $cursor = $position_next_gt + 1; + continue; + } + + // Grab out all the data + $type = substr($segment, 0, $position_first_space); + $attribute_string = + trim( + substr( + $segment, + $position_first_space + ) + ); + if ($attribute_string) { + $attr = $this->parseAttributeString( + $attribute_string, + $config, + $context + ); + } else { + $attr = array(); + } + + if ($is_self_closing) { + $token = new HTMLPurifier_Token_Empty($type, $attr); + } else { + $token = new HTMLPurifier_Token_Start($type, $attr); + } + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; + $cursor = $position_next_gt + 1; + $inside_tag = false; + continue; + } else { + // inside tag, but there's no ending > sign + if ($e) { + $e->send(E_WARNING, 'Lexer: Missing gt'); + } + $token = new + HTMLPurifier_Token_Text( + '<' . + $this->parseData( + substr($html, $cursor) + ) + ); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + } + // no cursor scroll? Hmm... + $array[] = $token; + break; + } + break; + } + + $context->destroy('CurrentLine'); + $context->destroy('CurrentCol'); + return $array; + } + + /** + * PHP 5.0.x compatible substr_count that implements offset and length + * @param string $haystack + * @param string $needle + * @param int $offset + * @param int $length + * @return int + */ + protected function substrCount($haystack, $needle, $offset, $length) + { + static $oldVersion; + if ($oldVersion === null) { + $oldVersion = version_compare(PHP_VERSION, '5.1', '<'); + } + if ($oldVersion) { + $haystack = substr($haystack, $offset, $length); + return substr_count($haystack, $needle); + } else { + return substr_count($haystack, $needle, $offset, $length); + } + } + + /** + * Takes the inside of an HTML tag and makes an assoc array of attributes. + * + * @param string $string Inside of tag excluding name. + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array Assoc array of attributes. + */ + public function parseAttributeString($string, $config, $context) + { + $string = (string)$string; // quick typecast + + if ($string == '') { + return array(); + } // no attributes + + $e = false; + if ($config->get('Core.CollectErrors')) { + $e =& $context->get('ErrorCollector'); + } + + // let's see if we can abort as quickly as possible + // one equal sign, no spaces => one attribute + $num_equal = substr_count($string, '='); + $has_space = strpos($string, ' '); + if ($num_equal === 0 && !$has_space) { + // bool attribute + return array($string => $string); + } elseif ($num_equal === 1 && !$has_space) { + // only one attribute + list($key, $quoted_value) = explode('=', $string); + $quoted_value = trim($quoted_value); + if (!$key) { + if ($e) { + $e->send(E_ERROR, 'Lexer: Missing attribute key'); + } + return array(); + } + if (!$quoted_value) { + return array($key => ''); + } + $first_char = @$quoted_value[0]; + $last_char = @$quoted_value[strlen($quoted_value) - 1]; + + $same_quote = ($first_char == $last_char); + $open_quote = ($first_char == '"' || $first_char == "'"); + + if ($same_quote && $open_quote) { + // well behaved + $value = substr($quoted_value, 1, strlen($quoted_value) - 2); + } else { + // not well behaved + if ($open_quote) { + if ($e) { + $e->send(E_ERROR, 'Lexer: Missing end quote'); + } + $value = substr($quoted_value, 1); + } else { + $value = $quoted_value; + } + } + if ($value === false) { + $value = ''; + } + return array($key => $this->parseData($value)); + } + + // setup loop environment + $array = array(); // return assoc array of attributes + $cursor = 0; // current position in string (moves forward) + $size = strlen($string); // size of the string (stays the same) + + // if we have unquoted attributes, the parser expects a terminating + // space, so let's guarantee that there's always a terminating space. + $string .= ' '; + + $old_cursor = -1; + while ($cursor < $size) { + if ($old_cursor >= $cursor) { + throw new Exception("Infinite loop detected"); + } + $old_cursor = $cursor; + + $cursor += ($value = strspn($string, $this->_whitespace, $cursor)); + // grab the key + + $key_begin = $cursor; //we're currently at the start of the key + + // scroll past all characters that are the key (not whitespace or =) + $cursor += strcspn($string, $this->_whitespace . '=', $cursor); + + $key_end = $cursor; // now at the end of the key + + $key = substr($string, $key_begin, $key_end - $key_begin); + + if (!$key) { + if ($e) { + $e->send(E_ERROR, 'Lexer: Missing attribute key'); + } + $cursor += 1 + strcspn($string, $this->_whitespace, $cursor + 1); // prevent infinite loop + continue; // empty key + } + + // scroll past all whitespace + $cursor += strspn($string, $this->_whitespace, $cursor); + + if ($cursor >= $size) { + $array[$key] = $key; + break; + } + + // if the next character is an equal sign, we've got a regular + // pair, otherwise, it's a bool attribute + $first_char = @$string[$cursor]; + + if ($first_char == '=') { + // key="value" + + $cursor++; + $cursor += strspn($string, $this->_whitespace, $cursor); + + if ($cursor === false) { + $array[$key] = ''; + break; + } + + // we might be in front of a quote right now + + $char = @$string[$cursor]; + + if ($char == '"' || $char == "'") { + // it's quoted, end bound is $char + $cursor++; + $value_begin = $cursor; + $cursor = strpos($string, $char, $cursor); + $value_end = $cursor; + } else { + // it's not quoted, end bound is whitespace + $value_begin = $cursor; + $cursor += strcspn($string, $this->_whitespace, $cursor); + $value_end = $cursor; + } + + // we reached a premature end + if ($cursor === false) { + $cursor = $size; + $value_end = $cursor; + } + + $value = substr($string, $value_begin, $value_end - $value_begin); + if ($value === false) { + $value = ''; + } + $array[$key] = $this->parseData($value); + $cursor++; + } else { + // boolattr + if ($key !== '') { + $array[$key] = $key; + } else { + // purely theoretical + if ($e) { + $e->send(E_ERROR, 'Lexer: Missing attribute key'); + } + } + } + } + return $array; + } +} + + + + + +/** + * Concrete comment node class. + */ +class HTMLPurifier_Node_Comment extends HTMLPurifier_Node +{ + /** + * Character data within comment. + * @type string + */ + public $data; + + /** + * @type bool + */ + public $is_whitespace = true; + + /** + * Transparent constructor. + * + * @param string $data String comment data. + * @param int $line + * @param int $col + */ + public function __construct($data, $line = null, $col = null) + { + $this->data = $data; + $this->line = $line; + $this->col = $col; + } + + public function toTokenPair() { + return array(new HTMLPurifier_Token_Comment($this->data, $this->line, $this->col), null); + } +} + + + +/** + * Concrete element node class. + */ +class HTMLPurifier_Node_Element extends HTMLPurifier_Node +{ + /** + * The lower-case name of the tag, like 'a', 'b' or 'blockquote'. + * + * @note Strictly speaking, XML tags are case sensitive, so we shouldn't + * be lower-casing them, but these tokens cater to HTML tags, which are + * insensitive. + * @type string + */ + public $name; + + /** + * Associative array of the node's attributes. + * @type array + */ + public $attr = array(); + + /** + * List of child elements. + * @type array + */ + public $children = array(); + + /** + * Does this use the form or the form, i.e. + * is it a pair of start/end tokens or an empty token. + * @bool + */ + public $empty = false; + + public $endCol = null, $endLine = null, $endArmor = array(); + + public function __construct($name, $attr = array(), $line = null, $col = null, $armor = array()) { + $this->name = $name; + $this->attr = $attr; + $this->line = $line; + $this->col = $col; + $this->armor = $armor; + } + + public function toTokenPair() { + // XXX inefficiency here, normalization is not necessary + if ($this->empty) { + return array(new HTMLPurifier_Token_Empty($this->name, $this->attr, $this->line, $this->col, $this->armor), null); + } else { + $start = new HTMLPurifier_Token_Start($this->name, $this->attr, $this->line, $this->col, $this->armor); + $end = new HTMLPurifier_Token_End($this->name, array(), $this->endLine, $this->endCol, $this->endArmor); + //$end->start = $start; + return array($start, $end); + } + } +} + + + + +/** + * Concrete text token class. + * + * Text tokens comprise of regular parsed character data (PCDATA) and raw + * character data (from the CDATA sections). Internally, their + * data is parsed with all entities expanded. Surprisingly, the text token + * does have a "tag name" called #PCDATA, which is how the DTD represents it + * in permissible child nodes. + */ +class HTMLPurifier_Node_Text extends HTMLPurifier_Node +{ + + /** + * PCDATA tag name compatible with DTD, see + * HTMLPurifier_ChildDef_Custom for details. + * @type string + */ + public $name = '#PCDATA'; + + /** + * @type string + */ + public $data; + /**< Parsed character data of text. */ + + /** + * @type bool + */ + public $is_whitespace; + + /**< Bool indicating if node is whitespace. */ + + /** + * Constructor, accepts data and determines if it is whitespace. + * @param string $data String parsed character data. + * @param int $line + * @param int $col + */ + public function __construct($data, $is_whitespace, $line = null, $col = null) + { + $this->data = $data; + $this->is_whitespace = $is_whitespace; + $this->line = $line; + $this->col = $col; + } + + public function toTokenPair() { + return array(new HTMLPurifier_Token_Text($this->data, $this->line, $this->col), null); + } +} + + + + + +/** + * Composite strategy that runs multiple strategies on tokens. + */ +abstract class HTMLPurifier_Strategy_Composite extends HTMLPurifier_Strategy +{ + + /** + * List of strategies to run tokens through. + * @type HTMLPurifier_Strategy[] + */ + protected $strategies = array(); + + /** + * @param HTMLPurifier_Token[] $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return HTMLPurifier_Token[] + */ + public function execute($tokens, $config, $context) + { + foreach ($this->strategies as $strategy) { + $tokens = $strategy->execute($tokens, $config, $context); + } + return $tokens; + } +} + + + + + +/** + * Core strategy composed of the big four strategies. + */ +class HTMLPurifier_Strategy_Core extends HTMLPurifier_Strategy_Composite +{ + public function __construct() + { + $this->strategies[] = new HTMLPurifier_Strategy_RemoveForeignElements(); + $this->strategies[] = new HTMLPurifier_Strategy_MakeWellFormed(); + $this->strategies[] = new HTMLPurifier_Strategy_FixNesting(); + $this->strategies[] = new HTMLPurifier_Strategy_ValidateAttributes(); + } +} + + + + + +/** + * Takes a well formed list of tokens and fixes their nesting. + * + * HTML elements dictate which elements are allowed to be their children, + * for example, you can't have a p tag in a span tag. Other elements have + * much more rigorous definitions: tables, for instance, require a specific + * order for their elements. There are also constraints not expressible by + * document type definitions, such as the chameleon nature of ins/del + * tags and global child exclusions. + * + * The first major objective of this strategy is to iterate through all + * the nodes and determine whether or not their children conform to the + * element's definition. If they do not, the child definition may + * optionally supply an amended list of elements that is valid or + * require that the entire node be deleted (and the previous node + * rescanned). + * + * The second objective is to ensure that explicitly excluded elements of + * an element do not appear in its children. Code that accomplishes this + * task is pervasive through the strategy, though the two are distinct tasks + * and could, theoretically, be seperated (although it's not recommended). + * + * @note Whether or not unrecognized children are silently dropped or + * translated into text depends on the child definitions. + * + * @todo Enable nodes to be bubbled out of the structure. This is + * easier with our new algorithm. + */ + +class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy +{ + + /** + * @param HTMLPurifier_Token[] $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array|HTMLPurifier_Token[] + */ + public function execute($tokens, $config, $context) + { + + //####################################################################// + // Pre-processing + + // O(n) pass to convert to a tree, so that we can efficiently + // refer to substrings + $top_node = HTMLPurifier_Arborize::arborize($tokens, $config, $context); + + // get a copy of the HTML definition + $definition = $config->getHTMLDefinition(); + + $excludes_enabled = !$config->get('Core.DisableExcludes'); + + // setup the context variable 'IsInline', for chameleon processing + // is 'false' when we are not inline, 'true' when it must always + // be inline, and an integer when it is inline for a certain + // branch of the document tree + $is_inline = $definition->info_parent_def->descendants_are_inline; + $context->register('IsInline', $is_inline); + + // setup error collector + $e =& $context->get('ErrorCollector', true); + + //####################################################################// + // Loop initialization + + // stack that contains all elements that are excluded + // it is organized by parent elements, similar to $stack, + // but it is only populated when an element with exclusions is + // processed, i.e. there won't be empty exclusions. + $exclude_stack = array($definition->info_parent_def->excludes); + + // variable that contains the start token while we are processing + // nodes. This enables error reporting to do its job + $node = $top_node; + // dummy token + list($token, $d) = $node->toTokenPair(); + $context->register('CurrentNode', $node); + $context->register('CurrentToken', $token); + + //####################################################################// + // Loop + + // We need to implement a post-order traversal iteratively, to + // avoid running into stack space limits. This is pretty tricky + // to reason about, so we just manually stack-ify the recursive + // variant: + // + // function f($node) { + // foreach ($node->children as $child) { + // f($child); + // } + // validate($node); + // } + // + // Thus, we will represent a stack frame as array($node, + // $is_inline, stack of children) + // e.g. array_reverse($node->children) - already processed + // children. + + $parent_def = $definition->info_parent_def; + $stack = array( + array($top_node, + $parent_def->descendants_are_inline, + $parent_def->excludes, // exclusions + 0) + ); + + while (!empty($stack)) { + list($node, $is_inline, $excludes, $ix) = array_pop($stack); + // recursive call + $go = false; + $def = empty($stack) ? $definition->info_parent_def : $definition->info[$node->name]; + while (isset($node->children[$ix])) { + $child = $node->children[$ix++]; + if ($child instanceof HTMLPurifier_Node_Element) { + $go = true; + $stack[] = array($node, $is_inline, $excludes, $ix); + $stack[] = array($child, + // ToDo: I don't think it matters if it's def or + // child_def, but double check this... + $is_inline || $def->descendants_are_inline, + empty($def->excludes) ? $excludes + : array_merge($excludes, $def->excludes), + 0); + break; + } + }; + if ($go) continue; + list($token, $d) = $node->toTokenPair(); + // base case + if ($excludes_enabled && isset($excludes[$node->name])) { + $node->dead = true; + if ($e) $e->send(E_ERROR, 'Strategy_FixNesting: Node excluded'); + } else { + // XXX I suppose it would be slightly more efficient to + // avoid the allocation here and have children + // strategies handle it + $children = array(); + foreach ($node->children as $child) { + if (!$child->dead) $children[] = $child; + } + $result = $def->child->validateChildren($children, $config, $context); + if ($result === true) { + // nop + $node->children = $children; + } elseif ($result === false) { + $node->dead = true; + if ($e) $e->send(E_ERROR, 'Strategy_FixNesting: Node removed'); + } else { + $node->children = $result; + if ($e) { + // XXX This will miss mutations of internal nodes. Perhaps defer to the child validators + if (empty($result) && !empty($children)) { + $e->send(E_ERROR, 'Strategy_FixNesting: Node contents removed'); + } else if ($result != $children) { + $e->send(E_WARNING, 'Strategy_FixNesting: Node reorganized'); + } + } + } + } + } + + //####################################################################// + // Post-processing + + // remove context variables + $context->destroy('IsInline'); + $context->destroy('CurrentNode'); + $context->destroy('CurrentToken'); + + //####################################################################// + // Return + + return HTMLPurifier_Arborize::flatten($node, $config, $context); + } +} + + + + + +/** + * Takes tokens makes them well-formed (balance end tags, etc.) + * + * Specification of the armor attributes this strategy uses: + * + * - MakeWellFormed_TagClosedError: This armor field is used to + * suppress tag closed errors for certain tokens [TagClosedSuppress], + * in particular, if a tag was generated automatically by HTML + * Purifier, we may rely on our infrastructure to close it for us + * and shouldn't report an error to the user [TagClosedAuto]. + */ +class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy +{ + + /** + * Array stream of tokens being processed. + * @type HTMLPurifier_Token[] + */ + protected $tokens; + + /** + * Current token. + * @type HTMLPurifier_Token + */ + protected $token; + + /** + * Zipper managing the true state. + * @type HTMLPurifier_Zipper + */ + protected $zipper; + + /** + * Current nesting of elements. + * @type array + */ + protected $stack; + + /** + * Injectors active in this stream processing. + * @type HTMLPurifier_Injector[] + */ + protected $injectors; + + /** + * Current instance of HTMLPurifier_Config. + * @type HTMLPurifier_Config + */ + protected $config; + + /** + * Current instance of HTMLPurifier_Context. + * @type HTMLPurifier_Context + */ + protected $context; + + /** + * @param HTMLPurifier_Token[] $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return HTMLPurifier_Token[] + * @throws HTMLPurifier_Exception + */ + public function execute($tokens, $config, $context) + { + $definition = $config->getHTMLDefinition(); + + // local variables + $generator = new HTMLPurifier_Generator($config, $context); + $escape_invalid_tags = $config->get('Core.EscapeInvalidTags'); + // used for autoclose early abortion + $global_parent_allowed_elements = $definition->info_parent_def->child->getAllowedElements($config); + $e = $context->get('ErrorCollector', true); + $i = false; // injector index + list($zipper, $token) = HTMLPurifier_Zipper::fromArray($tokens); + if ($token === NULL) { + return array(); + } + $reprocess = false; // whether or not to reprocess the same token + $stack = array(); + + // member variables + $this->stack =& $stack; + $this->tokens =& $tokens; + $this->token =& $token; + $this->zipper =& $zipper; + $this->config = $config; + $this->context = $context; + + // context variables + $context->register('CurrentNesting', $stack); + $context->register('InputZipper', $zipper); + $context->register('CurrentToken', $token); + + // -- begin INJECTOR -- + + $this->injectors = array(); + + $injectors = $config->getBatch('AutoFormat'); + $def_injectors = $definition->info_injector; + $custom_injectors = $injectors['Custom']; + unset($injectors['Custom']); // special case + foreach ($injectors as $injector => $b) { + // XXX: Fix with a legitimate lookup table of enabled filters + if (strpos($injector, '.') !== false) { + continue; + } + $injector = "HTMLPurifier_Injector_$injector"; + if (!$b) { + continue; + } + $this->injectors[] = new $injector; + } + foreach ($def_injectors as $injector) { + // assumed to be objects + $this->injectors[] = $injector; + } + foreach ($custom_injectors as $injector) { + if (!$injector) { + continue; + } + if (is_string($injector)) { + $injector = "HTMLPurifier_Injector_$injector"; + $injector = new $injector; + } + $this->injectors[] = $injector; + } + + // give the injectors references to the definition and context + // variables for performance reasons + foreach ($this->injectors as $ix => $injector) { + $error = $injector->prepare($config, $context); + if (!$error) { + continue; + } + array_splice($this->injectors, $ix, 1); // rm the injector + trigger_error("Cannot enable {$injector->name} injector because $error is not allowed", E_USER_WARNING); + } + + // -- end INJECTOR -- + + // a note on reprocessing: + // In order to reduce code duplication, whenever some code needs + // to make HTML changes in order to make things "correct", the + // new HTML gets sent through the purifier, regardless of its + // status. This means that if we add a start token, because it + // was totally necessary, we don't have to update nesting; we just + // punt ($reprocess = true; continue;) and it does that for us. + + // isset is in loop because $tokens size changes during loop exec + for (;; + // only increment if we don't need to reprocess + $reprocess ? $reprocess = false : $token = $zipper->next($token)) { + + // check for a rewind + if (is_int($i)) { + // possibility: disable rewinding if the current token has a + // rewind set on it already. This would offer protection from + // infinite loop, but might hinder some advanced rewinding. + $rewind_offset = $this->injectors[$i]->getRewindOffset(); + if (is_int($rewind_offset)) { + for ($j = 0; $j < $rewind_offset; $j++) { + if (empty($zipper->front)) break; + $token = $zipper->prev($token); + // indicate that other injectors should not process this token, + // but we need to reprocess it + unset($token->skip[$i]); + $token->rewind = $i; + if ($token instanceof HTMLPurifier_Token_Start) { + array_pop($this->stack); + } elseif ($token instanceof HTMLPurifier_Token_End) { + $this->stack[] = $token->start; + } + } + } + $i = false; + } + + // handle case of document end + if ($token === NULL) { + // kill processing if stack is empty + if (empty($this->stack)) { + break; + } + + // peek + $top_nesting = array_pop($this->stack); + $this->stack[] = $top_nesting; + + // send error [TagClosedSuppress] + if ($e && !isset($top_nesting->armor['MakeWellFormed_TagClosedError'])) { + $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by document end', $top_nesting); + } + + // append, don't splice, since this is the end + $token = new HTMLPurifier_Token_End($top_nesting->name); + + // punt! + $reprocess = true; + continue; + } + + //echo '
          '; printZipper($zipper, $token);//printTokens($this->stack); + //flush(); + + // quick-check: if it's not a tag, no need to process + if (empty($token->is_tag)) { + if ($token instanceof HTMLPurifier_Token_Text) { + foreach ($this->injectors as $i => $injector) { + if (isset($token->skip[$i])) { + continue; + } + if ($token->rewind !== null && $token->rewind !== $i) { + continue; + } + // XXX fuckup + $r = $token; + $injector->handleText($r); + $token = $this->processToken($r, $i); + $reprocess = true; + break; + } + } + // another possibility is a comment + continue; + } + + if (isset($definition->info[$token->name])) { + $type = $definition->info[$token->name]->child->type; + } else { + $type = false; // Type is unknown, treat accordingly + } + + // quick tag checks: anything that's *not* an end tag + $ok = false; + if ($type === 'empty' && $token instanceof HTMLPurifier_Token_Start) { + // claims to be a start tag but is empty + $token = new HTMLPurifier_Token_Empty( + $token->name, + $token->attr, + $token->line, + $token->col, + $token->armor + ); + $ok = true; + } elseif ($type && $type !== 'empty' && $token instanceof HTMLPurifier_Token_Empty) { + // claims to be empty but really is a start tag + // NB: this assignment is required + $old_token = $token; + $token = new HTMLPurifier_Token_End($token->name); + $token = $this->insertBefore( + new HTMLPurifier_Token_Start($old_token->name, $old_token->attr, $old_token->line, $old_token->col, $old_token->armor) + ); + // punt (since we had to modify the input stream in a non-trivial way) + $reprocess = true; + continue; + } elseif ($token instanceof HTMLPurifier_Token_Empty) { + // real empty token + $ok = true; + } elseif ($token instanceof HTMLPurifier_Token_Start) { + // start tag + + // ...unless they also have to close their parent + if (!empty($this->stack)) { + + // Performance note: you might think that it's rather + // inefficient, recalculating the autoclose information + // for every tag that a token closes (since when we + // do an autoclose, we push a new token into the + // stream and then /process/ that, before + // re-processing this token.) But this is + // necessary, because an injector can make an + // arbitrary transformations to the autoclosing + // tokens we introduce, so things may have changed + // in the meantime. Also, doing the inefficient thing is + // "easy" to reason about (for certain perverse definitions + // of "easy") + + $parent = array_pop($this->stack); + $this->stack[] = $parent; + + $parent_def = null; + $parent_elements = null; + $autoclose = false; + if (isset($definition->info[$parent->name])) { + $parent_def = $definition->info[$parent->name]; + $parent_elements = $parent_def->child->getAllowedElements($config); + $autoclose = !isset($parent_elements[$token->name]); + } + + if ($autoclose && $definition->info[$token->name]->wrap) { + // Check if an element can be wrapped by another + // element to make it valid in a context (for + // example,
              needs a
            • in between) + $wrapname = $definition->info[$token->name]->wrap; + $wrapdef = $definition->info[$wrapname]; + $elements = $wrapdef->child->getAllowedElements($config); + if (isset($elements[$token->name]) && isset($parent_elements[$wrapname])) { + $newtoken = new HTMLPurifier_Token_Start($wrapname); + $token = $this->insertBefore($newtoken); + $reprocess = true; + continue; + } + } + + $carryover = false; + if ($autoclose && $parent_def->formatting) { + $carryover = true; + } + + if ($autoclose) { + // check if this autoclose is doomed to fail + // (this rechecks $parent, which his harmless) + $autoclose_ok = isset($global_parent_allowed_elements[$token->name]); + if (!$autoclose_ok) { + foreach ($this->stack as $ancestor) { + $elements = $definition->info[$ancestor->name]->child->getAllowedElements($config); + if (isset($elements[$token->name])) { + $autoclose_ok = true; + break; + } + if ($definition->info[$token->name]->wrap) { + $wrapname = $definition->info[$token->name]->wrap; + $wrapdef = $definition->info[$wrapname]; + $wrap_elements = $wrapdef->child->getAllowedElements($config); + if (isset($wrap_elements[$token->name]) && isset($elements[$wrapname])) { + $autoclose_ok = true; + break; + } + } + } + } + if ($autoclose_ok) { + // errors need to be updated + $new_token = new HTMLPurifier_Token_End($parent->name); + $new_token->start = $parent; + // [TagClosedSuppress] + if ($e && !isset($parent->armor['MakeWellFormed_TagClosedError'])) { + if (!$carryover) { + $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag auto closed', $parent); + } else { + $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag carryover', $parent); + } + } + if ($carryover) { + $element = clone $parent; + // [TagClosedAuto] + $element->armor['MakeWellFormed_TagClosedError'] = true; + $element->carryover = true; + $token = $this->processToken(array($new_token, $token, $element)); + } else { + $token = $this->insertBefore($new_token); + } + } else { + $token = $this->remove(); + } + $reprocess = true; + continue; + } + + } + $ok = true; + } + + if ($ok) { + foreach ($this->injectors as $i => $injector) { + if (isset($token->skip[$i])) { + continue; + } + if ($token->rewind !== null && $token->rewind !== $i) { + continue; + } + $r = $token; + $injector->handleElement($r); + $token = $this->processToken($r, $i); + $reprocess = true; + break; + } + if (!$reprocess) { + // ah, nothing interesting happened; do normal processing + if ($token instanceof HTMLPurifier_Token_Start) { + $this->stack[] = $token; + } elseif ($token instanceof HTMLPurifier_Token_End) { + throw new HTMLPurifier_Exception( + 'Improper handling of end tag in start code; possible error in MakeWellFormed' + ); + } + } + continue; + } + + // sanity check: we should be dealing with a closing tag + if (!$token instanceof HTMLPurifier_Token_End) { + throw new HTMLPurifier_Exception('Unaccounted for tag token in input stream, bug in HTML Purifier'); + } + + // make sure that we have something open + if (empty($this->stack)) { + if ($escape_invalid_tags) { + if ($e) { + $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag to text'); + } + $token = new HTMLPurifier_Token_Text($generator->generateFromToken($token)); + } else { + if ($e) { + $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag removed'); + } + $token = $this->remove(); + } + $reprocess = true; + continue; + } + + // first, check for the simplest case: everything closes neatly. + // Eventually, everything passes through here; if there are problems + // we modify the input stream accordingly and then punt, so that + // the tokens get processed again. + $current_parent = array_pop($this->stack); + if ($current_parent->name == $token->name) { + $token->start = $current_parent; + foreach ($this->injectors as $i => $injector) { + if (isset($token->skip[$i])) { + continue; + } + if ($token->rewind !== null && $token->rewind !== $i) { + continue; + } + $r = $token; + $injector->handleEnd($r); + $token = $this->processToken($r, $i); + $this->stack[] = $current_parent; + $reprocess = true; + break; + } + continue; + } + + // okay, so we're trying to close the wrong tag + + // undo the pop previous pop + $this->stack[] = $current_parent; + + // scroll back the entire nest, trying to find our tag. + // (feature could be to specify how far you'd like to go) + $size = count($this->stack); + // -2 because -1 is the last element, but we already checked that + $skipped_tags = false; + for ($j = $size - 2; $j >= 0; $j--) { + if ($this->stack[$j]->name == $token->name) { + $skipped_tags = array_slice($this->stack, $j); + break; + } + } + + // we didn't find the tag, so remove + if ($skipped_tags === false) { + if ($escape_invalid_tags) { + if ($e) { + $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag to text'); + } + $token = new HTMLPurifier_Token_Text($generator->generateFromToken($token)); + } else { + if ($e) { + $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag removed'); + } + $token = $this->remove(); + } + $reprocess = true; + continue; + } + + // do errors, in REVERSE $j order: a,b,c with + $c = count($skipped_tags); + if ($e) { + for ($j = $c - 1; $j > 0; $j--) { + // notice we exclude $j == 0, i.e. the current ending tag, from + // the errors... [TagClosedSuppress] + if (!isset($skipped_tags[$j]->armor['MakeWellFormed_TagClosedError'])) { + $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by element end', $skipped_tags[$j]); + } + } + } + + // insert tags, in FORWARD $j order: c,b,a with + $replace = array($token); + for ($j = 1; $j < $c; $j++) { + // ...as well as from the insertions + $new_token = new HTMLPurifier_Token_End($skipped_tags[$j]->name); + $new_token->start = $skipped_tags[$j]; + array_unshift($replace, $new_token); + if (isset($definition->info[$new_token->name]) && $definition->info[$new_token->name]->formatting) { + // [TagClosedAuto] + $element = clone $skipped_tags[$j]; + $element->carryover = true; + $element->armor['MakeWellFormed_TagClosedError'] = true; + $replace[] = $element; + } + } + $token = $this->processToken($replace); + $reprocess = true; + continue; + } + + $context->destroy('CurrentToken'); + $context->destroy('CurrentNesting'); + $context->destroy('InputZipper'); + + unset($this->injectors, $this->stack, $this->tokens); + return $zipper->toArray($token); + } + + /** + * Processes arbitrary token values for complicated substitution patterns. + * In general: + * + * If $token is an array, it is a list of tokens to substitute for the + * current token. These tokens then get individually processed. If there + * is a leading integer in the list, that integer determines how many + * tokens from the stream should be removed. + * + * If $token is a regular token, it is swapped with the current token. + * + * If $token is false, the current token is deleted. + * + * If $token is an integer, that number of tokens (with the first token + * being the current one) will be deleted. + * + * @param HTMLPurifier_Token|array|int|bool $token Token substitution value + * @param HTMLPurifier_Injector|int $injector Injector that performed the substitution; default is if + * this is not an injector related operation. + * @throws HTMLPurifier_Exception + */ + protected function processToken($token, $injector = -1) + { + // normalize forms of token + if (is_object($token)) { + $token = array(1, $token); + } + if (is_int($token)) { + $token = array($token); + } + if ($token === false) { + $token = array(1); + } + if (!is_array($token)) { + throw new HTMLPurifier_Exception('Invalid token type from injector'); + } + if (!is_int($token[0])) { + array_unshift($token, 1); + } + if ($token[0] === 0) { + throw new HTMLPurifier_Exception('Deleting zero tokens is not valid'); + } + + // $token is now an array with the following form: + // array(number nodes to delete, new node 1, new node 2, ...) + + $delete = array_shift($token); + list($old, $r) = $this->zipper->splice($this->token, $delete, $token); + + if ($injector > -1) { + // determine appropriate skips + $oldskip = isset($old[0]) ? $old[0]->skip : array(); + foreach ($token as $object) { + $object->skip = $oldskip; + $object->skip[$injector] = true; + } + } + + return $r; + + } + + /** + * Inserts a token before the current token. Cursor now points to + * this token. You must reprocess after this. + * @param HTMLPurifier_Token $token + */ + private function insertBefore($token) + { + // NB not $this->zipper->insertBefore(), due to positioning + // differences + $splice = $this->zipper->splice($this->token, 0, array($token)); + + return $splice[1]; + } + + /** + * Removes current token. Cursor now points to new token occupying previously + * occupied space. You must reprocess after this. + */ + private function remove() + { + return $this->zipper->delete(); + } +} + + + + + +/** + * Removes all unrecognized tags from the list of tokens. + * + * This strategy iterates through all the tokens and removes unrecognized + * tokens. If a token is not recognized but a TagTransform is defined for + * that element, the element will be transformed accordingly. + */ + +class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy +{ + + /** + * @param HTMLPurifier_Token[] $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array|HTMLPurifier_Token[] + */ + public function execute($tokens, $config, $context) + { + $definition = $config->getHTMLDefinition(); + $generator = new HTMLPurifier_Generator($config, $context); + $result = array(); + + $escape_invalid_tags = $config->get('Core.EscapeInvalidTags'); + $remove_invalid_img = $config->get('Core.RemoveInvalidImg'); + + // currently only used to determine if comments should be kept + $trusted = $config->get('HTML.Trusted'); + $comment_lookup = $config->get('HTML.AllowedComments'); + $comment_regexp = $config->get('HTML.AllowedCommentsRegexp'); + $check_comments = $comment_lookup !== array() || $comment_regexp !== null; + + $remove_script_contents = $config->get('Core.RemoveScriptContents'); + $hidden_elements = $config->get('Core.HiddenElements'); + + // remove script contents compatibility + if ($remove_script_contents === true) { + $hidden_elements['script'] = true; + } elseif ($remove_script_contents === false && isset($hidden_elements['script'])) { + unset($hidden_elements['script']); + } + + $attr_validator = new HTMLPurifier_AttrValidator(); + + // removes tokens until it reaches a closing tag with its value + $remove_until = false; + + // converts comments into text tokens when this is equal to a tag name + $textify_comments = false; + + $token = false; + $context->register('CurrentToken', $token); + + $e = false; + if ($config->get('Core.CollectErrors')) { + $e =& $context->get('ErrorCollector'); + } + + foreach ($tokens as $token) { + if ($remove_until) { + if (empty($token->is_tag) || $token->name !== $remove_until) { + continue; + } + } + if (!empty($token->is_tag)) { + // DEFINITION CALL + + // before any processing, try to transform the element + if (isset($definition->info_tag_transform[$token->name])) { + $original_name = $token->name; + // there is a transformation for this tag + // DEFINITION CALL + $token = $definition-> + info_tag_transform[$token->name]->transform($token, $config, $context); + if ($e) { + $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Tag transform', $original_name); + } + } + + if (isset($definition->info[$token->name])) { + // mostly everything's good, but + // we need to make sure required attributes are in order + if (($token instanceof HTMLPurifier_Token_Start || $token instanceof HTMLPurifier_Token_Empty) && + $definition->info[$token->name]->required_attr && + ($token->name != 'img' || $remove_invalid_img) // ensure config option still works + ) { + $attr_validator->validateToken($token, $config, $context); + $ok = true; + foreach ($definition->info[$token->name]->required_attr as $name) { + if (!isset($token->attr[$name])) { + $ok = false; + break; + } + } + if (!$ok) { + if ($e) { + $e->send( + E_ERROR, + 'Strategy_RemoveForeignElements: Missing required attribute', + $name + ); + } + continue; + } + $token->armor['ValidateAttributes'] = true; + } + + if (isset($hidden_elements[$token->name]) && $token instanceof HTMLPurifier_Token_Start) { + $textify_comments = $token->name; + } elseif ($token->name === $textify_comments && $token instanceof HTMLPurifier_Token_End) { + $textify_comments = false; + } + + } elseif ($escape_invalid_tags) { + // invalid tag, generate HTML representation and insert in + if ($e) { + $e->send(E_WARNING, 'Strategy_RemoveForeignElements: Foreign element to text'); + } + $token = new HTMLPurifier_Token_Text( + $generator->generateFromToken($token) + ); + } else { + // check if we need to destroy all of the tag's children + // CAN BE GENERICIZED + if (isset($hidden_elements[$token->name])) { + if ($token instanceof HTMLPurifier_Token_Start) { + $remove_until = $token->name; + } elseif ($token instanceof HTMLPurifier_Token_Empty) { + // do nothing: we're still looking + } else { + $remove_until = false; + } + if ($e) { + $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Foreign meta element removed'); + } + } else { + if ($e) { + $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Foreign element removed'); + } + } + continue; + } + } elseif ($token instanceof HTMLPurifier_Token_Comment) { + // textify comments in script tags when they are allowed + if ($textify_comments !== false) { + $data = $token->data; + $token = new HTMLPurifier_Token_Text($data); + } elseif ($trusted || $check_comments) { + // always cleanup comments + $trailing_hyphen = false; + if ($e) { + // perform check whether or not there's a trailing hyphen + if (substr($token->data, -1) == '-') { + $trailing_hyphen = true; + } + } + $token->data = rtrim($token->data, '-'); + $found_double_hyphen = false; + while (strpos($token->data, '--') !== false) { + $found_double_hyphen = true; + $token->data = str_replace('--', '-', $token->data); + } + if ($trusted || !empty($comment_lookup[trim($token->data)]) || + ($comment_regexp !== null && preg_match($comment_regexp, trim($token->data)))) { + // OK good + if ($e) { + if ($trailing_hyphen) { + $e->send( + E_NOTICE, + 'Strategy_RemoveForeignElements: Trailing hyphen in comment removed' + ); + } + if ($found_double_hyphen) { + $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Hyphens in comment collapsed'); + } + } + } else { + if ($e) { + $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Comment removed'); + } + continue; + } + } else { + // strip comments + if ($e) { + $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Comment removed'); + } + continue; + } + } elseif ($token instanceof HTMLPurifier_Token_Text) { + } else { + continue; + } + $result[] = $token; + } + if ($remove_until && $e) { + // we removed tokens until the end, throw error + $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Token removed to end', $remove_until); + } + $context->destroy('CurrentToken'); + return $result; + } +} + + + + + +/** + * Validate all attributes in the tokens. + */ + +class HTMLPurifier_Strategy_ValidateAttributes extends HTMLPurifier_Strategy +{ + + /** + * @param HTMLPurifier_Token[] $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return HTMLPurifier_Token[] + */ + public function execute($tokens, $config, $context) + { + // setup validator + $validator = new HTMLPurifier_AttrValidator(); + + $token = false; + $context->register('CurrentToken', $token); + + foreach ($tokens as $key => $token) { + + // only process tokens that have attributes, + // namely start and empty tags + if (!$token instanceof HTMLPurifier_Token_Start && !$token instanceof HTMLPurifier_Token_Empty) { + continue; + } + + // skip tokens that are armored + if (!empty($token->armor['ValidateAttributes'])) { + continue; + } + + // note that we have no facilities here for removing tokens + $validator->validateToken($token, $config, $context); + } + $context->destroy('CurrentToken'); + return $tokens; + } +} + + + + + +/** + * Transforms FONT tags to the proper form (SPAN with CSS styling) + * + * This transformation takes the three proprietary attributes of FONT and + * transforms them into their corresponding CSS attributes. These are color, + * face, and size. + * + * @note Size is an interesting case because it doesn't map cleanly to CSS. + * Thanks to + * http://style.cleverchimp.com/font_size_intervals/altintervals.html + * for reasonable mappings. + * @warning This doesn't work completely correctly; specifically, this + * TagTransform operates before well-formedness is enforced, so + * the "active formatting elements" algorithm doesn't get applied. + */ +class HTMLPurifier_TagTransform_Font extends HTMLPurifier_TagTransform +{ + /** + * @type string + */ + public $transform_to = 'span'; + + /** + * @type array + */ + protected $_size_lookup = array( + '0' => 'xx-small', + '1' => 'xx-small', + '2' => 'small', + '3' => 'medium', + '4' => 'large', + '5' => 'x-large', + '6' => 'xx-large', + '7' => '300%', + '-1' => 'smaller', + '-2' => '60%', + '+1' => 'larger', + '+2' => '150%', + '+3' => '200%', + '+4' => '300%' + ); + + /** + * @param HTMLPurifier_Token_Tag $tag + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return HTMLPurifier_Token_End|string + */ + public function transform($tag, $config, $context) + { + if ($tag instanceof HTMLPurifier_Token_End) { + $new_tag = clone $tag; + $new_tag->name = $this->transform_to; + return $new_tag; + } + + $attr = $tag->attr; + $prepend_style = ''; + + // handle color transform + if (isset($attr['color'])) { + $prepend_style .= 'color:' . $attr['color'] . ';'; + unset($attr['color']); + } + + // handle face transform + if (isset($attr['face'])) { + $prepend_style .= 'font-family:' . $attr['face'] . ';'; + unset($attr['face']); + } + + // handle size transform + if (isset($attr['size'])) { + // normalize large numbers + if ($attr['size'] !== '') { + if ($attr['size']{0} == '+' || $attr['size']{0} == '-') { + $size = (int)$attr['size']; + if ($size < -2) { + $attr['size'] = '-2'; + } + if ($size > 4) { + $attr['size'] = '+4'; + } + } else { + $size = (int)$attr['size']; + if ($size > 7) { + $attr['size'] = '7'; + } + } + } + if (isset($this->_size_lookup[$attr['size']])) { + $prepend_style .= 'font-size:' . + $this->_size_lookup[$attr['size']] . ';'; + } + unset($attr['size']); + } + + if ($prepend_style) { + $attr['style'] = isset($attr['style']) ? + $prepend_style . $attr['style'] : + $prepend_style; + } + + $new_tag = clone $tag; + $new_tag->name = $this->transform_to; + $new_tag->attr = $attr; + + return $new_tag; + } +} + + + + + +/** + * Simple transformation, just change tag name to something else, + * and possibly add some styling. This will cover most of the deprecated + * tag cases. + */ +class HTMLPurifier_TagTransform_Simple extends HTMLPurifier_TagTransform +{ + /** + * @type string + */ + protected $style; + + /** + * @param string $transform_to Tag name to transform to. + * @param string $style CSS style to add to the tag + */ + public function __construct($transform_to, $style = null) + { + $this->transform_to = $transform_to; + $this->style = $style; + } + + /** + * @param HTMLPurifier_Token_Tag $tag + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function transform($tag, $config, $context) + { + $new_tag = clone $tag; + $new_tag->name = $this->transform_to; + if (!is_null($this->style) && + ($new_tag instanceof HTMLPurifier_Token_Start || $new_tag instanceof HTMLPurifier_Token_Empty) + ) { + $this->prependCSS($new_tag->attr, $this->style); + } + return $new_tag; + } +} + + + + + +/** + * Concrete comment token class. Generally will be ignored. + */ +class HTMLPurifier_Token_Comment extends HTMLPurifier_Token +{ + /** + * Character data within comment. + * @type string + */ + public $data; + + /** + * @type bool + */ + public $is_whitespace = true; + + /** + * Transparent constructor. + * + * @param string $data String comment data. + * @param int $line + * @param int $col + */ + public function __construct($data, $line = null, $col = null) + { + $this->data = $data; + $this->line = $line; + $this->col = $col; + } + + public function toNode() { + return new HTMLPurifier_Node_Comment($this->data, $this->line, $this->col); + } +} + + + + + +/** + * Abstract class of a tag token (start, end or empty), and its behavior. + */ +abstract class HTMLPurifier_Token_Tag extends HTMLPurifier_Token +{ + /** + * Static bool marker that indicates the class is a tag. + * + * This allows us to check objects with !empty($obj->is_tag) + * without having to use a function call is_a(). + * @type bool + */ + public $is_tag = true; + + /** + * The lower-case name of the tag, like 'a', 'b' or 'blockquote'. + * + * @note Strictly speaking, XML tags are case sensitive, so we shouldn't + * be lower-casing them, but these tokens cater to HTML tags, which are + * insensitive. + * @type string + */ + public $name; + + /** + * Associative array of the tag's attributes. + * @type array + */ + public $attr = array(); + + /** + * Non-overloaded constructor, which lower-cases passed tag name. + * + * @param string $name String name. + * @param array $attr Associative array of attributes. + * @param int $line + * @param int $col + * @param array $armor + */ + public function __construct($name, $attr = array(), $line = null, $col = null, $armor = array()) + { + $this->name = ctype_lower($name) ? $name : strtolower($name); + foreach ($attr as $key => $value) { + // normalization only necessary when key is not lowercase + if (!ctype_lower($key)) { + $new_key = strtolower($key); + if (!isset($attr[$new_key])) { + $attr[$new_key] = $attr[$key]; + } + if ($new_key !== $key) { + unset($attr[$key]); + } + } + } + $this->attr = $attr; + $this->line = $line; + $this->col = $col; + $this->armor = $armor; + } + + public function toNode() { + return new HTMLPurifier_Node_Element($this->name, $this->attr, $this->line, $this->col, $this->armor); + } +} + + + + + +/** + * Concrete empty token class. + */ +class HTMLPurifier_Token_Empty extends HTMLPurifier_Token_Tag +{ + public function toNode() { + $n = parent::toNode(); + $n->empty = true; + return $n; + } +} + + + + + +/** + * Concrete end token class. + * + * @warning This class accepts attributes even though end tags cannot. This + * is for optimization reasons, as under normal circumstances, the Lexers + * do not pass attributes. + */ +class HTMLPurifier_Token_End extends HTMLPurifier_Token_Tag +{ + /** + * Token that started this node. + * Added by MakeWellFormed. Please do not edit this! + * @type HTMLPurifier_Token + */ + public $start; + + public function toNode() { + throw new Exception("HTMLPurifier_Token_End->toNode not supported!"); + } +} + + + + + +/** + * Concrete start token class. + */ +class HTMLPurifier_Token_Start extends HTMLPurifier_Token_Tag +{ +} + + + + + +/** + * Concrete text token class. + * + * Text tokens comprise of regular parsed character data (PCDATA) and raw + * character data (from the CDATA sections). Internally, their + * data is parsed with all entities expanded. Surprisingly, the text token + * does have a "tag name" called #PCDATA, which is how the DTD represents it + * in permissible child nodes. + */ +class HTMLPurifier_Token_Text extends HTMLPurifier_Token +{ + + /** + * @type string + */ + public $name = '#PCDATA'; + /**< PCDATA tag name compatible with DTD. */ + + /** + * @type string + */ + public $data; + /**< Parsed character data of text. */ + + /** + * @type bool + */ + public $is_whitespace; + + /**< Bool indicating if node is whitespace. */ + + /** + * Constructor, accepts data and determines if it is whitespace. + * @param string $data String parsed character data. + * @param int $line + * @param int $col + */ + public function __construct($data, $line = null, $col = null) + { + $this->data = $data; + $this->is_whitespace = ctype_space($data); + $this->line = $line; + $this->col = $col; + } + + public function toNode() { + return new HTMLPurifier_Node_Text($this->data, $this->is_whitespace, $this->line, $this->col); + } +} + + + + + +class HTMLPurifier_URIFilter_DisableExternal extends HTMLPurifier_URIFilter +{ + /** + * @type string + */ + public $name = 'DisableExternal'; + + /** + * @type array + */ + protected $ourHostParts = false; + + /** + * @param HTMLPurifier_Config $config + * @return void + */ + public function prepare($config) + { + $our_host = $config->getDefinition('URI')->host; + if ($our_host !== null) { + $this->ourHostParts = array_reverse(explode('.', $our_host)); + } + } + + /** + * @param HTMLPurifier_URI $uri Reference + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + if (is_null($uri->host)) { + return true; + } + if ($this->ourHostParts === false) { + return false; + } + $host_parts = array_reverse(explode('.', $uri->host)); + foreach ($this->ourHostParts as $i => $x) { + if (!isset($host_parts[$i])) { + return false; + } + if ($host_parts[$i] != $this->ourHostParts[$i]) { + return false; + } + } + return true; + } +} + + + + + +class HTMLPurifier_URIFilter_DisableExternalResources extends HTMLPurifier_URIFilter_DisableExternal +{ + /** + * @type string + */ + public $name = 'DisableExternalResources'; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + if (!$context->get('EmbeddedURI', true)) { + return true; + } + return parent::filter($uri, $config, $context); + } +} + + + + + +class HTMLPurifier_URIFilter_DisableResources extends HTMLPurifier_URIFilter +{ + /** + * @type string + */ + public $name = 'DisableResources'; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + return !$context->get('EmbeddedURI', true); + } +} + + + + + +// It's not clear to me whether or not Punycode means that hostnames +// do not have canonical forms anymore. As far as I can tell, it's +// not a problem (punycoding should be identity when no Unicode +// points are involved), but I'm not 100% sure +class HTMLPurifier_URIFilter_HostBlacklist extends HTMLPurifier_URIFilter +{ + /** + * @type string + */ + public $name = 'HostBlacklist'; + + /** + * @type array + */ + protected $blacklist = array(); + + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function prepare($config) + { + $this->blacklist = $config->get('URI.HostBlacklist'); + return true; + } + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + foreach ($this->blacklist as $blacklisted_host_fragment) { + if (strpos($uri->host, $blacklisted_host_fragment) !== false) { + return false; + } + } + return true; + } +} + + + + + +// does not support network paths + +class HTMLPurifier_URIFilter_MakeAbsolute extends HTMLPurifier_URIFilter +{ + /** + * @type string + */ + public $name = 'MakeAbsolute'; + + /** + * @type + */ + protected $base; + + /** + * @type array + */ + protected $basePathStack = array(); + + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function prepare($config) + { + $def = $config->getDefinition('URI'); + $this->base = $def->base; + if (is_null($this->base)) { + trigger_error( + 'URI.MakeAbsolute is being ignored due to lack of ' . + 'value for URI.Base configuration', + E_USER_WARNING + ); + return false; + } + $this->base->fragment = null; // fragment is invalid for base URI + $stack = explode('/', $this->base->path); + array_pop($stack); // discard last segment + $stack = $this->_collapseStack($stack); // do pre-parsing + $this->basePathStack = $stack; + return true; + } + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + if (is_null($this->base)) { + return true; + } // abort early + if ($uri->path === '' && is_null($uri->scheme) && + is_null($uri->host) && is_null($uri->query) && is_null($uri->fragment)) { + // reference to current document + $uri = clone $this->base; + return true; + } + if (!is_null($uri->scheme)) { + // absolute URI already: don't change + if (!is_null($uri->host)) { + return true; + } + $scheme_obj = $uri->getSchemeObj($config, $context); + if (!$scheme_obj) { + // scheme not recognized + return false; + } + if (!$scheme_obj->hierarchical) { + // non-hierarchal URI with explicit scheme, don't change + return true; + } + // special case: had a scheme but always is hierarchical and had no authority + } + if (!is_null($uri->host)) { + // network path, don't bother + return true; + } + if ($uri->path === '') { + $uri->path = $this->base->path; + } elseif ($uri->path[0] !== '/') { + // relative path, needs more complicated processing + $stack = explode('/', $uri->path); + $new_stack = array_merge($this->basePathStack, $stack); + if ($new_stack[0] !== '' && !is_null($this->base->host)) { + array_unshift($new_stack, ''); + } + $new_stack = $this->_collapseStack($new_stack); + $uri->path = implode('/', $new_stack); + } else { + // absolute path, but still we should collapse + $uri->path = implode('/', $this->_collapseStack(explode('/', $uri->path))); + } + // re-combine + $uri->scheme = $this->base->scheme; + if (is_null($uri->userinfo)) { + $uri->userinfo = $this->base->userinfo; + } + if (is_null($uri->host)) { + $uri->host = $this->base->host; + } + if (is_null($uri->port)) { + $uri->port = $this->base->port; + } + return true; + } + + /** + * Resolve dots and double-dots in a path stack + * @param array $stack + * @return array + */ + private function _collapseStack($stack) + { + $result = array(); + $is_folder = false; + for ($i = 0; isset($stack[$i]); $i++) { + $is_folder = false; + // absorb an internally duplicated slash + if ($stack[$i] == '' && $i && isset($stack[$i + 1])) { + continue; + } + if ($stack[$i] == '..') { + if (!empty($result)) { + $segment = array_pop($result); + if ($segment === '' && empty($result)) { + // error case: attempted to back out too far: + // restore the leading slash + $result[] = ''; + } elseif ($segment === '..') { + $result[] = '..'; // cannot remove .. with .. + } + } else { + // relative path, preserve the double-dots + $result[] = '..'; + } + $is_folder = true; + continue; + } + if ($stack[$i] == '.') { + // silently absorb + $is_folder = true; + continue; + } + $result[] = $stack[$i]; + } + if ($is_folder) { + $result[] = ''; + } + return $result; + } +} + + + + + +class HTMLPurifier_URIFilter_Munge extends HTMLPurifier_URIFilter +{ + /** + * @type string + */ + public $name = 'Munge'; + + /** + * @type bool + */ + public $post = true; + + /** + * @type string + */ + private $target; + + /** + * @type HTMLPurifier_URIParser + */ + private $parser; + + /** + * @type bool + */ + private $doEmbed; + + /** + * @type string + */ + private $secretKey; + + /** + * @type array + */ + protected $replace = array(); + + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function prepare($config) + { + $this->target = $config->get('URI.' . $this->name); + $this->parser = new HTMLPurifier_URIParser(); + $this->doEmbed = $config->get('URI.MungeResources'); + $this->secretKey = $config->get('URI.MungeSecretKey'); + if ($this->secretKey && !function_exists('hash_hmac')) { + throw new Exception("Cannot use %URI.MungeSecretKey without hash_hmac support."); + } + return true; + } + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + if ($context->get('EmbeddedURI', true) && !$this->doEmbed) { + return true; + } + + $scheme_obj = $uri->getSchemeObj($config, $context); + if (!$scheme_obj) { + return true; + } // ignore unknown schemes, maybe another postfilter did it + if (!$scheme_obj->browsable) { + return true; + } // ignore non-browseable schemes, since we can't munge those in a reasonable way + if ($uri->isBenign($config, $context)) { + return true; + } // don't redirect if a benign URL + + $this->makeReplace($uri, $config, $context); + $this->replace = array_map('rawurlencode', $this->replace); + + $new_uri = strtr($this->target, $this->replace); + $new_uri = $this->parser->parse($new_uri); + // don't redirect if the target host is the same as the + // starting host + if ($uri->host === $new_uri->host) { + return true; + } + $uri = $new_uri; // overwrite + return true; + } + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + */ + protected function makeReplace($uri, $config, $context) + { + $string = $uri->toString(); + // always available + $this->replace['%s'] = $string; + $this->replace['%r'] = $context->get('EmbeddedURI', true); + $token = $context->get('CurrentToken', true); + $this->replace['%n'] = $token ? $token->name : null; + $this->replace['%m'] = $context->get('CurrentAttr', true); + $this->replace['%p'] = $context->get('CurrentCSSProperty', true); + // not always available + if ($this->secretKey) { + $this->replace['%t'] = hash_hmac("sha256", $string, $this->secretKey); + } + } +} + + + + + +/** + * Implements safety checks for safe iframes. + * + * @warning This filter is *critical* for ensuring that %HTML.SafeIframe + * works safely. + */ +class HTMLPurifier_URIFilter_SafeIframe extends HTMLPurifier_URIFilter +{ + /** + * @type string + */ + public $name = 'SafeIframe'; + + /** + * @type bool + */ + public $always_load = true; + + /** + * @type string + */ + protected $regexp = null; + + // XXX: The not so good bit about how this is all set up now is we + // can't check HTML.SafeIframe in the 'prepare' step: we have to + // defer till the actual filtering. + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function prepare($config) + { + $this->regexp = $config->get('URI.SafeIframeRegexp'); + return true; + } + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + // check if filter not applicable + if (!$config->get('HTML.SafeIframe')) { + return true; + } + // check if the filter should actually trigger + if (!$context->get('EmbeddedURI', true)) { + return true; + } + $token = $context->get('CurrentToken', true); + if (!($token && $token->name == 'iframe')) { + return true; + } + // check if we actually have some whitelists enabled + if ($this->regexp === null) { + return false; + } + // actually check the whitelists + return preg_match($this->regexp, $uri->toString()); + } +} + + + + + +/** + * Implements data: URI for base64 encoded images supported by GD. + */ +class HTMLPurifier_URIScheme_data extends HTMLPurifier_URIScheme +{ + /** + * @type bool + */ + public $browsable = true; + + /** + * @type array + */ + public $allowed_types = array( + // you better write validation code for other types if you + // decide to allow them + 'image/jpeg' => true, + 'image/gif' => true, + 'image/png' => true, + ); + // this is actually irrelevant since we only write out the path + // component + /** + * @type bool + */ + public $may_omit_host = true; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + $result = explode(',', $uri->path, 2); + $is_base64 = false; + $charset = null; + $content_type = null; + if (count($result) == 2) { + list($metadata, $data) = $result; + // do some legwork on the metadata + $metas = explode(';', $metadata); + while (!empty($metas)) { + $cur = array_shift($metas); + if ($cur == 'base64') { + $is_base64 = true; + break; + } + if (substr($cur, 0, 8) == 'charset=') { + // doesn't match if there are arbitrary spaces, but + // whatever dude + if ($charset !== null) { + continue; + } // garbage + $charset = substr($cur, 8); // not used + } else { + if ($content_type !== null) { + continue; + } // garbage + $content_type = $cur; + } + } + } else { + $data = $result[0]; + } + if ($content_type !== null && empty($this->allowed_types[$content_type])) { + return false; + } + if ($charset !== null) { + // error; we don't allow plaintext stuff + $charset = null; + } + $data = rawurldecode($data); + if ($is_base64) { + $raw_data = base64_decode($data); + } else { + $raw_data = $data; + } + // XXX probably want to refactor this into a general mechanism + // for filtering arbitrary content types + $file = tempnam("/tmp", ""); + file_put_contents($file, $raw_data); + if (function_exists('exif_imagetype')) { + $image_code = exif_imagetype($file); + unlink($file); + } elseif (function_exists('getimagesize')) { + set_error_handler(array($this, 'muteErrorHandler')); + $info = getimagesize($file); + restore_error_handler(); + unlink($file); + if ($info == false) { + return false; + } + $image_code = $info[2]; + } else { + trigger_error("could not find exif_imagetype or getimagesize functions", E_USER_ERROR); + } + $real_content_type = image_type_to_mime_type($image_code); + if ($real_content_type != $content_type) { + // we're nice guys; if the content type is something else we + // support, change it over + if (empty($this->allowed_types[$real_content_type])) { + return false; + } + $content_type = $real_content_type; + } + // ok, it's kosher, rewrite what we need + $uri->userinfo = null; + $uri->host = null; + $uri->port = null; + $uri->fragment = null; + $uri->query = null; + $uri->path = "$content_type;base64," . base64_encode($raw_data); + return true; + } + + /** + * @param int $errno + * @param string $errstr + */ + public function muteErrorHandler($errno, $errstr) + { + } +} + + + +/** + * Validates file as defined by RFC 1630 and RFC 1738. + */ +class HTMLPurifier_URIScheme_file extends HTMLPurifier_URIScheme +{ + /** + * Generally file:// URLs are not accessible from most + * machines, so placing them as an img src is incorrect. + * @type bool + */ + public $browsable = false; + + /** + * Basically the *only* URI scheme for which this is true, since + * accessing files on the local machine is very common. In fact, + * browsers on some operating systems don't understand the + * authority, though I hear it is used on Windows to refer to + * network shares. + * @type bool + */ + public $may_omit_host = true; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + // Authentication method is not supported + $uri->userinfo = null; + // file:// makes no provisions for accessing the resource + $uri->port = null; + // While it seems to work on Firefox, the querystring has + // no possible effect and is thus stripped. + $uri->query = null; + return true; + } +} + + + + + +/** + * Validates ftp (File Transfer Protocol) URIs as defined by generic RFC 1738. + */ +class HTMLPurifier_URIScheme_ftp extends HTMLPurifier_URIScheme +{ + /** + * @type int + */ + public $default_port = 21; + + /** + * @type bool + */ + public $browsable = true; // usually + + /** + * @type bool + */ + public $hierarchical = true; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + $uri->query = null; + + // typecode check + $semicolon_pos = strrpos($uri->path, ';'); // reverse + if ($semicolon_pos !== false) { + $type = substr($uri->path, $semicolon_pos + 1); // no semicolon + $uri->path = substr($uri->path, 0, $semicolon_pos); + $type_ret = ''; + if (strpos($type, '=') !== false) { + // figure out whether or not the declaration is correct + list($key, $typecode) = explode('=', $type, 2); + if ($key !== 'type') { + // invalid key, tack it back on encoded + $uri->path .= '%3B' . $type; + } elseif ($typecode === 'a' || $typecode === 'i' || $typecode === 'd') { + $type_ret = ";type=$typecode"; + } + } else { + $uri->path .= '%3B' . $type; + } + $uri->path = str_replace(';', '%3B', $uri->path); + $uri->path .= $type_ret; + } + return true; + } +} + + + + + +/** + * Validates http (HyperText Transfer Protocol) as defined by RFC 2616 + */ +class HTMLPurifier_URIScheme_http extends HTMLPurifier_URIScheme +{ + /** + * @type int + */ + public $default_port = 80; + + /** + * @type bool + */ + public $browsable = true; + + /** + * @type bool + */ + public $hierarchical = true; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + $uri->userinfo = null; + return true; + } +} + + + + + +/** + * Validates https (Secure HTTP) according to http scheme. + */ +class HTMLPurifier_URIScheme_https extends HTMLPurifier_URIScheme_http +{ + /** + * @type int + */ + public $default_port = 443; + /** + * @type bool + */ + public $secure = true; +} + + + + + +// VERY RELAXED! Shouldn't cause problems, not even Firefox checks if the +// email is valid, but be careful! + +/** + * Validates mailto (for E-mail) according to RFC 2368 + * @todo Validate the email address + * @todo Filter allowed query parameters + */ + +class HTMLPurifier_URIScheme_mailto extends HTMLPurifier_URIScheme +{ + /** + * @type bool + */ + public $browsable = false; + + /** + * @type bool + */ + public $may_omit_host = true; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + $uri->userinfo = null; + $uri->host = null; + $uri->port = null; + // we need to validate path against RFC 2368's addr-spec + return true; + } +} + + + + + +/** + * Validates news (Usenet) as defined by generic RFC 1738 + */ +class HTMLPurifier_URIScheme_news extends HTMLPurifier_URIScheme +{ + /** + * @type bool + */ + public $browsable = false; + + /** + * @type bool + */ + public $may_omit_host = true; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + $uri->userinfo = null; + $uri->host = null; + $uri->port = null; + $uri->query = null; + // typecode check needed on path + return true; + } +} + + + + + +/** + * Validates nntp (Network News Transfer Protocol) as defined by generic RFC 1738 + */ +class HTMLPurifier_URIScheme_nntp extends HTMLPurifier_URIScheme +{ + /** + * @type int + */ + public $default_port = 119; + + /** + * @type bool + */ + public $browsable = false; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + $uri->userinfo = null; + $uri->query = null; + return true; + } +} + + + + + +/** + * Performs safe variable parsing based on types which can be used by + * users. This may not be able to represent all possible data inputs, + * however. + */ +class HTMLPurifier_VarParser_Flexible extends HTMLPurifier_VarParser +{ + /** + * @param mixed $var + * @param int $type + * @param bool $allow_null + * @return array|bool|float|int|mixed|null|string + * @throws HTMLPurifier_VarParserException + */ + protected function parseImplementation($var, $type, $allow_null) + { + if ($allow_null && $var === null) { + return null; + } + switch ($type) { + // Note: if code "breaks" from the switch, it triggers a generic + // exception to be thrown. Specific errors can be specifically + // done here. + case self::MIXED: + case self::ISTRING: + case self::STRING: + case self::TEXT: + case self::ITEXT: + return $var; + case self::INT: + if (is_string($var) && ctype_digit($var)) { + $var = (int)$var; + } + return $var; + case self::FLOAT: + if ((is_string($var) && is_numeric($var)) || is_int($var)) { + $var = (float)$var; + } + return $var; + case self::BOOL: + if (is_int($var) && ($var === 0 || $var === 1)) { + $var = (bool)$var; + } elseif (is_string($var)) { + if ($var == 'on' || $var == 'true' || $var == '1') { + $var = true; + } elseif ($var == 'off' || $var == 'false' || $var == '0') { + $var = false; + } else { + throw new HTMLPurifier_VarParserException("Unrecognized value '$var' for $type"); + } + } + return $var; + case self::ALIST: + case self::HASH: + case self::LOOKUP: + if (is_string($var)) { + // special case: technically, this is an array with + // a single empty string item, but having an empty + // array is more intuitive + if ($var == '') { + return array(); + } + if (strpos($var, "\n") === false && strpos($var, "\r") === false) { + // simplistic string to array method that only works + // for simple lists of tag names or alphanumeric characters + $var = explode(',', $var); + } else { + $var = preg_split('/(,|[\n\r]+)/', $var); + } + // remove spaces + foreach ($var as $i => $j) { + $var[$i] = trim($j); + } + if ($type === self::HASH) { + // key:value,key2:value2 + $nvar = array(); + foreach ($var as $keypair) { + $c = explode(':', $keypair, 2); + if (!isset($c[1])) { + continue; + } + $nvar[trim($c[0])] = trim($c[1]); + } + $var = $nvar; + } + } + if (!is_array($var)) { + break; + } + $keys = array_keys($var); + if ($keys === array_keys($keys)) { + if ($type == self::ALIST) { + return $var; + } elseif ($type == self::LOOKUP) { + $new = array(); + foreach ($var as $key) { + $new[$key] = true; + } + return $new; + } else { + break; + } + } + if ($type === self::ALIST) { + trigger_error("Array list did not have consecutive integer indexes", E_USER_WARNING); + return array_values($var); + } + if ($type === self::LOOKUP) { + foreach ($var as $key => $value) { + if ($value !== true) { + trigger_error( + "Lookup array has non-true value at key '$key'; " . + "maybe your input array was not indexed numerically", + E_USER_WARNING + ); + } + $var[$key] = true; + } + } + return $var; + default: + $this->errorInconsistent(__CLASS__, $type); + } + $this->errorGeneric($var, $type); + } +} + + + + + +/** + * This variable parser uses PHP's internal code engine. Because it does + * this, it can represent all inputs; however, it is dangerous and cannot + * be used by users. + */ +class HTMLPurifier_VarParser_Native extends HTMLPurifier_VarParser +{ + + /** + * @param mixed $var + * @param int $type + * @param bool $allow_null + * @return null|string + */ + protected function parseImplementation($var, $type, $allow_null) + { + return $this->evalExpression($var); + } + + /** + * @param string $expr + * @return mixed + * @throws HTMLPurifier_VarParserException + */ + protected function evalExpression($expr) + { + $var = null; + $result = eval("\$var = $expr;"); + if ($result === false) { + throw new HTMLPurifier_VarParserException("Fatal error in evaluated code"); + } + return $var; + } +} + diff --git a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php index 92bd63095be..c05668a7060 100644 --- a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php +++ b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php @@ -1,44 +1,44 @@ -directives as $d) { - $schema->add( - $d->id->key, - $d->default, - $d->type, - $d->typeAllowsNull - ); - if ($d->allowed !== null) { - $schema->addAllowedValues( - $d->id->key, - $d->allowed - ); - } - foreach ($d->aliases as $alias) { - $schema->addAlias( - $alias->key, - $d->id->key - ); - } - if ($d->valueAliases !== null) { - $schema->addValueAliases( - $d->id->key, - $d->valueAliases - ); - } - } - $schema->postProcess(); - return $schema; - } - -} - -// vim: et sw=4 sts=4 +directives as $d) { + $schema->add( + $d->id->key, + $d->default, + $d->type, + $d->typeAllowsNull + ); + if ($d->allowed !== null) { + $schema->addAllowedValues( + $d->id->key, + $d->allowed + ); + } + foreach ($d->aliases as $alias) { + $schema->addAlias( + $alias->key, + $d->id->key + ); + } + if ($d->valueAliases !== null) { + $schema->addValueAliases( + $d->id->key, + $d->valueAliases + ); + } + } + $schema->postProcess(); + return $schema; + } + +} + +// vim: et sw=4 sts=4 diff --git a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Builder/Xml.php b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Builder/Xml.php index 7924b6adb10..244561a3726 100644 --- a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Builder/Xml.php +++ b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Builder/Xml.php @@ -1,106 +1,106 @@ -startElement('div'); - - $purifier = HTMLPurifier::getInstance(); - $html = $purifier->purify($html); - $this->writeAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); - $this->writeRaw($html); - - $this->endElement(); // div - } - - protected function export($var) { - if ($var === array()) return 'array()'; - return var_export($var, true); - } - - public function build($interchange) { - // global access, only use as last resort - $this->interchange = $interchange; - - $this->setIndent(true); - $this->startDocument('1.0', 'UTF-8'); - $this->startElement('configdoc'); - $this->writeElement('title', $interchange->name); - - foreach ($interchange->directives as $directive) { - $this->buildDirective($directive); - } - - if ($this->namespace) $this->endElement(); // namespace - - $this->endElement(); // configdoc - $this->flush(); - } - - public function buildDirective($directive) { - - // Kludge, although I suppose having a notion of a "root namespace" - // certainly makes things look nicer when documentation is built. - // Depends on things being sorted. - if (!$this->namespace || $this->namespace !== $directive->id->getRootNamespace()) { - if ($this->namespace) $this->endElement(); // namespace - $this->namespace = $directive->id->getRootNamespace(); - $this->startElement('namespace'); - $this->writeAttribute('id', $this->namespace); - $this->writeElement('name', $this->namespace); - } - - $this->startElement('directive'); - $this->writeAttribute('id', $directive->id->toString()); - - $this->writeElement('name', $directive->id->getDirective()); - - $this->startElement('aliases'); - foreach ($directive->aliases as $alias) $this->writeElement('alias', $alias->toString()); - $this->endElement(); // aliases - - $this->startElement('constraints'); - if ($directive->version) $this->writeElement('version', $directive->version); - $this->startElement('type'); - if ($directive->typeAllowsNull) $this->writeAttribute('allow-null', 'yes'); - $this->text($directive->type); - $this->endElement(); // type - if ($directive->allowed) { - $this->startElement('allowed'); - foreach ($directive->allowed as $value => $x) $this->writeElement('value', $value); - $this->endElement(); // allowed - } - $this->writeElement('default', $this->export($directive->default)); - $this->writeAttribute('xml:space', 'preserve'); - if ($directive->external) { - $this->startElement('external'); - foreach ($directive->external as $project) $this->writeElement('project', $project); - $this->endElement(); - } - $this->endElement(); // constraints - - if ($directive->deprecatedVersion) { - $this->startElement('deprecated'); - $this->writeElement('version', $directive->deprecatedVersion); - $this->writeElement('use', $directive->deprecatedUse->toString()); - $this->endElement(); // deprecated - } - - $this->startElement('description'); - $this->writeHTMLDiv($directive->description); - $this->endElement(); // description - - $this->endElement(); // directive - } - -} - -// vim: et sw=4 sts=4 +startElement('div'); + + $purifier = HTMLPurifier::getInstance(); + $html = $purifier->purify($html); + $this->writeAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); + $this->writeRaw($html); + + $this->endElement(); // div + } + + protected function export($var) { + if ($var === array()) return 'array()'; + return var_export($var, true); + } + + public function build($interchange) { + // global access, only use as last resort + $this->interchange = $interchange; + + $this->setIndent(true); + $this->startDocument('1.0', 'UTF-8'); + $this->startElement('configdoc'); + $this->writeElement('title', $interchange->name); + + foreach ($interchange->directives as $directive) { + $this->buildDirective($directive); + } + + if ($this->namespace) $this->endElement(); // namespace + + $this->endElement(); // configdoc + $this->flush(); + } + + public function buildDirective($directive) { + + // Kludge, although I suppose having a notion of a "root namespace" + // certainly makes things look nicer when documentation is built. + // Depends on things being sorted. + if (!$this->namespace || $this->namespace !== $directive->id->getRootNamespace()) { + if ($this->namespace) $this->endElement(); // namespace + $this->namespace = $directive->id->getRootNamespace(); + $this->startElement('namespace'); + $this->writeAttribute('id', $this->namespace); + $this->writeElement('name', $this->namespace); + } + + $this->startElement('directive'); + $this->writeAttribute('id', $directive->id->toString()); + + $this->writeElement('name', $directive->id->getDirective()); + + $this->startElement('aliases'); + foreach ($directive->aliases as $alias) $this->writeElement('alias', $alias->toString()); + $this->endElement(); // aliases + + $this->startElement('constraints'); + if ($directive->version) $this->writeElement('version', $directive->version); + $this->startElement('type'); + if ($directive->typeAllowsNull) $this->writeAttribute('allow-null', 'yes'); + $this->text($directive->type); + $this->endElement(); // type + if ($directive->allowed) { + $this->startElement('allowed'); + foreach ($directive->allowed as $value => $x) $this->writeElement('value', $value); + $this->endElement(); // allowed + } + $this->writeElement('default', $this->export($directive->default)); + $this->writeAttribute('xml:space', 'preserve'); + if ($directive->external) { + $this->startElement('external'); + foreach ($directive->external as $project) $this->writeElement('project', $project); + $this->endElement(); + } + $this->endElement(); // constraints + + if ($directive->deprecatedVersion) { + $this->startElement('deprecated'); + $this->writeElement('version', $directive->deprecatedVersion); + $this->writeElement('use', $directive->deprecatedUse->toString()); + $this->endElement(); // deprecated + } + + $this->startElement('description'); + $this->writeHTMLDiv($directive->description); + $this->endElement(); // description + + $this->endElement(); // directive + } + +} + +// vim: et sw=4 sts=4 diff --git a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Exception.php b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Exception.php index 1abdcfc0652..2671516c588 100644 --- a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Exception.php +++ b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Exception.php @@ -1,11 +1,11 @@ - array(directive info) - * @type HTMLPurifier_ConfigSchema_Interchange_Directive[] - */ - public $directives = array(); - - /** - * Adds a directive array to $directives - * @param HTMLPurifier_ConfigSchema_Interchange_Directive $directive - * @throws HTMLPurifier_ConfigSchema_Exception - */ - public function addDirective($directive) - { - if (isset($this->directives[$i = $directive->id->toString()])) { - throw new HTMLPurifier_ConfigSchema_Exception("Cannot redefine directive '$i'"); - } - $this->directives[$i] = $directive; - } - - /** - * Convenience function to perform standard validation. Throws exception - * on failed validation. - */ - public function validate() - { - $validator = new HTMLPurifier_ConfigSchema_Validator(); - return $validator->validate($this); - } -} - -// vim: et sw=4 sts=4 + array(directive info) + * @type HTMLPurifier_ConfigSchema_Interchange_Directive[] + */ + public $directives = array(); + + /** + * Adds a directive array to $directives + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $directive + * @throws HTMLPurifier_ConfigSchema_Exception + */ + public function addDirective($directive) + { + if (isset($this->directives[$i = $directive->id->toString()])) { + throw new HTMLPurifier_ConfigSchema_Exception("Cannot redefine directive '$i'"); + } + $this->directives[$i] = $directive; + } + + /** + * Convenience function to perform standard validation. Throws exception + * on failed validation. + */ + public function validate() + { + $validator = new HTMLPurifier_ConfigSchema_Validator(); + return $validator->validate($this); + } +} + +// vim: et sw=4 sts=4 diff --git a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Interchange/Directive.php b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Interchange/Directive.php index 2dced1f5456..ac8be0d9707 100644 --- a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Interchange/Directive.php +++ b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Interchange/Directive.php @@ -1,77 +1,77 @@ - true). - * Null if all values are allowed. - */ - public $allowed; - - /** - * List of aliases for the directive, - * e.g. array(new HTMLPurifier_ConfigSchema_Interchange_Id('Ns', 'Dir'))). - */ - public $aliases = array(); - - /** - * Hash of value aliases, e.g. array('alt' => 'real'). Null if value - * aliasing is disabled (necessary for non-scalar types). - */ - public $valueAliases; - - /** - * Version of HTML Purifier the directive was introduced, e.g. '1.3.1'. - * Null if the directive has always existed. - */ - public $version; - - /** - * ID of directive that supercedes this old directive, is an instance - * of HTMLPurifier_ConfigSchema_Interchange_Id. Null if not deprecated. - */ - public $deprecatedUse; - - /** - * Version of HTML Purifier this directive was deprecated. Null if not - * deprecated. - */ - public $deprecatedVersion; - - /** - * List of external projects this directive depends on, e.g. array('CSSTidy'). - */ - public $external = array(); - -} - -// vim: et sw=4 sts=4 + true). + * Null if all values are allowed. + */ + public $allowed; + + /** + * List of aliases for the directive, + * e.g. array(new HTMLPurifier_ConfigSchema_Interchange_Id('Ns', 'Dir'))). + */ + public $aliases = array(); + + /** + * Hash of value aliases, e.g. array('alt' => 'real'). Null if value + * aliasing is disabled (necessary for non-scalar types). + */ + public $valueAliases; + + /** + * Version of HTML Purifier the directive was introduced, e.g. '1.3.1'. + * Null if the directive has always existed. + */ + public $version; + + /** + * ID of directive that supercedes this old directive, is an instance + * of HTMLPurifier_ConfigSchema_Interchange_Id. Null if not deprecated. + */ + public $deprecatedUse; + + /** + * Version of HTML Purifier this directive was deprecated. Null if not + * deprecated. + */ + public $deprecatedVersion; + + /** + * List of external projects this directive depends on, e.g. array('CSSTidy'). + */ + public $external = array(); + +} + +// vim: et sw=4 sts=4 diff --git a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Interchange/Id.php b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Interchange/Id.php index 404c4042e56..b9b3c6f5cfb 100644 --- a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Interchange/Id.php +++ b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Interchange/Id.php @@ -1,37 +1,37 @@ -key = $key; - } - - /** - * @warning This is NOT magic, to ensure that people don't abuse SPL and - * cause problems for PHP 5.0 support. - */ - public function toString() { - return $this->key; - } - - public function getRootNamespace() { - return substr($this->key, 0, strpos($this->key, ".")); - } - - public function getDirective() { - return substr($this->key, strpos($this->key, ".") + 1); - } - - public static function make($id) { - return new HTMLPurifier_ConfigSchema_Interchange_Id($id); - } - -} - -// vim: et sw=4 sts=4 +key = $key; + } + + /** + * @warning This is NOT magic, to ensure that people don't abuse SPL and + * cause problems for PHP 5.0 support. + */ + public function toString() { + return $this->key; + } + + public function getRootNamespace() { + return substr($this->key, 0, strpos($this->key, ".")); + } + + public function getDirective() { + return substr($this->key, strpos($this->key, ".") + 1); + } + + public static function make($id) { + return new HTMLPurifier_ConfigSchema_Interchange_Id($id); + } + +} + +// vim: et sw=4 sts=4 diff --git a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/InterchangeBuilder.php b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/InterchangeBuilder.php index fe9b3268f45..655e6dd1b92 100644 --- a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/InterchangeBuilder.php +++ b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/InterchangeBuilder.php @@ -1,226 +1,226 @@ -varParser = $varParser ? $varParser : new HTMLPurifier_VarParser_Native(); - } - - /** - * @param string $dir - * @return HTMLPurifier_ConfigSchema_Interchange - */ - public static function buildFromDirectory($dir = null) - { - $builder = new HTMLPurifier_ConfigSchema_InterchangeBuilder(); - $interchange = new HTMLPurifier_ConfigSchema_Interchange(); - return $builder->buildDir($interchange, $dir); - } - - /** - * @param HTMLPurifier_ConfigSchema_Interchange $interchange - * @param string $dir - * @return HTMLPurifier_ConfigSchema_Interchange - */ - public function buildDir($interchange, $dir = null) - { - if (!$dir) { - $dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema'; - } - if (file_exists($dir . '/info.ini')) { - $info = parse_ini_file($dir . '/info.ini'); - $interchange->name = $info['name']; - } - - $files = array(); - $dh = opendir($dir); - while (false !== ($file = readdir($dh))) { - if (!$file || $file[0] == '.' || strrchr($file, '.') !== '.txt') { - continue; - } - $files[] = $file; - } - closedir($dh); - - sort($files); - foreach ($files as $file) { - $this->buildFile($interchange, $dir . '/' . $file); - } - return $interchange; - } - - /** - * @param HTMLPurifier_ConfigSchema_Interchange $interchange - * @param string $file - */ - public function buildFile($interchange, $file) - { - $parser = new HTMLPurifier_StringHashParser(); - $this->build( - $interchange, - new HTMLPurifier_StringHash($parser->parseFile($file)) - ); - } - - /** - * Builds an interchange object based on a hash. - * @param HTMLPurifier_ConfigSchema_Interchange $interchange HTMLPurifier_ConfigSchema_Interchange object to build - * @param HTMLPurifier_StringHash $hash source data - * @throws HTMLPurifier_ConfigSchema_Exception - */ - public function build($interchange, $hash) - { - if (!$hash instanceof HTMLPurifier_StringHash) { - $hash = new HTMLPurifier_StringHash($hash); - } - if (!isset($hash['ID'])) { - throw new HTMLPurifier_ConfigSchema_Exception('Hash does not have any ID'); - } - if (strpos($hash['ID'], '.') === false) { - if (count($hash) == 2 && isset($hash['DESCRIPTION'])) { - $hash->offsetGet('DESCRIPTION'); // prevent complaining - } else { - throw new HTMLPurifier_ConfigSchema_Exception('All directives must have a namespace'); - } - } else { - $this->buildDirective($interchange, $hash); - } - $this->_findUnused($hash); - } - - /** - * @param HTMLPurifier_ConfigSchema_Interchange $interchange - * @param HTMLPurifier_StringHash $hash - * @throws HTMLPurifier_ConfigSchema_Exception - */ - public function buildDirective($interchange, $hash) - { - $directive = new HTMLPurifier_ConfigSchema_Interchange_Directive(); - - // These are required elements: - $directive->id = $this->id($hash->offsetGet('ID')); - $id = $directive->id->toString(); // convenience - - if (isset($hash['TYPE'])) { - $type = explode('/', $hash->offsetGet('TYPE')); - if (isset($type[1])) { - $directive->typeAllowsNull = true; - } - $directive->type = $type[0]; - } else { - throw new HTMLPurifier_ConfigSchema_Exception("TYPE in directive hash '$id' not defined"); - } - - if (isset($hash['DEFAULT'])) { - try { - $directive->default = $this->varParser->parse( - $hash->offsetGet('DEFAULT'), - $directive->type, - $directive->typeAllowsNull - ); - } catch (HTMLPurifier_VarParserException $e) { - throw new HTMLPurifier_ConfigSchema_Exception($e->getMessage() . " in DEFAULT in directive hash '$id'"); - } - } - - if (isset($hash['DESCRIPTION'])) { - $directive->description = $hash->offsetGet('DESCRIPTION'); - } - - if (isset($hash['ALLOWED'])) { - $directive->allowed = $this->lookup($this->evalArray($hash->offsetGet('ALLOWED'))); - } - - if (isset($hash['VALUE-ALIASES'])) { - $directive->valueAliases = $this->evalArray($hash->offsetGet('VALUE-ALIASES')); - } - - if (isset($hash['ALIASES'])) { - $raw_aliases = trim($hash->offsetGet('ALIASES')); - $aliases = preg_split('/\s*,\s*/', $raw_aliases); - foreach ($aliases as $alias) { - $directive->aliases[] = $this->id($alias); - } - } - - if (isset($hash['VERSION'])) { - $directive->version = $hash->offsetGet('VERSION'); - } - - if (isset($hash['DEPRECATED-USE'])) { - $directive->deprecatedUse = $this->id($hash->offsetGet('DEPRECATED-USE')); - } - - if (isset($hash['DEPRECATED-VERSION'])) { - $directive->deprecatedVersion = $hash->offsetGet('DEPRECATED-VERSION'); - } - - if (isset($hash['EXTERNAL'])) { - $directive->external = preg_split('/\s*,\s*/', trim($hash->offsetGet('EXTERNAL'))); - } - - $interchange->addDirective($directive); - } - - /** - * Evaluates an array PHP code string without array() wrapper - * @param string $contents - */ - protected function evalArray($contents) - { - return eval('return array(' . $contents . ');'); - } - - /** - * Converts an array list into a lookup array. - * @param array $array - * @return array - */ - protected function lookup($array) - { - $ret = array(); - foreach ($array as $val) { - $ret[$val] = true; - } - return $ret; - } - - /** - * Convenience function that creates an HTMLPurifier_ConfigSchema_Interchange_Id - * object based on a string Id. - * @param string $id - * @return HTMLPurifier_ConfigSchema_Interchange_Id - */ - protected function id($id) - { - return HTMLPurifier_ConfigSchema_Interchange_Id::make($id); - } - - /** - * Triggers errors for any unused keys passed in the hash; such keys - * may indicate typos, missing values, etc. - * @param HTMLPurifier_StringHash $hash Hash to check. - */ - protected function _findUnused($hash) - { - $accessed = $hash->getAccessed(); - foreach ($hash as $k => $v) { - if (!isset($accessed[$k])) { - trigger_error("String hash key '$k' not used by builder", E_USER_NOTICE); - } - } - } -} - -// vim: et sw=4 sts=4 +varParser = $varParser ? $varParser : new HTMLPurifier_VarParser_Native(); + } + + /** + * @param string $dir + * @return HTMLPurifier_ConfigSchema_Interchange + */ + public static function buildFromDirectory($dir = null) + { + $builder = new HTMLPurifier_ConfigSchema_InterchangeBuilder(); + $interchange = new HTMLPurifier_ConfigSchema_Interchange(); + return $builder->buildDir($interchange, $dir); + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @param string $dir + * @return HTMLPurifier_ConfigSchema_Interchange + */ + public function buildDir($interchange, $dir = null) + { + if (!$dir) { + $dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema'; + } + if (file_exists($dir . '/info.ini')) { + $info = parse_ini_file($dir . '/info.ini'); + $interchange->name = $info['name']; + } + + $files = array(); + $dh = opendir($dir); + while (false !== ($file = readdir($dh))) { + if (!$file || $file[0] == '.' || strrchr($file, '.') !== '.txt') { + continue; + } + $files[] = $file; + } + closedir($dh); + + sort($files); + foreach ($files as $file) { + $this->buildFile($interchange, $dir . '/' . $file); + } + return $interchange; + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @param string $file + */ + public function buildFile($interchange, $file) + { + $parser = new HTMLPurifier_StringHashParser(); + $this->build( + $interchange, + new HTMLPurifier_StringHash($parser->parseFile($file)) + ); + } + + /** + * Builds an interchange object based on a hash. + * @param HTMLPurifier_ConfigSchema_Interchange $interchange HTMLPurifier_ConfigSchema_Interchange object to build + * @param HTMLPurifier_StringHash $hash source data + * @throws HTMLPurifier_ConfigSchema_Exception + */ + public function build($interchange, $hash) + { + if (!$hash instanceof HTMLPurifier_StringHash) { + $hash = new HTMLPurifier_StringHash($hash); + } + if (!isset($hash['ID'])) { + throw new HTMLPurifier_ConfigSchema_Exception('Hash does not have any ID'); + } + if (strpos($hash['ID'], '.') === false) { + if (count($hash) == 2 && isset($hash['DESCRIPTION'])) { + $hash->offsetGet('DESCRIPTION'); // prevent complaining + } else { + throw new HTMLPurifier_ConfigSchema_Exception('All directives must have a namespace'); + } + } else { + $this->buildDirective($interchange, $hash); + } + $this->_findUnused($hash); + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @param HTMLPurifier_StringHash $hash + * @throws HTMLPurifier_ConfigSchema_Exception + */ + public function buildDirective($interchange, $hash) + { + $directive = new HTMLPurifier_ConfigSchema_Interchange_Directive(); + + // These are required elements: + $directive->id = $this->id($hash->offsetGet('ID')); + $id = $directive->id->toString(); // convenience + + if (isset($hash['TYPE'])) { + $type = explode('/', $hash->offsetGet('TYPE')); + if (isset($type[1])) { + $directive->typeAllowsNull = true; + } + $directive->type = $type[0]; + } else { + throw new HTMLPurifier_ConfigSchema_Exception("TYPE in directive hash '$id' not defined"); + } + + if (isset($hash['DEFAULT'])) { + try { + $directive->default = $this->varParser->parse( + $hash->offsetGet('DEFAULT'), + $directive->type, + $directive->typeAllowsNull + ); + } catch (HTMLPurifier_VarParserException $e) { + throw new HTMLPurifier_ConfigSchema_Exception($e->getMessage() . " in DEFAULT in directive hash '$id'"); + } + } + + if (isset($hash['DESCRIPTION'])) { + $directive->description = $hash->offsetGet('DESCRIPTION'); + } + + if (isset($hash['ALLOWED'])) { + $directive->allowed = $this->lookup($this->evalArray($hash->offsetGet('ALLOWED'))); + } + + if (isset($hash['VALUE-ALIASES'])) { + $directive->valueAliases = $this->evalArray($hash->offsetGet('VALUE-ALIASES')); + } + + if (isset($hash['ALIASES'])) { + $raw_aliases = trim($hash->offsetGet('ALIASES')); + $aliases = preg_split('/\s*,\s*/', $raw_aliases); + foreach ($aliases as $alias) { + $directive->aliases[] = $this->id($alias); + } + } + + if (isset($hash['VERSION'])) { + $directive->version = $hash->offsetGet('VERSION'); + } + + if (isset($hash['DEPRECATED-USE'])) { + $directive->deprecatedUse = $this->id($hash->offsetGet('DEPRECATED-USE')); + } + + if (isset($hash['DEPRECATED-VERSION'])) { + $directive->deprecatedVersion = $hash->offsetGet('DEPRECATED-VERSION'); + } + + if (isset($hash['EXTERNAL'])) { + $directive->external = preg_split('/\s*,\s*/', trim($hash->offsetGet('EXTERNAL'))); + } + + $interchange->addDirective($directive); + } + + /** + * Evaluates an array PHP code string without array() wrapper + * @param string $contents + */ + protected function evalArray($contents) + { + return eval('return array(' . $contents . ');'); + } + + /** + * Converts an array list into a lookup array. + * @param array $array + * @return array + */ + protected function lookup($array) + { + $ret = array(); + foreach ($array as $val) { + $ret[$val] = true; + } + return $ret; + } + + /** + * Convenience function that creates an HTMLPurifier_ConfigSchema_Interchange_Id + * object based on a string Id. + * @param string $id + * @return HTMLPurifier_ConfigSchema_Interchange_Id + */ + protected function id($id) + { + return HTMLPurifier_ConfigSchema_Interchange_Id::make($id); + } + + /** + * Triggers errors for any unused keys passed in the hash; such keys + * may indicate typos, missing values, etc. + * @param HTMLPurifier_StringHash $hash Hash to check. + */ + protected function _findUnused($hash) + { + $accessed = $hash->getAccessed(); + foreach ($hash as $k => $v) { + if (!isset($accessed[$k])) { + trigger_error("String hash key '$k' not used by builder", E_USER_NOTICE); + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Validator.php b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Validator.php index 9f14444f375..fb31277889f 100644 --- a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Validator.php +++ b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Validator.php @@ -1,248 +1,248 @@ -parser = new HTMLPurifier_VarParser(); - } - - /** - * Validates a fully-formed interchange object. - * @param HTMLPurifier_ConfigSchema_Interchange $interchange - * @return bool - */ - public function validate($interchange) - { - $this->interchange = $interchange; - $this->aliases = array(); - // PHP is a bit lax with integer <=> string conversions in - // arrays, so we don't use the identical !== comparison - foreach ($interchange->directives as $i => $directive) { - $id = $directive->id->toString(); - if ($i != $id) { - $this->error(false, "Integrity violation: key '$i' does not match internal id '$id'"); - } - $this->validateDirective($directive); - } - return true; - } - - /** - * Validates a HTMLPurifier_ConfigSchema_Interchange_Id object. - * @param HTMLPurifier_ConfigSchema_Interchange_Id $id - */ - public function validateId($id) - { - $id_string = $id->toString(); - $this->context[] = "id '$id_string'"; - if (!$id instanceof HTMLPurifier_ConfigSchema_Interchange_Id) { - // handled by InterchangeBuilder - $this->error(false, 'is not an instance of HTMLPurifier_ConfigSchema_Interchange_Id'); - } - // keys are now unconstrained (we might want to narrow down to A-Za-z0-9.) - // we probably should check that it has at least one namespace - $this->with($id, 'key') - ->assertNotEmpty() - ->assertIsString(); // implicit assertIsString handled by InterchangeBuilder - array_pop($this->context); - } - - /** - * Validates a HTMLPurifier_ConfigSchema_Interchange_Directive object. - * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d - */ - public function validateDirective($d) - { - $id = $d->id->toString(); - $this->context[] = "directive '$id'"; - $this->validateId($d->id); - - $this->with($d, 'description') - ->assertNotEmpty(); - - // BEGIN - handled by InterchangeBuilder - $this->with($d, 'type') - ->assertNotEmpty(); - $this->with($d, 'typeAllowsNull') - ->assertIsBool(); - try { - // This also tests validity of $d->type - $this->parser->parse($d->default, $d->type, $d->typeAllowsNull); - } catch (HTMLPurifier_VarParserException $e) { - $this->error('default', 'had error: ' . $e->getMessage()); - } - // END - handled by InterchangeBuilder - - if (!is_null($d->allowed) || !empty($d->valueAliases)) { - // allowed and valueAliases require that we be dealing with - // strings, so check for that early. - $d_int = HTMLPurifier_VarParser::$types[$d->type]; - if (!isset(HTMLPurifier_VarParser::$stringTypes[$d_int])) { - $this->error('type', 'must be a string type when used with allowed or value aliases'); - } - } - - $this->validateDirectiveAllowed($d); - $this->validateDirectiveValueAliases($d); - $this->validateDirectiveAliases($d); - - array_pop($this->context); - } - - /** - * Extra validation if $allowed member variable of - * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. - * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d - */ - public function validateDirectiveAllowed($d) - { - if (is_null($d->allowed)) { - return; - } - $this->with($d, 'allowed') - ->assertNotEmpty() - ->assertIsLookup(); // handled by InterchangeBuilder - if (is_string($d->default) && !isset($d->allowed[$d->default])) { - $this->error('default', 'must be an allowed value'); - } - $this->context[] = 'allowed'; - foreach ($d->allowed as $val => $x) { - if (!is_string($val)) { - $this->error("value $val", 'must be a string'); - } - } - array_pop($this->context); - } - - /** - * Extra validation if $valueAliases member variable of - * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. - * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d - */ - public function validateDirectiveValueAliases($d) - { - if (is_null($d->valueAliases)) { - return; - } - $this->with($d, 'valueAliases') - ->assertIsArray(); // handled by InterchangeBuilder - $this->context[] = 'valueAliases'; - foreach ($d->valueAliases as $alias => $real) { - if (!is_string($alias)) { - $this->error("alias $alias", 'must be a string'); - } - if (!is_string($real)) { - $this->error("alias target $real from alias '$alias'", 'must be a string'); - } - if ($alias === $real) { - $this->error("alias '$alias'", "must not be an alias to itself"); - } - } - if (!is_null($d->allowed)) { - foreach ($d->valueAliases as $alias => $real) { - if (isset($d->allowed[$alias])) { - $this->error("alias '$alias'", 'must not be an allowed value'); - } elseif (!isset($d->allowed[$real])) { - $this->error("alias '$alias'", 'must be an alias to an allowed value'); - } - } - } - array_pop($this->context); - } - - /** - * Extra validation if $aliases member variable of - * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. - * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d - */ - public function validateDirectiveAliases($d) - { - $this->with($d, 'aliases') - ->assertIsArray(); // handled by InterchangeBuilder - $this->context[] = 'aliases'; - foreach ($d->aliases as $alias) { - $this->validateId($alias); - $s = $alias->toString(); - if (isset($this->interchange->directives[$s])) { - $this->error("alias '$s'", 'collides with another directive'); - } - if (isset($this->aliases[$s])) { - $other_directive = $this->aliases[$s]; - $this->error("alias '$s'", "collides with alias for directive '$other_directive'"); - } - $this->aliases[$s] = $d->id->toString(); - } - array_pop($this->context); - } - - // protected helper functions - - /** - * Convenience function for generating HTMLPurifier_ConfigSchema_ValidatorAtom - * for validating simple member variables of objects. - * @param $obj - * @param $member - * @return HTMLPurifier_ConfigSchema_ValidatorAtom - */ - protected function with($obj, $member) - { - return new HTMLPurifier_ConfigSchema_ValidatorAtom($this->getFormattedContext(), $obj, $member); - } - - /** - * Emits an error, providing helpful context. - * @throws HTMLPurifier_ConfigSchema_Exception - */ - protected function error($target, $msg) - { - if ($target !== false) { - $prefix = ucfirst($target) . ' in ' . $this->getFormattedContext(); - } else { - $prefix = ucfirst($this->getFormattedContext()); - } - throw new HTMLPurifier_ConfigSchema_Exception(trim($prefix . ' ' . $msg)); - } - - /** - * Returns a formatted context string. - * @return string - */ - protected function getFormattedContext() - { - return implode(' in ', array_reverse($this->context)); - } -} - -// vim: et sw=4 sts=4 +parser = new HTMLPurifier_VarParser(); + } + + /** + * Validates a fully-formed interchange object. + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @return bool + */ + public function validate($interchange) + { + $this->interchange = $interchange; + $this->aliases = array(); + // PHP is a bit lax with integer <=> string conversions in + // arrays, so we don't use the identical !== comparison + foreach ($interchange->directives as $i => $directive) { + $id = $directive->id->toString(); + if ($i != $id) { + $this->error(false, "Integrity violation: key '$i' does not match internal id '$id'"); + } + $this->validateDirective($directive); + } + return true; + } + + /** + * Validates a HTMLPurifier_ConfigSchema_Interchange_Id object. + * @param HTMLPurifier_ConfigSchema_Interchange_Id $id + */ + public function validateId($id) + { + $id_string = $id->toString(); + $this->context[] = "id '$id_string'"; + if (!$id instanceof HTMLPurifier_ConfigSchema_Interchange_Id) { + // handled by InterchangeBuilder + $this->error(false, 'is not an instance of HTMLPurifier_ConfigSchema_Interchange_Id'); + } + // keys are now unconstrained (we might want to narrow down to A-Za-z0-9.) + // we probably should check that it has at least one namespace + $this->with($id, 'key') + ->assertNotEmpty() + ->assertIsString(); // implicit assertIsString handled by InterchangeBuilder + array_pop($this->context); + } + + /** + * Validates a HTMLPurifier_ConfigSchema_Interchange_Directive object. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + public function validateDirective($d) + { + $id = $d->id->toString(); + $this->context[] = "directive '$id'"; + $this->validateId($d->id); + + $this->with($d, 'description') + ->assertNotEmpty(); + + // BEGIN - handled by InterchangeBuilder + $this->with($d, 'type') + ->assertNotEmpty(); + $this->with($d, 'typeAllowsNull') + ->assertIsBool(); + try { + // This also tests validity of $d->type + $this->parser->parse($d->default, $d->type, $d->typeAllowsNull); + } catch (HTMLPurifier_VarParserException $e) { + $this->error('default', 'had error: ' . $e->getMessage()); + } + // END - handled by InterchangeBuilder + + if (!is_null($d->allowed) || !empty($d->valueAliases)) { + // allowed and valueAliases require that we be dealing with + // strings, so check for that early. + $d_int = HTMLPurifier_VarParser::$types[$d->type]; + if (!isset(HTMLPurifier_VarParser::$stringTypes[$d_int])) { + $this->error('type', 'must be a string type when used with allowed or value aliases'); + } + } + + $this->validateDirectiveAllowed($d); + $this->validateDirectiveValueAliases($d); + $this->validateDirectiveAliases($d); + + array_pop($this->context); + } + + /** + * Extra validation if $allowed member variable of + * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + public function validateDirectiveAllowed($d) + { + if (is_null($d->allowed)) { + return; + } + $this->with($d, 'allowed') + ->assertNotEmpty() + ->assertIsLookup(); // handled by InterchangeBuilder + if (is_string($d->default) && !isset($d->allowed[$d->default])) { + $this->error('default', 'must be an allowed value'); + } + $this->context[] = 'allowed'; + foreach ($d->allowed as $val => $x) { + if (!is_string($val)) { + $this->error("value $val", 'must be a string'); + } + } + array_pop($this->context); + } + + /** + * Extra validation if $valueAliases member variable of + * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + public function validateDirectiveValueAliases($d) + { + if (is_null($d->valueAliases)) { + return; + } + $this->with($d, 'valueAliases') + ->assertIsArray(); // handled by InterchangeBuilder + $this->context[] = 'valueAliases'; + foreach ($d->valueAliases as $alias => $real) { + if (!is_string($alias)) { + $this->error("alias $alias", 'must be a string'); + } + if (!is_string($real)) { + $this->error("alias target $real from alias '$alias'", 'must be a string'); + } + if ($alias === $real) { + $this->error("alias '$alias'", "must not be an alias to itself"); + } + } + if (!is_null($d->allowed)) { + foreach ($d->valueAliases as $alias => $real) { + if (isset($d->allowed[$alias])) { + $this->error("alias '$alias'", 'must not be an allowed value'); + } elseif (!isset($d->allowed[$real])) { + $this->error("alias '$alias'", 'must be an alias to an allowed value'); + } + } + } + array_pop($this->context); + } + + /** + * Extra validation if $aliases member variable of + * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + public function validateDirectiveAliases($d) + { + $this->with($d, 'aliases') + ->assertIsArray(); // handled by InterchangeBuilder + $this->context[] = 'aliases'; + foreach ($d->aliases as $alias) { + $this->validateId($alias); + $s = $alias->toString(); + if (isset($this->interchange->directives[$s])) { + $this->error("alias '$s'", 'collides with another directive'); + } + if (isset($this->aliases[$s])) { + $other_directive = $this->aliases[$s]; + $this->error("alias '$s'", "collides with alias for directive '$other_directive'"); + } + $this->aliases[$s] = $d->id->toString(); + } + array_pop($this->context); + } + + // protected helper functions + + /** + * Convenience function for generating HTMLPurifier_ConfigSchema_ValidatorAtom + * for validating simple member variables of objects. + * @param $obj + * @param $member + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + protected function with($obj, $member) + { + return new HTMLPurifier_ConfigSchema_ValidatorAtom($this->getFormattedContext(), $obj, $member); + } + + /** + * Emits an error, providing helpful context. + * @throws HTMLPurifier_ConfigSchema_Exception + */ + protected function error($target, $msg) + { + if ($target !== false) { + $prefix = ucfirst($target) . ' in ' . $this->getFormattedContext(); + } else { + $prefix = ucfirst($this->getFormattedContext()); + } + throw new HTMLPurifier_ConfigSchema_Exception(trim($prefix . ' ' . $msg)); + } + + /** + * Returns a formatted context string. + * @return string + */ + protected function getFormattedContext() + { + return implode(' in ', array_reverse($this->context)); + } +} + +// vim: et sw=4 sts=4 diff --git a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/ValidatorAtom.php b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/ValidatorAtom.php index a2e0b4a1b32..c9aa3644af7 100644 --- a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/ValidatorAtom.php +++ b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/ValidatorAtom.php @@ -1,130 +1,130 @@ -context = $context; - $this->obj = $obj; - $this->member = $member; - $this->contents =& $obj->$member; - } - - /** - * @return HTMLPurifier_ConfigSchema_ValidatorAtom - */ - public function assertIsString() - { - if (!is_string($this->contents)) { - $this->error('must be a string'); - } - return $this; - } - - /** - * @return HTMLPurifier_ConfigSchema_ValidatorAtom - */ - public function assertIsBool() - { - if (!is_bool($this->contents)) { - $this->error('must be a boolean'); - } - return $this; - } - - /** - * @return HTMLPurifier_ConfigSchema_ValidatorAtom - */ - public function assertIsArray() - { - if (!is_array($this->contents)) { - $this->error('must be an array'); - } - return $this; - } - - /** - * @return HTMLPurifier_ConfigSchema_ValidatorAtom - */ - public function assertNotNull() - { - if ($this->contents === null) { - $this->error('must not be null'); - } - return $this; - } - - /** - * @return HTMLPurifier_ConfigSchema_ValidatorAtom - */ - public function assertAlnum() - { - $this->assertIsString(); - if (!ctype_alnum($this->contents)) { - $this->error('must be alphanumeric'); - } - return $this; - } - - /** - * @return HTMLPurifier_ConfigSchema_ValidatorAtom - */ - public function assertNotEmpty() - { - if (empty($this->contents)) { - $this->error('must not be empty'); - } - return $this; - } - - /** - * @return HTMLPurifier_ConfigSchema_ValidatorAtom - */ - public function assertIsLookup() - { - $this->assertIsArray(); - foreach ($this->contents as $v) { - if ($v !== true) { - $this->error('must be a lookup array'); - } - } - return $this; - } - - /** - * @param string $msg - * @throws HTMLPurifier_ConfigSchema_Exception - */ - protected function error($msg) - { - throw new HTMLPurifier_ConfigSchema_Exception(ucfirst($this->member) . ' in ' . $this->context . ' ' . $msg); - } -} - -// vim: et sw=4 sts=4 +context = $context; + $this->obj = $obj; + $this->member = $member; + $this->contents =& $obj->$member; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertIsString() + { + if (!is_string($this->contents)) { + $this->error('must be a string'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertIsBool() + { + if (!is_bool($this->contents)) { + $this->error('must be a boolean'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertIsArray() + { + if (!is_array($this->contents)) { + $this->error('must be an array'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertNotNull() + { + if ($this->contents === null) { + $this->error('must not be null'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertAlnum() + { + $this->assertIsString(); + if (!ctype_alnum($this->contents)) { + $this->error('must be alphanumeric'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertNotEmpty() + { + if (empty($this->contents)) { + $this->error('must not be empty'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertIsLookup() + { + $this->assertIsArray(); + foreach ($this->contents as $v) { + if ($v !== true) { + $this->error('must be a lookup array'); + } + } + return $this; + } + + /** + * @param string $msg + * @throws HTMLPurifier_ConfigSchema_Exception + */ + protected function error($msg) + { + throw new HTMLPurifier_ConfigSchema_Exception(ucfirst($this->member) . ' in ' . $this->context . ' ' . $msg); + } +} + +// vim: et sw=4 sts=4 diff --git a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Filter/ExtractStyleBlocks.php b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Filter/ExtractStyleBlocks.php index aeb25df7ebd..df937ace738 100644 --- a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Filter/ExtractStyleBlocks.php +++ b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Filter/ExtractStyleBlocks.php @@ -1,289 +1,289 @@ - blocks from input HTML, cleans them up - * using CSSTidy, and then places them in $purifier->context->get('StyleBlocks') - * so they can be used elsewhere in the document. - * - * @note - * See tests/HTMLPurifier/Filter/ExtractStyleBlocksTest.php for - * sample usage. - * - * @note - * This filter can also be used on stylesheets not included in the - * document--something purists would probably prefer. Just directly - * call HTMLPurifier_Filter_ExtractStyleBlocks->cleanCSS() - */ -class HTMLPurifier_Filter_ExtractStyleBlocks extends HTMLPurifier_Filter -{ - - public $name = 'ExtractStyleBlocks'; - private $_styleMatches = array(); - private $_tidy; - - private $_id_attrdef; - private $_class_attrdef; - private $_enum_attrdef; - - public function __construct() { - $this->_tidy = new csstidy(); - $this->_tidy->set_cfg('lowercase_s', false); - $this->_id_attrdef = new HTMLPurifier_AttrDef_HTML_ID(true); - $this->_class_attrdef = new HTMLPurifier_AttrDef_CSS_Ident(); - $this->_enum_attrdef = new HTMLPurifier_AttrDef_Enum(array('first-child', 'link', 'visited', 'active', 'hover', 'focus')); - } - - /** - * Save the contents of CSS blocks to style matches - * @param $matches preg_replace style $matches array - */ - protected function styleCallback($matches) { - $this->_styleMatches[] = $matches[1]; - } - - /** - * Removes inline #isU', array($this, 'styleCallback'), $html); - $style_blocks = $this->_styleMatches; - $this->_styleMatches = array(); // reset - $context->register('StyleBlocks', $style_blocks); // $context must not be reused - if ($this->_tidy) { - foreach ($style_blocks as &$style) { - $style = $this->cleanCSS($style, $config, $context); - } - } - return $html; - } - - /** - * Takes CSS (the stuff found in in a font-family prop). - if ($config->get('Filter.ExtractStyleBlocks.Escaping')) { - $css = str_replace( - array('<', '>', '&'), - array('\3C ', '\3E ', '\26 '), - $css - ); - } - return $css; - } - -} - -// vim: et sw=4 sts=4 + blocks from input HTML, cleans them up + * using CSSTidy, and then places them in $purifier->context->get('StyleBlocks') + * so they can be used elsewhere in the document. + * + * @note + * See tests/HTMLPurifier/Filter/ExtractStyleBlocksTest.php for + * sample usage. + * + * @note + * This filter can also be used on stylesheets not included in the + * document--something purists would probably prefer. Just directly + * call HTMLPurifier_Filter_ExtractStyleBlocks->cleanCSS() + */ +class HTMLPurifier_Filter_ExtractStyleBlocks extends HTMLPurifier_Filter +{ + + public $name = 'ExtractStyleBlocks'; + private $_styleMatches = array(); + private $_tidy; + + private $_id_attrdef; + private $_class_attrdef; + private $_enum_attrdef; + + public function __construct() { + $this->_tidy = new csstidy(); + $this->_tidy->set_cfg('lowercase_s', false); + $this->_id_attrdef = new HTMLPurifier_AttrDef_HTML_ID(true); + $this->_class_attrdef = new HTMLPurifier_AttrDef_CSS_Ident(); + $this->_enum_attrdef = new HTMLPurifier_AttrDef_Enum(array('first-child', 'link', 'visited', 'active', 'hover', 'focus')); + } + + /** + * Save the contents of CSS blocks to style matches + * @param $matches preg_replace style $matches array + */ + protected function styleCallback($matches) { + $this->_styleMatches[] = $matches[1]; + } + + /** + * Removes inline #isU', array($this, 'styleCallback'), $html); + $style_blocks = $this->_styleMatches; + $this->_styleMatches = array(); // reset + $context->register('StyleBlocks', $style_blocks); // $context must not be reused + if ($this->_tidy) { + foreach ($style_blocks as &$style) { + $style = $this->cleanCSS($style, $config, $context); + } + } + return $html; + } + + /** + * Takes CSS (the stuff found in in a font-family prop). + if ($config->get('Filter.ExtractStyleBlocks.Escaping')) { + $css = str_replace( + array('<', '>', '&'), + array('\3C ', '\3E ', '\26 '), + $css + ); + } + return $css; + } + +} + +// vim: et sw=4 sts=4 diff --git a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Filter/YouTube.php b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Filter/YouTube.php index 46338876a91..23df221eaa3 100644 --- a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Filter/YouTube.php +++ b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Filter/YouTube.php @@ -1,39 +1,39 @@ -]+>.+?'. - 'http://www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+).+?#s'; - $pre_replace = '\1'; - return preg_replace($pre_regex, $pre_replace, $html); - } - - public function postFilter($html, $config, $context) { - $post_regex = '#((?:v|cp)/[A-Za-z0-9\-_=]+)#'; - return preg_replace_callback($post_regex, array($this, 'postFilterCallback'), $html); - } - - protected function armorUrl($url) { - return str_replace('--', '--', $url); - } - - protected function postFilterCallback($matches) { - $url = $this->armorUrl($matches[1]); - return ''. - ''. - ''. - ''; - - } -} - -// vim: et sw=4 sts=4 +]+>.+?'. + 'http://www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+).+?#s'; + $pre_replace = '\1'; + return preg_replace($pre_regex, $pre_replace, $html); + } + + public function postFilter($html, $config, $context) { + $post_regex = '#((?:v|cp)/[A-Za-z0-9\-_=]+)#'; + return preg_replace_callback($post_regex, array($this, 'postFilterCallback'), $html); + } + + protected function armorUrl($url) { + return str_replace('--', '--', $url); + } + + protected function postFilterCallback($matches) { + $url = $this->armorUrl($matches[1]); + return ''. + ''. + ''. + ''; + + } +} + +// vim: et sw=4 sts=4 diff --git a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Language/classes/en-x-test.php b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Language/classes/en-x-test.php index d2f9baaaa9e..d52fcb7ac18 100644 --- a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Language/classes/en-x-test.php +++ b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Language/classes/en-x-test.php @@ -1,12 +1,12 @@ - 'HTML Purifier X' -); - -// vim: et sw=4 sts=4 + 'HTML Purifier X' +); + +// vim: et sw=4 sts=4 diff --git a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Language/messages/en-x-testmini.php b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Language/messages/en-x-testmini.php index ed8560fd586..806c83fbf75 100644 --- a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Language/messages/en-x-testmini.php +++ b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Language/messages/en-x-testmini.php @@ -1,12 +1,12 @@ - 'HTML Purifier XNone' -); - -// vim: et sw=4 sts=4 + 'HTML Purifier XNone' +); + +// vim: et sw=4 sts=4 diff --git a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Language/messages/en.php b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Language/messages/en.php index 1fa30bdfedd..c7f197e1e1b 100644 --- a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Language/messages/en.php +++ b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Language/messages/en.php @@ -1,55 +1,55 @@ - 'HTML Purifier', -// for unit testing purposes - 'LanguageFactoryTest: Pizza' => 'Pizza', - 'LanguageTest: List' => '$1', - 'LanguageTest: Hash' => '$1.Keys; $1.Values', - 'Item separator' => ', ', - 'Item separator last' => ' and ', // non-Harvard style - - 'ErrorCollector: No errors' => 'No errors detected. However, because error reporting is still incomplete, there may have been errors that the error collector was not notified of; please inspect the output HTML carefully.', - 'ErrorCollector: At line' => ' at line $line', - 'ErrorCollector: Incidental errors' => 'Incidental errors', - 'Lexer: Unclosed comment' => 'Unclosed comment', - 'Lexer: Unescaped lt' => 'Unescaped less-than sign (<) should be <', - 'Lexer: Missing gt' => 'Missing greater-than sign (>), previous less-than sign (<) should be escaped', - 'Lexer: Missing attribute key' => 'Attribute declaration has no key', - 'Lexer: Missing end quote' => 'Attribute declaration has no end quote', - 'Lexer: Extracted body' => 'Removed document metadata tags', - 'Strategy_RemoveForeignElements: Tag transform' => '<$1> element transformed into $CurrentToken.Serialized', - 'Strategy_RemoveForeignElements: Missing required attribute' => '$CurrentToken.Compact element missing required attribute $1', - 'Strategy_RemoveForeignElements: Foreign element to text' => 'Unrecognized $CurrentToken.Serialized tag converted to text', - 'Strategy_RemoveForeignElements: Foreign element removed' => 'Unrecognized $CurrentToken.Serialized tag removed', - 'Strategy_RemoveForeignElements: Comment removed' => 'Comment containing "$CurrentToken.Data" removed', - 'Strategy_RemoveForeignElements: Foreign meta element removed' => 'Unrecognized $CurrentToken.Serialized meta tag and all descendants removed', - 'Strategy_RemoveForeignElements: Token removed to end' => 'Tags and text starting from $1 element where removed to end', - 'Strategy_RemoveForeignElements: Trailing hyphen in comment removed' => 'Trailing hyphen(s) in comment removed', - 'Strategy_RemoveForeignElements: Hyphens in comment collapsed' => 'Double hyphens in comments are not allowed, and were collapsed into single hyphens', - 'Strategy_MakeWellFormed: Unnecessary end tag removed' => 'Unnecessary $CurrentToken.Serialized tag removed', - 'Strategy_MakeWellFormed: Unnecessary end tag to text' => 'Unnecessary $CurrentToken.Serialized tag converted to text', - 'Strategy_MakeWellFormed: Tag auto closed' => '$1.Compact started on line $1.Line auto-closed by $CurrentToken.Compact', - 'Strategy_MakeWellFormed: Tag carryover' => '$1.Compact started on line $1.Line auto-continued into $CurrentToken.Compact', - 'Strategy_MakeWellFormed: Stray end tag removed' => 'Stray $CurrentToken.Serialized tag removed', - 'Strategy_MakeWellFormed: Stray end tag to text' => 'Stray $CurrentToken.Serialized tag converted to text', - 'Strategy_MakeWellFormed: Tag closed by element end' => '$1.Compact tag started on line $1.Line closed by end of $CurrentToken.Serialized', - 'Strategy_MakeWellFormed: Tag closed by document end' => '$1.Compact tag started on line $1.Line closed by end of document', - 'Strategy_FixNesting: Node removed' => '$CurrentToken.Compact node removed', - 'Strategy_FixNesting: Node excluded' => '$CurrentToken.Compact node removed due to descendant exclusion by ancestor element', - 'Strategy_FixNesting: Node reorganized' => 'Contents of $CurrentToken.Compact node reorganized to enforce its content model', - 'Strategy_FixNesting: Node contents removed' => 'Contents of $CurrentToken.Compact node removed', - 'AttrValidator: Attributes transformed' => 'Attributes on $CurrentToken.Compact transformed from $1.Keys to $2.Keys', - 'AttrValidator: Attribute removed' => '$CurrentAttr.Name attribute on $CurrentToken.Compact removed', -); - -$errorNames = array( - E_ERROR => 'Error', - E_WARNING => 'Warning', - E_NOTICE => 'Notice' -); - -// vim: et sw=4 sts=4 + 'HTML Purifier', +// for unit testing purposes + 'LanguageFactoryTest: Pizza' => 'Pizza', + 'LanguageTest: List' => '$1', + 'LanguageTest: Hash' => '$1.Keys; $1.Values', + 'Item separator' => ', ', + 'Item separator last' => ' and ', // non-Harvard style + + 'ErrorCollector: No errors' => 'No errors detected. However, because error reporting is still incomplete, there may have been errors that the error collector was not notified of; please inspect the output HTML carefully.', + 'ErrorCollector: At line' => ' at line $line', + 'ErrorCollector: Incidental errors' => 'Incidental errors', + 'Lexer: Unclosed comment' => 'Unclosed comment', + 'Lexer: Unescaped lt' => 'Unescaped less-than sign (<) should be <', + 'Lexer: Missing gt' => 'Missing greater-than sign (>), previous less-than sign (<) should be escaped', + 'Lexer: Missing attribute key' => 'Attribute declaration has no key', + 'Lexer: Missing end quote' => 'Attribute declaration has no end quote', + 'Lexer: Extracted body' => 'Removed document metadata tags', + 'Strategy_RemoveForeignElements: Tag transform' => '<$1> element transformed into $CurrentToken.Serialized', + 'Strategy_RemoveForeignElements: Missing required attribute' => '$CurrentToken.Compact element missing required attribute $1', + 'Strategy_RemoveForeignElements: Foreign element to text' => 'Unrecognized $CurrentToken.Serialized tag converted to text', + 'Strategy_RemoveForeignElements: Foreign element removed' => 'Unrecognized $CurrentToken.Serialized tag removed', + 'Strategy_RemoveForeignElements: Comment removed' => 'Comment containing "$CurrentToken.Data" removed', + 'Strategy_RemoveForeignElements: Foreign meta element removed' => 'Unrecognized $CurrentToken.Serialized meta tag and all descendants removed', + 'Strategy_RemoveForeignElements: Token removed to end' => 'Tags and text starting from $1 element where removed to end', + 'Strategy_RemoveForeignElements: Trailing hyphen in comment removed' => 'Trailing hyphen(s) in comment removed', + 'Strategy_RemoveForeignElements: Hyphens in comment collapsed' => 'Double hyphens in comments are not allowed, and were collapsed into single hyphens', + 'Strategy_MakeWellFormed: Unnecessary end tag removed' => 'Unnecessary $CurrentToken.Serialized tag removed', + 'Strategy_MakeWellFormed: Unnecessary end tag to text' => 'Unnecessary $CurrentToken.Serialized tag converted to text', + 'Strategy_MakeWellFormed: Tag auto closed' => '$1.Compact started on line $1.Line auto-closed by $CurrentToken.Compact', + 'Strategy_MakeWellFormed: Tag carryover' => '$1.Compact started on line $1.Line auto-continued into $CurrentToken.Compact', + 'Strategy_MakeWellFormed: Stray end tag removed' => 'Stray $CurrentToken.Serialized tag removed', + 'Strategy_MakeWellFormed: Stray end tag to text' => 'Stray $CurrentToken.Serialized tag converted to text', + 'Strategy_MakeWellFormed: Tag closed by element end' => '$1.Compact tag started on line $1.Line closed by end of $CurrentToken.Serialized', + 'Strategy_MakeWellFormed: Tag closed by document end' => '$1.Compact tag started on line $1.Line closed by end of document', + 'Strategy_FixNesting: Node removed' => '$CurrentToken.Compact node removed', + 'Strategy_FixNesting: Node excluded' => '$CurrentToken.Compact node removed due to descendant exclusion by ancestor element', + 'Strategy_FixNesting: Node reorganized' => 'Contents of $CurrentToken.Compact node reorganized to enforce its content model', + 'Strategy_FixNesting: Node contents removed' => 'Contents of $CurrentToken.Compact node removed', + 'AttrValidator: Attributes transformed' => 'Attributes on $CurrentToken.Compact transformed from $1.Keys to $2.Keys', + 'AttrValidator: Attribute removed' => '$CurrentAttr.Name attribute on $CurrentToken.Compact removed', +); + +$errorNames = array( + E_ERROR => 'Error', + E_WARNING => 'Warning', + E_NOTICE => 'Notice' +); + +// vim: et sw=4 sts=4 diff --git a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Lexer/PH5P.php b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Lexer/PH5P.php index a43c56f9e01..faf00b82913 100644 --- a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Lexer/PH5P.php +++ b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Lexer/PH5P.php @@ -1,3904 +1,3904 @@ -normalize($html, $config, $context); - $new_html = $this->wrapHTML($new_html, $config, $context); - try { - $parser = new HTML5($new_html); - $doc = $parser->save(); - } catch (DOMException $e) { - // Uh oh, it failed. Punt to DirectLex. - $lexer = new HTMLPurifier_Lexer_DirectLex(); - $context->register('PH5PError', $e); // save the error, so we can detect it - return $lexer->tokenizeHTML($html, $config, $context); // use original HTML - } - $tokens = array(); - $this->tokenizeDOM( - $doc->getElementsByTagName('html')->item(0)-> // - getElementsByTagName('body')->item(0)-> // - getElementsByTagName('div')->item(0) //
              - , $tokens); - return $tokens; - } - -} - -/* - -Copyright 2007 Jeroen van der Meer - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -*/ - -class HTML5 { - private $data; - private $char; - private $EOF; - private $state; - private $tree; - private $token; - private $content_model; - private $escape = false; - private $entities = array('AElig;','AElig','AMP;','AMP','Aacute;','Aacute', - 'Acirc;','Acirc','Agrave;','Agrave','Alpha;','Aring;','Aring','Atilde;', - 'Atilde','Auml;','Auml','Beta;','COPY;','COPY','Ccedil;','Ccedil','Chi;', - 'Dagger;','Delta;','ETH;','ETH','Eacute;','Eacute','Ecirc;','Ecirc','Egrave;', - 'Egrave','Epsilon;','Eta;','Euml;','Euml','GT;','GT','Gamma;','Iacute;', - 'Iacute','Icirc;','Icirc','Igrave;','Igrave','Iota;','Iuml;','Iuml','Kappa;', - 'LT;','LT','Lambda;','Mu;','Ntilde;','Ntilde','Nu;','OElig;','Oacute;', - 'Oacute','Ocirc;','Ocirc','Ograve;','Ograve','Omega;','Omicron;','Oslash;', - 'Oslash','Otilde;','Otilde','Ouml;','Ouml','Phi;','Pi;','Prime;','Psi;', - 'QUOT;','QUOT','REG;','REG','Rho;','Scaron;','Sigma;','THORN;','THORN', - 'TRADE;','Tau;','Theta;','Uacute;','Uacute','Ucirc;','Ucirc','Ugrave;', - 'Ugrave','Upsilon;','Uuml;','Uuml','Xi;','Yacute;','Yacute','Yuml;','Zeta;', - 'aacute;','aacute','acirc;','acirc','acute;','acute','aelig;','aelig', - 'agrave;','agrave','alefsym;','alpha;','amp;','amp','and;','ang;','apos;', - 'aring;','aring','asymp;','atilde;','atilde','auml;','auml','bdquo;','beta;', - 'brvbar;','brvbar','bull;','cap;','ccedil;','ccedil','cedil;','cedil', - 'cent;','cent','chi;','circ;','clubs;','cong;','copy;','copy','crarr;', - 'cup;','curren;','curren','dArr;','dagger;','darr;','deg;','deg','delta;', - 'diams;','divide;','divide','eacute;','eacute','ecirc;','ecirc','egrave;', - 'egrave','empty;','emsp;','ensp;','epsilon;','equiv;','eta;','eth;','eth', - 'euml;','euml','euro;','exist;','fnof;','forall;','frac12;','frac12', - 'frac14;','frac14','frac34;','frac34','frasl;','gamma;','ge;','gt;','gt', - 'hArr;','harr;','hearts;','hellip;','iacute;','iacute','icirc;','icirc', - 'iexcl;','iexcl','igrave;','igrave','image;','infin;','int;','iota;', - 'iquest;','iquest','isin;','iuml;','iuml','kappa;','lArr;','lambda;','lang;', - 'laquo;','laquo','larr;','lceil;','ldquo;','le;','lfloor;','lowast;','loz;', - 'lrm;','lsaquo;','lsquo;','lt;','lt','macr;','macr','mdash;','micro;','micro', - 'middot;','middot','minus;','mu;','nabla;','nbsp;','nbsp','ndash;','ne;', - 'ni;','not;','not','notin;','nsub;','ntilde;','ntilde','nu;','oacute;', - 'oacute','ocirc;','ocirc','oelig;','ograve;','ograve','oline;','omega;', - 'omicron;','oplus;','or;','ordf;','ordf','ordm;','ordm','oslash;','oslash', - 'otilde;','otilde','otimes;','ouml;','ouml','para;','para','part;','permil;', - 'perp;','phi;','pi;','piv;','plusmn;','plusmn','pound;','pound','prime;', - 'prod;','prop;','psi;','quot;','quot','rArr;','radic;','rang;','raquo;', - 'raquo','rarr;','rceil;','rdquo;','real;','reg;','reg','rfloor;','rho;', - 'rlm;','rsaquo;','rsquo;','sbquo;','scaron;','sdot;','sect;','sect','shy;', - 'shy','sigma;','sigmaf;','sim;','spades;','sub;','sube;','sum;','sup1;', - 'sup1','sup2;','sup2','sup3;','sup3','sup;','supe;','szlig;','szlig','tau;', - 'there4;','theta;','thetasym;','thinsp;','thorn;','thorn','tilde;','times;', - 'times','trade;','uArr;','uacute;','uacute','uarr;','ucirc;','ucirc', - 'ugrave;','ugrave','uml;','uml','upsih;','upsilon;','uuml;','uuml','weierp;', - 'xi;','yacute;','yacute','yen;','yen','yuml;','yuml','zeta;','zwj;','zwnj;'); - - const PCDATA = 0; - const RCDATA = 1; - const CDATA = 2; - const PLAINTEXT = 3; - - const DOCTYPE = 0; - const STARTTAG = 1; - const ENDTAG = 2; - const COMMENT = 3; - const CHARACTR = 4; - const EOF = 5; - - public function __construct($data) { - - $this->data = $data; - $this->char = -1; - $this->EOF = strlen($data); - $this->tree = new HTML5TreeConstructer; - $this->content_model = self::PCDATA; - - $this->state = 'data'; - - while($this->state !== null) { - $this->{$this->state.'State'}(); - } - } - - public function save() { - return $this->tree->save(); - } - - private function char() { - return ($this->char < $this->EOF) - ? $this->data[$this->char] - : false; - } - - private function character($s, $l = 0) { - if($s + $l < $this->EOF) { - if($l === 0) { - return $this->data[$s]; - } else { - return substr($this->data, $s, $l); - } - } - } - - private function characters($char_class, $start) { - return preg_replace('#^(['.$char_class.']+).*#s', '\\1', substr($this->data, $start)); - } - - private function dataState() { - // Consume the next input character - $this->char++; - $char = $this->char(); - - if($char === '&' && ($this->content_model === self::PCDATA || $this->content_model === self::RCDATA)) { - /* U+0026 AMPERSAND (&) - When the content model flag is set to one of the PCDATA or RCDATA - states: switch to the entity data state. Otherwise: treat it as per - the "anything else" entry below. */ - $this->state = 'entityData'; - - } elseif($char === '-') { - /* If the content model flag is set to either the RCDATA state or - the CDATA state, and the escape flag is false, and there are at - least three characters before this one in the input stream, and the - last four characters in the input stream, including this one, are - U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+002D HYPHEN-MINUS, - and U+002D HYPHEN-MINUS (""), - set the escape flag to false. */ - if(($this->content_model === self::RCDATA || - $this->content_model === self::CDATA) && $this->escape === true && - $this->character($this->char, 3) === '-->') { - $this->escape = false; - } - - /* In any case, emit the input character as a character token. - Stay in the data state. */ - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => $char - )); - - } elseif($this->char === $this->EOF) { - /* EOF - Emit an end-of-file token. */ - $this->EOF(); - - } elseif($this->content_model === self::PLAINTEXT) { - /* When the content model flag is set to the PLAINTEXT state - THIS DIFFERS GREATLY FROM THE SPEC: Get the remaining characters of - the text and emit it as a character token. */ - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => substr($this->data, $this->char) - )); - - $this->EOF(); - - } else { - /* Anything else - THIS DIFFERS GREATLY FROM THE SPEC: Get as many character that - otherwise would also be treated as a character token and emit it - as a single character token. Stay in the data state. */ - $len = strcspn($this->data, '<&', $this->char); - $char = substr($this->data, $this->char, $len); - $this->char += $len - 1; - - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => $char - )); - - $this->state = 'data'; - } - } - - private function entityDataState() { - // Attempt to consume an entity. - $entity = $this->entity(); - - // If nothing is returned, emit a U+0026 AMPERSAND character token. - // Otherwise, emit the character token that was returned. - $char = (!$entity) ? '&' : $entity; - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => $char - )); - - // Finally, switch to the data state. - $this->state = 'data'; - } - - private function tagOpenState() { - switch($this->content_model) { - case self::RCDATA: - case self::CDATA: - /* If the next input character is a U+002F SOLIDUS (/) character, - consume it and switch to the close tag open state. If the next - input character is not a U+002F SOLIDUS (/) character, emit a - U+003C LESS-THAN SIGN character token and switch to the data - state to process the next input character. */ - if($this->character($this->char + 1) === '/') { - $this->char++; - $this->state = 'closeTagOpen'; - - } else { - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => '<' - )); - - $this->state = 'data'; - } - break; - - case self::PCDATA: - // If the content model flag is set to the PCDATA state - // Consume the next input character: - $this->char++; - $char = $this->char(); - - if($char === '!') { - /* U+0021 EXCLAMATION MARK (!) - Switch to the markup declaration open state. */ - $this->state = 'markupDeclarationOpen'; - - } elseif($char === '/') { - /* U+002F SOLIDUS (/) - Switch to the close tag open state. */ - $this->state = 'closeTagOpen'; - - } elseif(preg_match('/^[A-Za-z]$/', $char)) { - /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z - Create a new start tag token, set its tag name to the lowercase - version of the input character (add 0x0020 to the character's code - point), then switch to the tag name state. (Don't emit the token - yet; further details will be filled in before it is emitted.) */ - $this->token = array( - 'name' => strtolower($char), - 'type' => self::STARTTAG, - 'attr' => array() - ); - - $this->state = 'tagName'; - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Parse error. Emit a U+003C LESS-THAN SIGN character token and a - U+003E GREATER-THAN SIGN character token. Switch to the data state. */ - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => '<>' - )); - - $this->state = 'data'; - - } elseif($char === '?') { - /* U+003F QUESTION MARK (?) - Parse error. Switch to the bogus comment state. */ - $this->state = 'bogusComment'; - - } else { - /* Anything else - Parse error. Emit a U+003C LESS-THAN SIGN character token and - reconsume the current input character in the data state. */ - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => '<' - )); - - $this->char--; - $this->state = 'data'; - } - break; - } - } - - private function closeTagOpenState() { - $next_node = strtolower($this->characters('A-Za-z', $this->char + 1)); - $the_same = count($this->tree->stack) > 0 && $next_node === end($this->tree->stack)->nodeName; - - if(($this->content_model === self::RCDATA || $this->content_model === self::CDATA) && - (!$the_same || ($the_same && (!preg_match('/[\t\n\x0b\x0c >\/]/', - $this->character($this->char + 1 + strlen($next_node))) || $this->EOF === $this->char)))) { - /* If the content model flag is set to the RCDATA or CDATA states then - examine the next few characters. If they do not match the tag name of - the last start tag token emitted (case insensitively), or if they do but - they are not immediately followed by one of the following characters: - * U+0009 CHARACTER TABULATION - * U+000A LINE FEED (LF) - * U+000B LINE TABULATION - * U+000C FORM FEED (FF) - * U+0020 SPACE - * U+003E GREATER-THAN SIGN (>) - * U+002F SOLIDUS (/) - * EOF - ...then there is a parse error. Emit a U+003C LESS-THAN SIGN character - token, a U+002F SOLIDUS character token, and switch to the data state - to process the next input character. */ - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => 'state = 'data'; - - } else { - /* Otherwise, if the content model flag is set to the PCDATA state, - or if the next few characters do match that tag name, consume the - next input character: */ - $this->char++; - $char = $this->char(); - - if(preg_match('/^[A-Za-z]$/', $char)) { - /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z - Create a new end tag token, set its tag name to the lowercase version - of the input character (add 0x0020 to the character's code point), then - switch to the tag name state. (Don't emit the token yet; further details - will be filled in before it is emitted.) */ - $this->token = array( - 'name' => strtolower($char), - 'type' => self::ENDTAG - ); - - $this->state = 'tagName'; - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Parse error. Switch to the data state. */ - $this->state = 'data'; - - } elseif($this->char === $this->EOF) { - /* EOF - Parse error. Emit a U+003C LESS-THAN SIGN character token and a U+002F - SOLIDUS character token. Reconsume the EOF character in the data state. */ - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => 'char--; - $this->state = 'data'; - - } else { - /* Parse error. Switch to the bogus comment state. */ - $this->state = 'bogusComment'; - } - } - } - - private function tagNameState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Switch to the before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the EOF - character in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } elseif($char === '/') { - /* U+002F SOLIDUS (/) - Parse error unless this is a permitted slash. Switch to the before - attribute name state. */ - $this->state = 'beforeAttributeName'; - - } else { - /* Anything else - Append the current input character to the current tag token's tag name. - Stay in the tag name state. */ - $this->token['name'] .= strtolower($char); - $this->state = 'tagName'; - } - } - - private function beforeAttributeNameState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Stay in the before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif($char === '/') { - /* U+002F SOLIDUS (/) - Parse error unless this is a permitted slash. Stay in the before - attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the EOF - character in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } else { - /* Anything else - Start a new attribute in the current tag token. Set that attribute's - name to the current input character, and its value to the empty string. - Switch to the attribute name state. */ - $this->token['attr'][] = array( - 'name' => strtolower($char), - 'value' => null - ); - - $this->state = 'attributeName'; - } - } - - private function attributeNameState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Stay in the before attribute name state. */ - $this->state = 'afterAttributeName'; - - } elseif($char === '=') { - /* U+003D EQUALS SIGN (=) - Switch to the before attribute value state. */ - $this->state = 'beforeAttributeValue'; - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif($char === '/' && $this->character($this->char + 1) !== '>') { - /* U+002F SOLIDUS (/) - Parse error unless this is a permitted slash. Switch to the before - attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the EOF - character in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } else { - /* Anything else - Append the current input character to the current attribute's name. - Stay in the attribute name state. */ - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['name'] .= strtolower($char); - - $this->state = 'attributeName'; - } - } - - private function afterAttributeNameState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Stay in the after attribute name state. */ - $this->state = 'afterAttributeName'; - - } elseif($char === '=') { - /* U+003D EQUALS SIGN (=) - Switch to the before attribute value state. */ - $this->state = 'beforeAttributeValue'; - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif($char === '/' && $this->character($this->char + 1) !== '>') { - /* U+002F SOLIDUS (/) - Parse error unless this is a permitted slash. Switch to the - before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the EOF - character in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } else { - /* Anything else - Start a new attribute in the current tag token. Set that attribute's - name to the current input character, and its value to the empty string. - Switch to the attribute name state. */ - $this->token['attr'][] = array( - 'name' => strtolower($char), - 'value' => null - ); - - $this->state = 'attributeName'; - } - } - - private function beforeAttributeValueState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Stay in the before attribute value state. */ - $this->state = 'beforeAttributeValue'; - - } elseif($char === '"') { - /* U+0022 QUOTATION MARK (") - Switch to the attribute value (double-quoted) state. */ - $this->state = 'attributeValueDoubleQuoted'; - - } elseif($char === '&') { - /* U+0026 AMPERSAND (&) - Switch to the attribute value (unquoted) state and reconsume - this input character. */ - $this->char--; - $this->state = 'attributeValueUnquoted'; - - } elseif($char === '\'') { - /* U+0027 APOSTROPHE (') - Switch to the attribute value (single-quoted) state. */ - $this->state = 'attributeValueSingleQuoted'; - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } else { - /* Anything else - Append the current input character to the current attribute's value. - Switch to the attribute value (unquoted) state. */ - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['value'] .= $char; - - $this->state = 'attributeValueUnquoted'; - } - } - - private function attributeValueDoubleQuotedState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if($char === '"') { - /* U+0022 QUOTATION MARK (") - Switch to the before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($char === '&') { - /* U+0026 AMPERSAND (&) - Switch to the entity in attribute value state. */ - $this->entityInAttributeValueState('double'); - - } elseif($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the character - in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } else { - /* Anything else - Append the current input character to the current attribute's value. - Stay in the attribute value (double-quoted) state. */ - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['value'] .= $char; - - $this->state = 'attributeValueDoubleQuoted'; - } - } - - private function attributeValueSingleQuotedState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if($char === '\'') { - /* U+0022 QUOTATION MARK (') - Switch to the before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($char === '&') { - /* U+0026 AMPERSAND (&) - Switch to the entity in attribute value state. */ - $this->entityInAttributeValueState('single'); - - } elseif($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the character - in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } else { - /* Anything else - Append the current input character to the current attribute's value. - Stay in the attribute value (single-quoted) state. */ - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['value'] .= $char; - - $this->state = 'attributeValueSingleQuoted'; - } - } - - private function attributeValueUnquotedState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Switch to the before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($char === '&') { - /* U+0026 AMPERSAND (&) - Switch to the entity in attribute value state. */ - $this->entityInAttributeValueState(); - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } else { - /* Anything else - Append the current input character to the current attribute's value. - Stay in the attribute value (unquoted) state. */ - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['value'] .= $char; - - $this->state = 'attributeValueUnquoted'; - } - } - - private function entityInAttributeValueState() { - // Attempt to consume an entity. - $entity = $this->entity(); - - // If nothing is returned, append a U+0026 AMPERSAND character to the - // current attribute's value. Otherwise, emit the character token that - // was returned. - $char = (!$entity) - ? '&' - : $entity; - - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['value'] .= $char; - } - - private function bogusCommentState() { - /* Consume every character up to the first U+003E GREATER-THAN SIGN - character (>) or the end of the file (EOF), whichever comes first. Emit - a comment token whose data is the concatenation of all the characters - starting from and including the character that caused the state machine - to switch into the bogus comment state, up to and including the last - consumed character before the U+003E character, if any, or up to the - end of the file otherwise. (If the comment was started by the end of - the file (EOF), the token is empty.) */ - $data = $this->characters('^>', $this->char); - $this->emitToken(array( - 'data' => $data, - 'type' => self::COMMENT - )); - - $this->char += strlen($data); - - /* Switch to the data state. */ - $this->state = 'data'; - - /* If the end of the file was reached, reconsume the EOF character. */ - if($this->char === $this->EOF) { - $this->char = $this->EOF - 1; - } - } - - private function markupDeclarationOpenState() { - /* If the next two characters are both U+002D HYPHEN-MINUS (-) - characters, consume those two characters, create a comment token whose - data is the empty string, and switch to the comment state. */ - if($this->character($this->char + 1, 2) === '--') { - $this->char += 2; - $this->state = 'comment'; - $this->token = array( - 'data' => null, - 'type' => self::COMMENT - ); - - /* Otherwise if the next seven chacacters are a case-insensitive match - for the word "DOCTYPE", then consume those characters and switch to the - DOCTYPE state. */ - } elseif(strtolower($this->character($this->char + 1, 7)) === 'doctype') { - $this->char += 7; - $this->state = 'doctype'; - - /* Otherwise, is is a parse error. Switch to the bogus comment state. - The next character that is consumed, if any, is the first character - that will be in the comment. */ - } else { - $this->char++; - $this->state = 'bogusComment'; - } - } - - private function commentState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - /* U+002D HYPHEN-MINUS (-) */ - if($char === '-') { - /* Switch to the comment dash state */ - $this->state = 'commentDash'; - - /* EOF */ - } elseif($this->char === $this->EOF) { - /* Parse error. Emit the comment token. Reconsume the EOF character - in the data state. */ - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - /* Anything else */ - } else { - /* Append the input character to the comment token's data. Stay in - the comment state. */ - $this->token['data'] .= $char; - } - } - - private function commentDashState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - /* U+002D HYPHEN-MINUS (-) */ - if($char === '-') { - /* Switch to the comment end state */ - $this->state = 'commentEnd'; - - /* EOF */ - } elseif($this->char === $this->EOF) { - /* Parse error. Emit the comment token. Reconsume the EOF character - in the data state. */ - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - /* Anything else */ - } else { - /* Append a U+002D HYPHEN-MINUS (-) character and the input - character to the comment token's data. Switch to the comment state. */ - $this->token['data'] .= '-'.$char; - $this->state = 'comment'; - } - } - - private function commentEndState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if($char === '>') { - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif($char === '-') { - $this->token['data'] .= '-'; - - } elseif($this->char === $this->EOF) { - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - } else { - $this->token['data'] .= '--'.$char; - $this->state = 'comment'; - } - } - - private function doctypeState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - $this->state = 'beforeDoctypeName'; - - } else { - $this->char--; - $this->state = 'beforeDoctypeName'; - } - } - - private function beforeDoctypeNameState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - // Stay in the before DOCTYPE name state. - - } elseif(preg_match('/^[a-z]$/', $char)) { - $this->token = array( - 'name' => strtoupper($char), - 'type' => self::DOCTYPE, - 'error' => true - ); - - $this->state = 'doctypeName'; - - } elseif($char === '>') { - $this->emitToken(array( - 'name' => null, - 'type' => self::DOCTYPE, - 'error' => true - )); - - $this->state = 'data'; - - } elseif($this->char === $this->EOF) { - $this->emitToken(array( - 'name' => null, - 'type' => self::DOCTYPE, - 'error' => true - )); - - $this->char--; - $this->state = 'data'; - - } else { - $this->token = array( - 'name' => $char, - 'type' => self::DOCTYPE, - 'error' => true - ); - - $this->state = 'doctypeName'; - } - } - - private function doctypeNameState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - $this->state = 'AfterDoctypeName'; - - } elseif($char === '>') { - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif(preg_match('/^[a-z]$/', $char)) { - $this->token['name'] .= strtoupper($char); - - } elseif($this->char === $this->EOF) { - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - } else { - $this->token['name'] .= $char; - } - - $this->token['error'] = ($this->token['name'] === 'HTML') - ? false - : true; - } - - private function afterDoctypeNameState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - // Stay in the DOCTYPE name state. - - } elseif($char === '>') { - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif($this->char === $this->EOF) { - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - } else { - $this->token['error'] = true; - $this->state = 'bogusDoctype'; - } - } - - private function bogusDoctypeState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if($char === '>') { - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif($this->char === $this->EOF) { - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - } else { - // Stay in the bogus DOCTYPE state. - } - } - - private function entity() { - $start = $this->char; - - // This section defines how to consume an entity. This definition is - // used when parsing entities in text and in attributes. - - // The behaviour depends on the identity of the next character (the - // one immediately after the U+0026 AMPERSAND character): - - switch($this->character($this->char + 1)) { - // U+0023 NUMBER SIGN (#) - case '#': - - // The behaviour further depends on the character after the - // U+0023 NUMBER SIGN: - switch($this->character($this->char + 1)) { - // U+0078 LATIN SMALL LETTER X - // U+0058 LATIN CAPITAL LETTER X - case 'x': - case 'X': - // Follow the steps below, but using the range of - // characters U+0030 DIGIT ZERO through to U+0039 DIGIT - // NINE, U+0061 LATIN SMALL LETTER A through to U+0066 - // LATIN SMALL LETTER F, and U+0041 LATIN CAPITAL LETTER - // A, through to U+0046 LATIN CAPITAL LETTER F (in other - // words, 0-9, A-F, a-f). - $char = 1; - $char_class = '0-9A-Fa-f'; - break; - - // Anything else - default: - // Follow the steps below, but using the range of - // characters U+0030 DIGIT ZERO through to U+0039 DIGIT - // NINE (i.e. just 0-9). - $char = 0; - $char_class = '0-9'; - break; - } - - // Consume as many characters as match the range of characters - // given above. - $this->char++; - $e_name = $this->characters($char_class, $this->char + $char + 1); - $entity = $this->character($start, $this->char); - $cond = strlen($e_name) > 0; - - // The rest of the parsing happens bellow. - break; - - // Anything else - default: - // Consume the maximum number of characters possible, with the - // consumed characters case-sensitively matching one of the - // identifiers in the first column of the entities table. - $e_name = $this->characters('0-9A-Za-z;', $this->char + 1); - $len = strlen($e_name); - - for($c = 1; $c <= $len; $c++) { - $id = substr($e_name, 0, $c); - $this->char++; - - if(in_array($id, $this->entities)) { - if ($e_name[$c-1] !== ';') { - if ($c < $len && $e_name[$c] == ';') { - $this->char++; // consume extra semicolon - } - } - $entity = $id; - break; - } - } - - $cond = isset($entity); - // The rest of the parsing happens bellow. - break; - } - - if(!$cond) { - // If no match can be made, then this is a parse error. No - // characters are consumed, and nothing is returned. - $this->char = $start; - return false; - } - - // Return a character token for the character corresponding to the - // entity name (as given by the second column of the entities table). - return html_entity_decode('&'.$entity.';', ENT_QUOTES, 'UTF-8'); - } - - private function emitToken($token) { - $emit = $this->tree->emitToken($token); - - if(is_int($emit)) { - $this->content_model = $emit; - - } elseif($token['type'] === self::ENDTAG) { - $this->content_model = self::PCDATA; - } - } - - private function EOF() { - $this->state = null; - $this->tree->emitToken(array( - 'type' => self::EOF - )); - } -} - -class HTML5TreeConstructer { - public $stack = array(); - - private $phase; - private $mode; - private $dom; - private $foster_parent = null; - private $a_formatting = array(); - - private $head_pointer = null; - private $form_pointer = null; - - private $scoping = array('button','caption','html','marquee','object','table','td','th'); - private $formatting = array('a','b','big','em','font','i','nobr','s','small','strike','strong','tt','u'); - private $special = array('address','area','base','basefont','bgsound', - 'blockquote','body','br','center','col','colgroup','dd','dir','div','dl', - 'dt','embed','fieldset','form','frame','frameset','h1','h2','h3','h4','h5', - 'h6','head','hr','iframe','image','img','input','isindex','li','link', - 'listing','menu','meta','noembed','noframes','noscript','ol','optgroup', - 'option','p','param','plaintext','pre','script','select','spacer','style', - 'tbody','textarea','tfoot','thead','title','tr','ul','wbr'); - - // The different phases. - const INIT_PHASE = 0; - const ROOT_PHASE = 1; - const MAIN_PHASE = 2; - const END_PHASE = 3; - - // The different insertion modes for the main phase. - const BEFOR_HEAD = 0; - const IN_HEAD = 1; - const AFTER_HEAD = 2; - const IN_BODY = 3; - const IN_TABLE = 4; - const IN_CAPTION = 5; - const IN_CGROUP = 6; - const IN_TBODY = 7; - const IN_ROW = 8; - const IN_CELL = 9; - const IN_SELECT = 10; - const AFTER_BODY = 11; - const IN_FRAME = 12; - const AFTR_FRAME = 13; - - // The different types of elements. - const SPECIAL = 0; - const SCOPING = 1; - const FORMATTING = 2; - const PHRASING = 3; - - const MARKER = 0; - - public function __construct() { - $this->phase = self::INIT_PHASE; - $this->mode = self::BEFOR_HEAD; - $this->dom = new DOMDocument; - - $this->dom->encoding = 'UTF-8'; - $this->dom->preserveWhiteSpace = true; - $this->dom->substituteEntities = true; - $this->dom->strictErrorChecking = false; - } - - // Process tag tokens - public function emitToken($token) { - switch($this->phase) { - case self::INIT_PHASE: return $this->initPhase($token); break; - case self::ROOT_PHASE: return $this->rootElementPhase($token); break; - case self::MAIN_PHASE: return $this->mainPhase($token); break; - case self::END_PHASE : return $this->trailingEndPhase($token); break; - } - } - - private function initPhase($token) { - /* Initially, the tree construction stage must handle each token - emitted from the tokenisation stage as follows: */ - - /* A DOCTYPE token that is marked as being in error - A comment token - A start tag token - An end tag token - A character token that is not one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE - An end-of-file token */ - if((isset($token['error']) && $token['error']) || - $token['type'] === HTML5::COMMENT || - $token['type'] === HTML5::STARTTAG || - $token['type'] === HTML5::ENDTAG || - $token['type'] === HTML5::EOF || - ($token['type'] === HTML5::CHARACTR && isset($token['data']) && - !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']))) { - /* This specification does not define how to handle this case. In - particular, user agents may ignore the entirety of this specification - altogether for such documents, and instead invoke special parse modes - with a greater emphasis on backwards compatibility. */ - - $this->phase = self::ROOT_PHASE; - return $this->rootElementPhase($token); - - /* A DOCTYPE token marked as being correct */ - } elseif(isset($token['error']) && !$token['error']) { - /* Append a DocumentType node to the Document node, with the name - attribute set to the name given in the DOCTYPE token (which will be - "HTML"), and the other attributes specific to DocumentType objects - set to null, empty lists, or the empty string as appropriate. */ - $doctype = new DOMDocumentType(null, null, 'HTML'); - - /* Then, switch to the root element phase of the tree construction - stage. */ - $this->phase = self::ROOT_PHASE; - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - } elseif(isset($token['data']) && preg_match('/^[\t\n\x0b\x0c ]+$/', - $token['data'])) { - /* Append that character to the Document node. */ - $text = $this->dom->createTextNode($token['data']); - $this->dom->appendChild($text); - } - } - - private function rootElementPhase($token) { - /* After the initial phase, as each token is emitted from the tokenisation - stage, it must be processed as described in this section. */ - - /* A DOCTYPE token */ - if($token['type'] === HTML5::DOCTYPE) { - // Parse error. Ignore the token. - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the Document object with the data - attribute set to the data given in the comment token. */ - $comment = $this->dom->createComment($token['data']); - $this->dom->appendChild($comment); - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - } elseif($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Append that character to the Document node. */ - $text = $this->dom->createTextNode($token['data']); - $this->dom->appendChild($text); - - /* A character token that is not one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED - (FF), or U+0020 SPACE - A start tag token - An end tag token - An end-of-file token */ - } elseif(($token['type'] === HTML5::CHARACTR && - !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || - $token['type'] === HTML5::STARTTAG || - $token['type'] === HTML5::ENDTAG || - $token['type'] === HTML5::EOF) { - /* Create an HTMLElement node with the tag name html, in the HTML - namespace. Append it to the Document object. Switch to the main - phase and reprocess the current token. */ - $html = $this->dom->createElement('html'); - $this->dom->appendChild($html); - $this->stack[] = $html; - - $this->phase = self::MAIN_PHASE; - return $this->mainPhase($token); - } - } - - private function mainPhase($token) { - /* Tokens in the main phase must be handled as follows: */ - - /* A DOCTYPE token */ - if($token['type'] === HTML5::DOCTYPE) { - // Parse error. Ignore the token. - - /* A start tag token with the tag name "html" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'html') { - /* If this start tag token was not the first start tag token, then - it is a parse error. */ - - /* For each attribute on the token, check to see if the attribute - is already present on the top element of the stack of open elements. - If it is not, add the attribute and its corresponding value to that - element. */ - foreach($token['attr'] as $attr) { - if(!$this->stack[0]->hasAttribute($attr['name'])) { - $this->stack[0]->setAttribute($attr['name'], $attr['value']); - } - } - - /* An end-of-file token */ - } elseif($token['type'] === HTML5::EOF) { - /* Generate implied end tags. */ - $this->generateImpliedEndTags(); - - /* Anything else. */ - } else { - /* Depends on the insertion mode: */ - switch($this->mode) { - case self::BEFOR_HEAD: return $this->beforeHead($token); break; - case self::IN_HEAD: return $this->inHead($token); break; - case self::AFTER_HEAD: return $this->afterHead($token); break; - case self::IN_BODY: return $this->inBody($token); break; - case self::IN_TABLE: return $this->inTable($token); break; - case self::IN_CAPTION: return $this->inCaption($token); break; - case self::IN_CGROUP: return $this->inColumnGroup($token); break; - case self::IN_TBODY: return $this->inTableBody($token); break; - case self::IN_ROW: return $this->inRow($token); break; - case self::IN_CELL: return $this->inCell($token); break; - case self::IN_SELECT: return $this->inSelect($token); break; - case self::AFTER_BODY: return $this->afterBody($token); break; - case self::IN_FRAME: return $this->inFrameset($token); break; - case self::AFTR_FRAME: return $this->afterFrameset($token); break; - case self::END_PHASE: return $this->trailingEndPhase($token); break; - } - } - } - - private function beforeHead($token) { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - if($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Append the character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data attribute - set to the data given in the comment token. */ - $this->insertComment($token['data']); - - /* A start tag token with the tag name "head" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') { - /* Create an element for the token, append the new element to the - current node and push it onto the stack of open elements. */ - $element = $this->insertElement($token); - - /* Set the head element pointer to this new element node. */ - $this->head_pointer = $element; - - /* Change the insertion mode to "in head". */ - $this->mode = self::IN_HEAD; - - /* A start tag token whose tag name is one of: "base", "link", "meta", - "script", "style", "title". Or an end tag with the tag name "html". - Or a character token that is not one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE. Or any other start tag token */ - } elseif($token['type'] === HTML5::STARTTAG || - ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') || - ($token['type'] === HTML5::CHARACTR && !preg_match('/^[\t\n\x0b\x0c ]$/', - $token['data']))) { - /* Act as if a start tag token with the tag name "head" and no - attributes had been seen, then reprocess the current token. */ - $this->beforeHead(array( - 'name' => 'head', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - return $this->inHead($token); - - /* Any other end tag */ - } elseif($token['type'] === HTML5::ENDTAG) { - /* Parse error. Ignore the token. */ - } - } - - private function inHead($token) { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE. - - THIS DIFFERS FROM THE SPEC: If the current node is either a title, style - or script element, append the character to the current node regardless - of its content. */ - if(($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || ( - $token['type'] === HTML5::CHARACTR && in_array(end($this->stack)->nodeName, - array('title', 'style', 'script')))) { - /* Append the character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data attribute - set to the data given in the comment token. */ - $this->insertComment($token['data']); - - } elseif($token['type'] === HTML5::ENDTAG && - in_array($token['name'], array('title', 'style', 'script'))) { - array_pop($this->stack); - return HTML5::PCDATA; - - /* A start tag with the tag name "title" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'title') { - /* Create an element for the token and append the new element to the - node pointed to by the head element pointer, or, if that is null - (innerHTML case), to the current node. */ - if($this->head_pointer !== null) { - $element = $this->insertElement($token, false); - $this->head_pointer->appendChild($element); - - } else { - $element = $this->insertElement($token); - } - - /* Switch the tokeniser's content model flag to the RCDATA state. */ - return HTML5::RCDATA; - - /* A start tag with the tag name "style" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'style') { - /* Create an element for the token and append the new element to the - node pointed to by the head element pointer, or, if that is null - (innerHTML case), to the current node. */ - if($this->head_pointer !== null) { - $element = $this->insertElement($token, false); - $this->head_pointer->appendChild($element); - - } else { - $this->insertElement($token); - } - - /* Switch the tokeniser's content model flag to the CDATA state. */ - return HTML5::CDATA; - - /* A start tag with the tag name "script" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'script') { - /* Create an element for the token. */ - $element = $this->insertElement($token, false); - $this->head_pointer->appendChild($element); - - /* Switch the tokeniser's content model flag to the CDATA state. */ - return HTML5::CDATA; - - /* A start tag with the tag name "base", "link", or "meta" */ - } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('base', 'link', 'meta'))) { - /* Create an element for the token and append the new element to the - node pointed to by the head element pointer, or, if that is null - (innerHTML case), to the current node. */ - if($this->head_pointer !== null) { - $element = $this->insertElement($token, false); - $this->head_pointer->appendChild($element); - array_pop($this->stack); - - } else { - $this->insertElement($token); - } - - /* An end tag with the tag name "head" */ - } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'head') { - /* If the current node is a head element, pop the current node off - the stack of open elements. */ - if($this->head_pointer->isSameNode(end($this->stack))) { - array_pop($this->stack); - - /* Otherwise, this is a parse error. */ - } else { - // k - } - - /* Change the insertion mode to "after head". */ - $this->mode = self::AFTER_HEAD; - - /* A start tag with the tag name "head" or an end tag except "html". */ - } elseif(($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') || - ($token['type'] === HTML5::ENDTAG && $token['name'] !== 'html')) { - // Parse error. Ignore the token. - - /* Anything else */ - } else { - /* If the current node is a head element, act as if an end tag - token with the tag name "head" had been seen. */ - if($this->head_pointer->isSameNode(end($this->stack))) { - $this->inHead(array( - 'name' => 'head', - 'type' => HTML5::ENDTAG - )); - - /* Otherwise, change the insertion mode to "after head". */ - } else { - $this->mode = self::AFTER_HEAD; - } - - /* Then, reprocess the current token. */ - return $this->afterHead($token); - } - } - - private function afterHead($token) { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - if($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Append the character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data attribute - set to the data given in the comment token. */ - $this->insertComment($token['data']); - - /* A start tag token with the tag name "body" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'body') { - /* Insert a body element for the token. */ - $this->insertElement($token); - - /* Change the insertion mode to "in body". */ - $this->mode = self::IN_BODY; - - /* A start tag token with the tag name "frameset" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'frameset') { - /* Insert a frameset element for the token. */ - $this->insertElement($token); - - /* Change the insertion mode to "in frameset". */ - $this->mode = self::IN_FRAME; - - /* A start tag token whose tag name is one of: "base", "link", "meta", - "script", "style", "title" */ - } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('base', 'link', 'meta', 'script', 'style', 'title'))) { - /* Parse error. Switch the insertion mode back to "in head" and - reprocess the token. */ - $this->mode = self::IN_HEAD; - return $this->inHead($token); - - /* Anything else */ - } else { - /* Act as if a start tag token with the tag name "body" and no - attributes had been seen, and then reprocess the current token. */ - $this->afterHead(array( - 'name' => 'body', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - return $this->inBody($token); - } - } - - private function inBody($token) { - /* Handle the token as follows: */ - - switch($token['type']) { - /* A character token */ - case HTML5::CHARACTR: - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Append the token's character to the current node. */ - $this->insertText($token['data']); - break; - - /* A comment token */ - case HTML5::COMMENT: - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $this->insertComment($token['data']); - break; - - case HTML5::STARTTAG: - switch($token['name']) { - /* A start tag token whose tag name is one of: "script", - "style" */ - case 'script': case 'style': - /* Process the token as if the insertion mode had been "in - head". */ - return $this->inHead($token); - break; - - /* A start tag token whose tag name is one of: "base", "link", - "meta", "title" */ - case 'base': case 'link': case 'meta': case 'title': - /* Parse error. Process the token as if the insertion mode - had been "in head". */ - return $this->inHead($token); - break; - - /* A start tag token with the tag name "body" */ - case 'body': - /* Parse error. If the second element on the stack of open - elements is not a body element, or, if the stack of open - elements has only one node on it, then ignore the token. - (innerHTML case) */ - if(count($this->stack) === 1 || $this->stack[1]->nodeName !== 'body') { - // Ignore - - /* Otherwise, for each attribute on the token, check to see - if the attribute is already present on the body element (the - second element) on the stack of open elements. If it is not, - add the attribute and its corresponding value to that - element. */ - } else { - foreach($token['attr'] as $attr) { - if(!$this->stack[1]->hasAttribute($attr['name'])) { - $this->stack[1]->setAttribute($attr['name'], $attr['value']); - } - } - } - break; - - /* A start tag whose tag name is one of: "address", - "blockquote", "center", "dir", "div", "dl", "fieldset", - "listing", "menu", "ol", "p", "ul" */ - case 'address': case 'blockquote': case 'center': case 'dir': - case 'div': case 'dl': case 'fieldset': case 'listing': - case 'menu': case 'ol': case 'p': case 'ul': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been - seen. */ - if($this->elementInScope('p')) { - $this->emitToken(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - break; - - /* A start tag whose tag name is "form" */ - case 'form': - /* If the form element pointer is not null, ignore the - token with a parse error. */ - if($this->form_pointer !== null) { - // Ignore. - - /* Otherwise: */ - } else { - /* If the stack of open elements has a p element in - scope, then act as if an end tag with the tag name p - had been seen. */ - if($this->elementInScope('p')) { - $this->emitToken(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - } - - /* Insert an HTML element for the token, and set the - form element pointer to point to the element created. */ - $element = $this->insertElement($token); - $this->form_pointer = $element; - } - break; - - /* A start tag whose tag name is "li", "dd" or "dt" */ - case 'li': case 'dd': case 'dt': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been - seen. */ - if($this->elementInScope('p')) { - $this->emitToken(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - } - - $stack_length = count($this->stack) - 1; - - for($n = $stack_length; 0 <= $n; $n--) { - /* 1. Initialise node to be the current node (the - bottommost node of the stack). */ - $stop = false; - $node = $this->stack[$n]; - $cat = $this->getElementCategory($node->tagName); - - /* 2. If node is an li, dd or dt element, then pop all - the nodes from the current node up to node, including - node, then stop this algorithm. */ - if($token['name'] === $node->tagName || ($token['name'] !== 'li' - && ($node->tagName === 'dd' || $node->tagName === 'dt'))) { - for($x = $stack_length; $x >= $n ; $x--) { - array_pop($this->stack); - } - - break; - } - - /* 3. If node is not in the formatting category, and is - not in the phrasing category, and is not an address or - div element, then stop this algorithm. */ - if($cat !== self::FORMATTING && $cat !== self::PHRASING && - $node->tagName !== 'address' && $node->tagName !== 'div') { - break; - } - } - - /* Finally, insert an HTML element with the same tag - name as the token's. */ - $this->insertElement($token); - break; - - /* A start tag token whose tag name is "plaintext" */ - case 'plaintext': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been - seen. */ - if($this->elementInScope('p')) { - $this->emitToken(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - return HTML5::PLAINTEXT; - break; - - /* A start tag whose tag name is one of: "h1", "h2", "h3", "h4", - "h5", "h6" */ - case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been seen. */ - if($this->elementInScope('p')) { - $this->emitToken(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - } - - /* If the stack of open elements has in scope an element whose - tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then - this is a parse error; pop elements from the stack until an - element with one of those tag names has been popped from the - stack. */ - while($this->elementInScope(array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'))) { - array_pop($this->stack); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - break; - - /* A start tag whose tag name is "a" */ - case 'a': - /* If the list of active formatting elements contains - an element whose tag name is "a" between the end of the - list and the last marker on the list (or the start of - the list if there is no marker on the list), then this - is a parse error; act as if an end tag with the tag name - "a" had been seen, then remove that element from the list - of active formatting elements and the stack of open - elements if the end tag didn't already remove it (it - might not have if the element is not in table scope). */ - $leng = count($this->a_formatting); - - for($n = $leng - 1; $n >= 0; $n--) { - if($this->a_formatting[$n] === self::MARKER) { - break; - - } elseif($this->a_formatting[$n]->nodeName === 'a') { - $this->emitToken(array( - 'name' => 'a', - 'type' => HTML5::ENDTAG - )); - break; - } - } - - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $el = $this->insertElement($token); - - /* Add that element to the list of active formatting - elements. */ - $this->a_formatting[] = $el; - break; - - /* A start tag whose tag name is one of: "b", "big", "em", "font", - "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ - case 'b': case 'big': case 'em': case 'font': case 'i': - case 'nobr': case 's': case 'small': case 'strike': - case 'strong': case 'tt': case 'u': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $el = $this->insertElement($token); - - /* Add that element to the list of active formatting - elements. */ - $this->a_formatting[] = $el; - break; - - /* A start tag token whose tag name is "button" */ - case 'button': - /* If the stack of open elements has a button element in scope, - then this is a parse error; act as if an end tag with the tag - name "button" had been seen, then reprocess the token. (We don't - do that. Unnecessary.) */ - if($this->elementInScope('button')) { - $this->inBody(array( - 'name' => 'button', - 'type' => HTML5::ENDTAG - )); - } - - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Insert a marker at the end of the list of active - formatting elements. */ - $this->a_formatting[] = self::MARKER; - break; - - /* A start tag token whose tag name is one of: "marquee", "object" */ - case 'marquee': case 'object': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Insert a marker at the end of the list of active - formatting elements. */ - $this->a_formatting[] = self::MARKER; - break; - - /* A start tag token whose tag name is "xmp" */ - case 'xmp': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Switch the content model flag to the CDATA state. */ - return HTML5::CDATA; - break; - - /* A start tag whose tag name is "table" */ - case 'table': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been seen. */ - if($this->elementInScope('p')) { - $this->emitToken(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Change the insertion mode to "in table". */ - $this->mode = self::IN_TABLE; - break; - - /* A start tag whose tag name is one of: "area", "basefont", - "bgsound", "br", "embed", "img", "param", "spacer", "wbr" */ - case 'area': case 'basefont': case 'bgsound': case 'br': - case 'embed': case 'img': case 'param': case 'spacer': - case 'wbr': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Immediately pop the current node off the stack of open elements. */ - array_pop($this->stack); - break; - - /* A start tag whose tag name is "hr" */ - case 'hr': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been seen. */ - if($this->elementInScope('p')) { - $this->emitToken(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Immediately pop the current node off the stack of open elements. */ - array_pop($this->stack); - break; - - /* A start tag whose tag name is "image" */ - case 'image': - /* Parse error. Change the token's tag name to "img" and - reprocess it. (Don't ask.) */ - $token['name'] = 'img'; - return $this->inBody($token); - break; - - /* A start tag whose tag name is "input" */ - case 'input': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an input element for the token. */ - $element = $this->insertElement($token, false); - - /* If the form element pointer is not null, then associate the - input element with the form element pointed to by the form - element pointer. */ - $this->form_pointer !== null - ? $this->form_pointer->appendChild($element) - : end($this->stack)->appendChild($element); - - /* Pop that input element off the stack of open elements. */ - array_pop($this->stack); - break; - - /* A start tag whose tag name is "isindex" */ - case 'isindex': - /* Parse error. */ - // w/e - - /* If the form element pointer is not null, - then ignore the token. */ - if($this->form_pointer === null) { - /* Act as if a start tag token with the tag name "form" had - been seen. */ - $this->inBody(array( - 'name' => 'body', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - /* Act as if a start tag token with the tag name "hr" had - been seen. */ - $this->inBody(array( - 'name' => 'hr', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - /* Act as if a start tag token with the tag name "p" had - been seen. */ - $this->inBody(array( - 'name' => 'p', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - /* Act as if a start tag token with the tag name "label" - had been seen. */ - $this->inBody(array( - 'name' => 'label', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - /* Act as if a stream of character tokens had been seen. */ - $this->insertText('This is a searchable index. '. - 'Insert your search keywords here: '); - - /* Act as if a start tag token with the tag name "input" - had been seen, with all the attributes from the "isindex" - token, except with the "name" attribute set to the value - "isindex" (ignoring any explicit "name" attribute). */ - $attr = $token['attr']; - $attr[] = array('name' => 'name', 'value' => 'isindex'); - - $this->inBody(array( - 'name' => 'input', - 'type' => HTML5::STARTTAG, - 'attr' => $attr - )); - - /* Act as if a stream of character tokens had been seen - (see below for what they should say). */ - $this->insertText('This is a searchable index. '. - 'Insert your search keywords here: '); - - /* Act as if an end tag token with the tag name "label" - had been seen. */ - $this->inBody(array( - 'name' => 'label', - 'type' => HTML5::ENDTAG - )); - - /* Act as if an end tag token with the tag name "p" had - been seen. */ - $this->inBody(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - - /* Act as if a start tag token with the tag name "hr" had - been seen. */ - $this->inBody(array( - 'name' => 'hr', - 'type' => HTML5::ENDTAG - )); - - /* Act as if an end tag token with the tag name "form" had - been seen. */ - $this->inBody(array( - 'name' => 'form', - 'type' => HTML5::ENDTAG - )); - } - break; - - /* A start tag whose tag name is "textarea" */ - case 'textarea': - $this->insertElement($token); - - /* Switch the tokeniser's content model flag to the - RCDATA state. */ - return HTML5::RCDATA; - break; - - /* A start tag whose tag name is one of: "iframe", "noembed", - "noframes" */ - case 'iframe': case 'noembed': case 'noframes': - $this->insertElement($token); - - /* Switch the tokeniser's content model flag to the CDATA state. */ - return HTML5::CDATA; - break; - - /* A start tag whose tag name is "select" */ - case 'select': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Change the insertion mode to "in select". */ - $this->mode = self::IN_SELECT; - break; - - /* A start or end tag whose tag name is one of: "caption", "col", - "colgroup", "frame", "frameset", "head", "option", "optgroup", - "tbody", "td", "tfoot", "th", "thead", "tr". */ - case 'caption': case 'col': case 'colgroup': case 'frame': - case 'frameset': case 'head': case 'option': case 'optgroup': - case 'tbody': case 'td': case 'tfoot': case 'th': case 'thead': - case 'tr': - // Parse error. Ignore the token. - break; - - /* A start or end tag whose tag name is one of: "event-source", - "section", "nav", "article", "aside", "header", "footer", - "datagrid", "command" */ - case 'event-source': case 'section': case 'nav': case 'article': - case 'aside': case 'header': case 'footer': case 'datagrid': - case 'command': - // Work in progress! - break; - - /* A start tag token not covered by the previous entries */ - default: - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - $this->insertElement($token, true, true); - break; - } - break; - - case HTML5::ENDTAG: - switch($token['name']) { - /* An end tag with the tag name "body" */ - case 'body': - /* If the second element in the stack of open elements is - not a body element, this is a parse error. Ignore the token. - (innerHTML case) */ - if(count($this->stack) < 2 || $this->stack[1]->nodeName !== 'body') { - // Ignore. - - /* If the current node is not the body element, then this - is a parse error. */ - } elseif(end($this->stack)->nodeName !== 'body') { - // Parse error. - } - - /* Change the insertion mode to "after body". */ - $this->mode = self::AFTER_BODY; - break; - - /* An end tag with the tag name "html" */ - case 'html': - /* Act as if an end tag with tag name "body" had been seen, - then, if that token wasn't ignored, reprocess the current - token. */ - $this->inBody(array( - 'name' => 'body', - 'type' => HTML5::ENDTAG - )); - - return $this->afterBody($token); - break; - - /* An end tag whose tag name is one of: "address", "blockquote", - "center", "dir", "div", "dl", "fieldset", "listing", "menu", - "ol", "pre", "ul" */ - case 'address': case 'blockquote': case 'center': case 'dir': - case 'div': case 'dl': case 'fieldset': case 'listing': - case 'menu': case 'ol': case 'pre': case 'ul': - /* If the stack of open elements has an element in scope - with the same tag name as that of the token, then generate - implied end tags. */ - if($this->elementInScope($token['name'])) { - $this->generateImpliedEndTags(); - - /* Now, if the current node is not an element with - the same tag name as that of the token, then this - is a parse error. */ - // w/e - - /* If the stack of open elements has an element in - scope with the same tag name as that of the token, - then pop elements from this stack until an element - with that tag name has been popped from the stack. */ - for($n = count($this->stack) - 1; $n >= 0; $n--) { - if($this->stack[$n]->nodeName === $token['name']) { - $n = -1; - } - - array_pop($this->stack); - } - } - break; - - /* An end tag whose tag name is "form" */ - case 'form': - /* If the stack of open elements has an element in scope - with the same tag name as that of the token, then generate - implied end tags. */ - if($this->elementInScope($token['name'])) { - $this->generateImpliedEndTags(); - - } - - if(end($this->stack)->nodeName !== $token['name']) { - /* Now, if the current node is not an element with the - same tag name as that of the token, then this is a parse - error. */ - // w/e - - } else { - /* Otherwise, if the current node is an element with - the same tag name as that of the token pop that element - from the stack. */ - array_pop($this->stack); - } - - /* In any case, set the form element pointer to null. */ - $this->form_pointer = null; - break; - - /* An end tag whose tag name is "p" */ - case 'p': - /* If the stack of open elements has a p element in scope, - then generate implied end tags, except for p elements. */ - if($this->elementInScope('p')) { - $this->generateImpliedEndTags(array('p')); - - /* If the current node is not a p element, then this is - a parse error. */ - // k - - /* If the stack of open elements has a p element in - scope, then pop elements from this stack until the stack - no longer has a p element in scope. */ - for($n = count($this->stack) - 1; $n >= 0; $n--) { - if($this->elementInScope('p')) { - array_pop($this->stack); - - } else { - break; - } - } - } - break; - - /* An end tag whose tag name is "dd", "dt", or "li" */ - case 'dd': case 'dt': case 'li': - /* If the stack of open elements has an element in scope - whose tag name matches the tag name of the token, then - generate implied end tags, except for elements with the - same tag name as the token. */ - if($this->elementInScope($token['name'])) { - $this->generateImpliedEndTags(array($token['name'])); - - /* If the current node is not an element with the same - tag name as the token, then this is a parse error. */ - // w/e - - /* If the stack of open elements has an element in scope - whose tag name matches the tag name of the token, then - pop elements from this stack until an element with that - tag name has been popped from the stack. */ - for($n = count($this->stack) - 1; $n >= 0; $n--) { - if($this->stack[$n]->nodeName === $token['name']) { - $n = -1; - } - - array_pop($this->stack); - } - } - break; - - /* An end tag whose tag name is one of: "h1", "h2", "h3", "h4", - "h5", "h6" */ - case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': - $elements = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'); - - /* If the stack of open elements has in scope an element whose - tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then - generate implied end tags. */ - if($this->elementInScope($elements)) { - $this->generateImpliedEndTags(); - - /* Now, if the current node is not an element with the same - tag name as that of the token, then this is a parse error. */ - // w/e - - /* If the stack of open elements has in scope an element - whose tag name is one of "h1", "h2", "h3", "h4", "h5", or - "h6", then pop elements from the stack until an element - with one of those tag names has been popped from the stack. */ - while($this->elementInScope($elements)) { - array_pop($this->stack); - } - } - break; - - /* An end tag whose tag name is one of: "a", "b", "big", "em", - "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ - case 'a': case 'b': case 'big': case 'em': case 'font': - case 'i': case 'nobr': case 's': case 'small': case 'strike': - case 'strong': case 'tt': case 'u': - /* 1. Let the formatting element be the last element in - the list of active formatting elements that: - * is between the end of the list and the last scope - marker in the list, if any, or the start of the list - otherwise, and - * has the same tag name as the token. - */ - while(true) { - for($a = count($this->a_formatting) - 1; $a >= 0; $a--) { - if($this->a_formatting[$a] === self::MARKER) { - break; - - } elseif($this->a_formatting[$a]->tagName === $token['name']) { - $formatting_element = $this->a_formatting[$a]; - $in_stack = in_array($formatting_element, $this->stack, true); - $fe_af_pos = $a; - break; - } - } - - /* If there is no such node, or, if that node is - also in the stack of open elements but the element - is not in scope, then this is a parse error. Abort - these steps. The token is ignored. */ - if(!isset($formatting_element) || ($in_stack && - !$this->elementInScope($token['name']))) { - break; - - /* Otherwise, if there is such a node, but that node - is not in the stack of open elements, then this is a - parse error; remove the element from the list, and - abort these steps. */ - } elseif(isset($formatting_element) && !$in_stack) { - unset($this->a_formatting[$fe_af_pos]); - $this->a_formatting = array_merge($this->a_formatting); - break; - } - - /* 2. Let the furthest block be the topmost node in the - stack of open elements that is lower in the stack - than the formatting element, and is not an element in - the phrasing or formatting categories. There might - not be one. */ - $fe_s_pos = array_search($formatting_element, $this->stack, true); - $length = count($this->stack); - - for($s = $fe_s_pos + 1; $s < $length; $s++) { - $category = $this->getElementCategory($this->stack[$s]->nodeName); - - if($category !== self::PHRASING && $category !== self::FORMATTING) { - $furthest_block = $this->stack[$s]; - } - } - - /* 3. If there is no furthest block, then the UA must - skip the subsequent steps and instead just pop all - the nodes from the bottom of the stack of open - elements, from the current node up to the formatting - element, and remove the formatting element from the - list of active formatting elements. */ - if(!isset($furthest_block)) { - for($n = $length - 1; $n >= $fe_s_pos; $n--) { - array_pop($this->stack); - } - - unset($this->a_formatting[$fe_af_pos]); - $this->a_formatting = array_merge($this->a_formatting); - break; - } - - /* 4. Let the common ancestor be the element - immediately above the formatting element in the stack - of open elements. */ - $common_ancestor = $this->stack[$fe_s_pos - 1]; - - /* 5. If the furthest block has a parent node, then - remove the furthest block from its parent node. */ - if($furthest_block->parentNode !== null) { - $furthest_block->parentNode->removeChild($furthest_block); - } - - /* 6. Let a bookmark note the position of the - formatting element in the list of active formatting - elements relative to the elements on either side - of it in the list. */ - $bookmark = $fe_af_pos; - - /* 7. Let node and last node be the furthest block. - Follow these steps: */ - $node = $furthest_block; - $last_node = $furthest_block; - - while(true) { - for($n = array_search($node, $this->stack, true) - 1; $n >= 0; $n--) { - /* 7.1 Let node be the element immediately - prior to node in the stack of open elements. */ - $node = $this->stack[$n]; - - /* 7.2 If node is not in the list of active - formatting elements, then remove node from - the stack of open elements and then go back - to step 1. */ - if(!in_array($node, $this->a_formatting, true)) { - unset($this->stack[$n]); - $this->stack = array_merge($this->stack); - - } else { - break; - } - } - - /* 7.3 Otherwise, if node is the formatting - element, then go to the next step in the overall - algorithm. */ - if($node === $formatting_element) { - break; - - /* 7.4 Otherwise, if last node is the furthest - block, then move the aforementioned bookmark to - be immediately after the node in the list of - active formatting elements. */ - } elseif($last_node === $furthest_block) { - $bookmark = array_search($node, $this->a_formatting, true) + 1; - } - - /* 7.5 If node has any children, perform a - shallow clone of node, replace the entry for - node in the list of active formatting elements - with an entry for the clone, replace the entry - for node in the stack of open elements with an - entry for the clone, and let node be the clone. */ - if($node->hasChildNodes()) { - $clone = $node->cloneNode(); - $s_pos = array_search($node, $this->stack, true); - $a_pos = array_search($node, $this->a_formatting, true); - - $this->stack[$s_pos] = $clone; - $this->a_formatting[$a_pos] = $clone; - $node = $clone; - } - - /* 7.6 Insert last node into node, first removing - it from its previous parent node if any. */ - if($last_node->parentNode !== null) { - $last_node->parentNode->removeChild($last_node); - } - - $node->appendChild($last_node); - - /* 7.7 Let last node be node. */ - $last_node = $node; - } - - /* 8. Insert whatever last node ended up being in - the previous step into the common ancestor node, - first removing it from its previous parent node if - any. */ - if($last_node->parentNode !== null) { - $last_node->parentNode->removeChild($last_node); - } - - $common_ancestor->appendChild($last_node); - - /* 9. Perform a shallow clone of the formatting - element. */ - $clone = $formatting_element->cloneNode(); - - /* 10. Take all of the child nodes of the furthest - block and append them to the clone created in the - last step. */ - while($furthest_block->hasChildNodes()) { - $child = $furthest_block->firstChild; - $furthest_block->removeChild($child); - $clone->appendChild($child); - } - - /* 11. Append that clone to the furthest block. */ - $furthest_block->appendChild($clone); - - /* 12. Remove the formatting element from the list - of active formatting elements, and insert the clone - into the list of active formatting elements at the - position of the aforementioned bookmark. */ - $fe_af_pos = array_search($formatting_element, $this->a_formatting, true); - unset($this->a_formatting[$fe_af_pos]); - $this->a_formatting = array_merge($this->a_formatting); - - $af_part1 = array_slice($this->a_formatting, 0, $bookmark - 1); - $af_part2 = array_slice($this->a_formatting, $bookmark, count($this->a_formatting)); - $this->a_formatting = array_merge($af_part1, array($clone), $af_part2); - - /* 13. Remove the formatting element from the stack - of open elements, and insert the clone into the stack - of open elements immediately after (i.e. in a more - deeply nested position than) the position of the - furthest block in that stack. */ - $fe_s_pos = array_search($formatting_element, $this->stack, true); - $fb_s_pos = array_search($furthest_block, $this->stack, true); - unset($this->stack[$fe_s_pos]); - - $s_part1 = array_slice($this->stack, 0, $fb_s_pos); - $s_part2 = array_slice($this->stack, $fb_s_pos + 1, count($this->stack)); - $this->stack = array_merge($s_part1, array($clone), $s_part2); - - /* 14. Jump back to step 1 in this series of steps. */ - unset($formatting_element, $fe_af_pos, $fe_s_pos, $furthest_block); - } - break; - - /* An end tag token whose tag name is one of: "button", - "marquee", "object" */ - case 'button': case 'marquee': case 'object': - /* If the stack of open elements has an element in scope whose - tag name matches the tag name of the token, then generate implied - tags. */ - if($this->elementInScope($token['name'])) { - $this->generateImpliedEndTags(); - - /* Now, if the current node is not an element with the same - tag name as the token, then this is a parse error. */ - // k - - /* Now, if the stack of open elements has an element in scope - whose tag name matches the tag name of the token, then pop - elements from the stack until that element has been popped from - the stack, and clear the list of active formatting elements up - to the last marker. */ - for($n = count($this->stack) - 1; $n >= 0; $n--) { - if($this->stack[$n]->nodeName === $token['name']) { - $n = -1; - } - - array_pop($this->stack); - } - - $marker = end(array_keys($this->a_formatting, self::MARKER, true)); - - for($n = count($this->a_formatting) - 1; $n > $marker; $n--) { - array_pop($this->a_formatting); - } - } - break; - - /* Or an end tag whose tag name is one of: "area", "basefont", - "bgsound", "br", "embed", "hr", "iframe", "image", "img", - "input", "isindex", "noembed", "noframes", "param", "select", - "spacer", "table", "textarea", "wbr" */ - case 'area': case 'basefont': case 'bgsound': case 'br': - case 'embed': case 'hr': case 'iframe': case 'image': - case 'img': case 'input': case 'isindex': case 'noembed': - case 'noframes': case 'param': case 'select': case 'spacer': - case 'table': case 'textarea': case 'wbr': - // Parse error. Ignore the token. - break; - - /* An end tag token not covered by the previous entries */ - default: - for($n = count($this->stack) - 1; $n >= 0; $n--) { - /* Initialise node to be the current node (the bottommost - node of the stack). */ - $node = end($this->stack); - - /* If node has the same tag name as the end tag token, - then: */ - if($token['name'] === $node->nodeName) { - /* Generate implied end tags. */ - $this->generateImpliedEndTags(); - - /* If the tag name of the end tag token does not - match the tag name of the current node, this is a - parse error. */ - // k - - /* Pop all the nodes from the current node up to - node, including node, then stop this algorithm. */ - for($x = count($this->stack) - $n; $x >= $n; $x--) { - array_pop($this->stack); - } - - } else { - $category = $this->getElementCategory($node); - - if($category !== self::SPECIAL && $category !== self::SCOPING) { - /* Otherwise, if node is in neither the formatting - category nor the phrasing category, then this is a - parse error. Stop this algorithm. The end tag token - is ignored. */ - return false; - } - } - } - break; - } - break; - } - } - - private function inTable($token) { - $clear = array('html', 'table'); - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - if($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Append the character to the current node. */ - $text = $this->dom->createTextNode($token['data']); - end($this->stack)->appendChild($text); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $comment = $this->dom->createComment($token['data']); - end($this->stack)->appendChild($comment); - - /* A start tag whose tag name is "caption" */ - } elseif($token['type'] === HTML5::STARTTAG && - $token['name'] === 'caption') { - /* Clear the stack back to a table context. */ - $this->clearStackToTableContext($clear); - - /* Insert a marker at the end of the list of active - formatting elements. */ - $this->a_formatting[] = self::MARKER; - - /* Insert an HTML element for the token, then switch the - insertion mode to "in caption". */ - $this->insertElement($token); - $this->mode = self::IN_CAPTION; - - /* A start tag whose tag name is "colgroup" */ - } elseif($token['type'] === HTML5::STARTTAG && - $token['name'] === 'colgroup') { - /* Clear the stack back to a table context. */ - $this->clearStackToTableContext($clear); - - /* Insert an HTML element for the token, then switch the - insertion mode to "in column group". */ - $this->insertElement($token); - $this->mode = self::IN_CGROUP; - - /* A start tag whose tag name is "col" */ - } elseif($token['type'] === HTML5::STARTTAG && - $token['name'] === 'col') { - $this->inTable(array( - 'name' => 'colgroup', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - $this->inColumnGroup($token); - - /* A start tag whose tag name is one of: "tbody", "tfoot", "thead" */ - } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('tbody', 'tfoot', 'thead'))) { - /* Clear the stack back to a table context. */ - $this->clearStackToTableContext($clear); - - /* Insert an HTML element for the token, then switch the insertion - mode to "in table body". */ - $this->insertElement($token); - $this->mode = self::IN_TBODY; - - /* A start tag whose tag name is one of: "td", "th", "tr" */ - } elseif($token['type'] === HTML5::STARTTAG && - in_array($token['name'], array('td', 'th', 'tr'))) { - /* Act as if a start tag token with the tag name "tbody" had been - seen, then reprocess the current token. */ - $this->inTable(array( - 'name' => 'tbody', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - return $this->inTableBody($token); - - /* A start tag whose tag name is "table" */ - } elseif($token['type'] === HTML5::STARTTAG && - $token['name'] === 'table') { - /* Parse error. Act as if an end tag token with the tag name "table" - had been seen, then, if that token wasn't ignored, reprocess the - current token. */ - $this->inTable(array( - 'name' => 'table', - 'type' => HTML5::ENDTAG - )); - - return $this->mainPhase($token); - - /* An end tag whose tag name is "table" */ - } elseif($token['type'] === HTML5::ENDTAG && - $token['name'] === 'table') { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. (innerHTML case) */ - if(!$this->elementInScope($token['name'], true)) { - return false; - - /* Otherwise: */ - } else { - /* Generate implied end tags. */ - $this->generateImpliedEndTags(); - - /* Now, if the current node is not a table element, then this - is a parse error. */ - // w/e - - /* Pop elements from this stack until a table element has been - popped from the stack. */ - while(true) { - $current = end($this->stack)->nodeName; - array_pop($this->stack); - - if($current === 'table') { - break; - } - } - - /* Reset the insertion mode appropriately. */ - $this->resetInsertionMode(); - } - - /* An end tag whose tag name is one of: "body", "caption", "col", - "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ - } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], - array('body', 'caption', 'col', 'colgroup', 'html', 'tbody', 'td', - 'tfoot', 'th', 'thead', 'tr'))) { - // Parse error. Ignore the token. - - /* Anything else */ - } else { - /* Parse error. Process the token as if the insertion mode was "in - body", with the following exception: */ - - /* If the current node is a table, tbody, tfoot, thead, or tr - element, then, whenever a node would be inserted into the current - node, it must instead be inserted into the foster parent element. */ - if(in_array(end($this->stack)->nodeName, - array('table', 'tbody', 'tfoot', 'thead', 'tr'))) { - /* The foster parent element is the parent element of the last - table element in the stack of open elements, if there is a - table element and it has such a parent element. If there is no - table element in the stack of open elements (innerHTML case), - then the foster parent element is the first element in the - stack of open elements (the html element). Otherwise, if there - is a table element in the stack of open elements, but the last - table element in the stack of open elements has no parent, or - its parent node is not an element, then the foster parent - element is the element before the last table element in the - stack of open elements. */ - for($n = count($this->stack) - 1; $n >= 0; $n--) { - if($this->stack[$n]->nodeName === 'table') { - $table = $this->stack[$n]; - break; - } - } - - if(isset($table) && $table->parentNode !== null) { - $this->foster_parent = $table->parentNode; - - } elseif(!isset($table)) { - $this->foster_parent = $this->stack[0]; - - } elseif(isset($table) && ($table->parentNode === null || - $table->parentNode->nodeType !== XML_ELEMENT_NODE)) { - $this->foster_parent = $this->stack[$n - 1]; - } - } - - $this->inBody($token); - } - } - - private function inCaption($token) { - /* An end tag whose tag name is "caption" */ - if($token['type'] === HTML5::ENDTAG && $token['name'] === 'caption') { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. (innerHTML case) */ - if(!$this->elementInScope($token['name'], true)) { - // Ignore - - /* Otherwise: */ - } else { - /* Generate implied end tags. */ - $this->generateImpliedEndTags(); - - /* Now, if the current node is not a caption element, then this - is a parse error. */ - // w/e - - /* Pop elements from this stack until a caption element has - been popped from the stack. */ - while(true) { - $node = end($this->stack)->nodeName; - array_pop($this->stack); - - if($node === 'caption') { - break; - } - } - - /* Clear the list of active formatting elements up to the last - marker. */ - $this->clearTheActiveFormattingElementsUpToTheLastMarker(); - - /* Switch the insertion mode to "in table". */ - $this->mode = self::IN_TABLE; - } - - /* A start tag whose tag name is one of: "caption", "col", "colgroup", - "tbody", "td", "tfoot", "th", "thead", "tr", or an end tag whose tag - name is "table" */ - } elseif(($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', - 'thead', 'tr'))) || ($token['type'] === HTML5::ENDTAG && - $token['name'] === 'table')) { - /* Parse error. Act as if an end tag with the tag name "caption" - had been seen, then, if that token wasn't ignored, reprocess the - current token. */ - $this->inCaption(array( - 'name' => 'caption', - 'type' => HTML5::ENDTAG - )); - - return $this->inTable($token); - - /* An end tag whose tag name is one of: "body", "col", "colgroup", - "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ - } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], - array('body', 'col', 'colgroup', 'html', 'tbody', 'tfoot', 'th', - 'thead', 'tr'))) { - // Parse error. Ignore the token. - - /* Anything else */ - } else { - /* Process the token as if the insertion mode was "in body". */ - $this->inBody($token); - } - } - - private function inColumnGroup($token) { - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - if($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Append the character to the current node. */ - $text = $this->dom->createTextNode($token['data']); - end($this->stack)->appendChild($text); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $comment = $this->dom->createComment($token['data']); - end($this->stack)->appendChild($comment); - - /* A start tag whose tag name is "col" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'col') { - /* Insert a col element for the token. Immediately pop the current - node off the stack of open elements. */ - $this->insertElement($token); - array_pop($this->stack); - - /* An end tag whose tag name is "colgroup" */ - } elseif($token['type'] === HTML5::ENDTAG && - $token['name'] === 'colgroup') { - /* If the current node is the root html element, then this is a - parse error, ignore the token. (innerHTML case) */ - if(end($this->stack)->nodeName === 'html') { - // Ignore - - /* Otherwise, pop the current node (which will be a colgroup - element) from the stack of open elements. Switch the insertion - mode to "in table". */ - } else { - array_pop($this->stack); - $this->mode = self::IN_TABLE; - } - - /* An end tag whose tag name is "col" */ - } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'col') { - /* Parse error. Ignore the token. */ - - /* Anything else */ - } else { - /* Act as if an end tag with the tag name "colgroup" had been seen, - and then, if that token wasn't ignored, reprocess the current token. */ - $this->inColumnGroup(array( - 'name' => 'colgroup', - 'type' => HTML5::ENDTAG - )); - - return $this->inTable($token); - } - } - - private function inTableBody($token) { - $clear = array('tbody', 'tfoot', 'thead', 'html'); - - /* A start tag whose tag name is "tr" */ - if($token['type'] === HTML5::STARTTAG && $token['name'] === 'tr') { - /* Clear the stack back to a table body context. */ - $this->clearStackToTableContext($clear); - - /* Insert a tr element for the token, then switch the insertion - mode to "in row". */ - $this->insertElement($token); - $this->mode = self::IN_ROW; - - /* A start tag whose tag name is one of: "th", "td" */ - } elseif($token['type'] === HTML5::STARTTAG && - ($token['name'] === 'th' || $token['name'] === 'td')) { - /* Parse error. Act as if a start tag with the tag name "tr" had - been seen, then reprocess the current token. */ - $this->inTableBody(array( - 'name' => 'tr', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - return $this->inRow($token); - - /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ - } elseif($token['type'] === HTML5::ENDTAG && - in_array($token['name'], array('tbody', 'tfoot', 'thead'))) { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. */ - if(!$this->elementInScope($token['name'], true)) { - // Ignore - - /* Otherwise: */ - } else { - /* Clear the stack back to a table body context. */ - $this->clearStackToTableContext($clear); - - /* Pop the current node from the stack of open elements. Switch - the insertion mode to "in table". */ - array_pop($this->stack); - $this->mode = self::IN_TABLE; - } - - /* A start tag whose tag name is one of: "caption", "col", "colgroup", - "tbody", "tfoot", "thead", or an end tag whose tag name is "table" */ - } elseif(($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('caption', 'col', 'colgroup', 'tbody', 'tfoor', 'thead'))) || - ($token['type'] === HTML5::STARTTAG && $token['name'] === 'table')) { - /* If the stack of open elements does not have a tbody, thead, or - tfoot element in table scope, this is a parse error. Ignore the - token. (innerHTML case) */ - if(!$this->elementInScope(array('tbody', 'thead', 'tfoot'), true)) { - // Ignore. - - /* Otherwise: */ - } else { - /* Clear the stack back to a table body context. */ - $this->clearStackToTableContext($clear); - - /* Act as if an end tag with the same tag name as the current - node ("tbody", "tfoot", or "thead") had been seen, then - reprocess the current token. */ - $this->inTableBody(array( - 'name' => end($this->stack)->nodeName, - 'type' => HTML5::ENDTAG - )); - - return $this->mainPhase($token); - } - - /* An end tag whose tag name is one of: "body", "caption", "col", - "colgroup", "html", "td", "th", "tr" */ - } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], - array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr'))) { - /* Parse error. Ignore the token. */ - - /* Anything else */ - } else { - /* Process the token as if the insertion mode was "in table". */ - $this->inTable($token); - } - } - - private function inRow($token) { - $clear = array('tr', 'html'); - - /* A start tag whose tag name is one of: "th", "td" */ - if($token['type'] === HTML5::STARTTAG && - ($token['name'] === 'th' || $token['name'] === 'td')) { - /* Clear the stack back to a table row context. */ - $this->clearStackToTableContext($clear); - - /* Insert an HTML element for the token, then switch the insertion - mode to "in cell". */ - $this->insertElement($token); - $this->mode = self::IN_CELL; - - /* Insert a marker at the end of the list of active formatting - elements. */ - $this->a_formatting[] = self::MARKER; - - /* An end tag whose tag name is "tr" */ - } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'tr') { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. (innerHTML case) */ - if(!$this->elementInScope($token['name'], true)) { - // Ignore. - - /* Otherwise: */ - } else { - /* Clear the stack back to a table row context. */ - $this->clearStackToTableContext($clear); - - /* Pop the current node (which will be a tr element) from the - stack of open elements. Switch the insertion mode to "in table - body". */ - array_pop($this->stack); - $this->mode = self::IN_TBODY; - } - - /* A start tag whose tag name is one of: "caption", "col", "colgroup", - "tbody", "tfoot", "thead", "tr" or an end tag whose tag name is "table" */ - } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead', 'tr'))) { - /* Act as if an end tag with the tag name "tr" had been seen, then, - if that token wasn't ignored, reprocess the current token. */ - $this->inRow(array( - 'name' => 'tr', - 'type' => HTML5::ENDTAG - )); - - return $this->inCell($token); - - /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ - } elseif($token['type'] === HTML5::ENDTAG && - in_array($token['name'], array('tbody', 'tfoot', 'thead'))) { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. */ - if(!$this->elementInScope($token['name'], true)) { - // Ignore. - - /* Otherwise: */ - } else { - /* Otherwise, act as if an end tag with the tag name "tr" had - been seen, then reprocess the current token. */ - $this->inRow(array( - 'name' => 'tr', - 'type' => HTML5::ENDTAG - )); - - return $this->inCell($token); - } - - /* An end tag whose tag name is one of: "body", "caption", "col", - "colgroup", "html", "td", "th" */ - } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], - array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr'))) { - /* Parse error. Ignore the token. */ - - /* Anything else */ - } else { - /* Process the token as if the insertion mode was "in table". */ - $this->inTable($token); - } - } - - private function inCell($token) { - /* An end tag whose tag name is one of: "td", "th" */ - if($token['type'] === HTML5::ENDTAG && - ($token['name'] === 'td' || $token['name'] === 'th')) { - /* If the stack of open elements does not have an element in table - scope with the same tag name as that of the token, then this is a - parse error and the token must be ignored. */ - if(!$this->elementInScope($token['name'], true)) { - // Ignore. - - /* Otherwise: */ - } else { - /* Generate implied end tags, except for elements with the same - tag name as the token. */ - $this->generateImpliedEndTags(array($token['name'])); - - /* Now, if the current node is not an element with the same tag - name as the token, then this is a parse error. */ - // k - - /* Pop elements from this stack until an element with the same - tag name as the token has been popped from the stack. */ - while(true) { - $node = end($this->stack)->nodeName; - array_pop($this->stack); - - if($node === $token['name']) { - break; - } - } - - /* Clear the list of active formatting elements up to the last - marker. */ - $this->clearTheActiveFormattingElementsUpToTheLastMarker(); - - /* Switch the insertion mode to "in row". (The current node - will be a tr element at this point.) */ - $this->mode = self::IN_ROW; - } - - /* A start tag whose tag name is one of: "caption", "col", "colgroup", - "tbody", "td", "tfoot", "th", "thead", "tr" */ - } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', - 'thead', 'tr'))) { - /* If the stack of open elements does not have a td or th element - in table scope, then this is a parse error; ignore the token. - (innerHTML case) */ - if(!$this->elementInScope(array('td', 'th'), true)) { - // Ignore. - - /* Otherwise, close the cell (see below) and reprocess the current - token. */ - } else { - $this->closeCell(); - return $this->inRow($token); - } - - /* A start tag whose tag name is one of: "caption", "col", "colgroup", - "tbody", "td", "tfoot", "th", "thead", "tr" */ - } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', - 'thead', 'tr'))) { - /* If the stack of open elements does not have a td or th element - in table scope, then this is a parse error; ignore the token. - (innerHTML case) */ - if(!$this->elementInScope(array('td', 'th'), true)) { - // Ignore. - - /* Otherwise, close the cell (see below) and reprocess the current - token. */ - } else { - $this->closeCell(); - return $this->inRow($token); - } - - /* An end tag whose tag name is one of: "body", "caption", "col", - "colgroup", "html" */ - } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], - array('body', 'caption', 'col', 'colgroup', 'html'))) { - /* Parse error. Ignore the token. */ - - /* An end tag whose tag name is one of: "table", "tbody", "tfoot", - "thead", "tr" */ - } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], - array('table', 'tbody', 'tfoot', 'thead', 'tr'))) { - /* If the stack of open elements does not have an element in table - scope with the same tag name as that of the token (which can only - happen for "tbody", "tfoot" and "thead", or, in the innerHTML case), - then this is a parse error and the token must be ignored. */ - if(!$this->elementInScope($token['name'], true)) { - // Ignore. - - /* Otherwise, close the cell (see below) and reprocess the current - token. */ - } else { - $this->closeCell(); - return $this->inRow($token); - } - - /* Anything else */ - } else { - /* Process the token as if the insertion mode was "in body". */ - $this->inBody($token); - } - } - - private function inSelect($token) { - /* Handle the token as follows: */ - - /* A character token */ - if($token['type'] === HTML5::CHARACTR) { - /* Append the token's character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $this->insertComment($token['data']); - - /* A start tag token whose tag name is "option" */ - } elseif($token['type'] === HTML5::STARTTAG && - $token['name'] === 'option') { - /* If the current node is an option element, act as if an end tag - with the tag name "option" had been seen. */ - if(end($this->stack)->nodeName === 'option') { - $this->inSelect(array( - 'name' => 'option', - 'type' => HTML5::ENDTAG - )); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* A start tag token whose tag name is "optgroup" */ - } elseif($token['type'] === HTML5::STARTTAG && - $token['name'] === 'optgroup') { - /* If the current node is an option element, act as if an end tag - with the tag name "option" had been seen. */ - if(end($this->stack)->nodeName === 'option') { - $this->inSelect(array( - 'name' => 'option', - 'type' => HTML5::ENDTAG - )); - } - - /* If the current node is an optgroup element, act as if an end tag - with the tag name "optgroup" had been seen. */ - if(end($this->stack)->nodeName === 'optgroup') { - $this->inSelect(array( - 'name' => 'optgroup', - 'type' => HTML5::ENDTAG - )); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* An end tag token whose tag name is "optgroup" */ - } elseif($token['type'] === HTML5::ENDTAG && - $token['name'] === 'optgroup') { - /* First, if the current node is an option element, and the node - immediately before it in the stack of open elements is an optgroup - element, then act as if an end tag with the tag name "option" had - been seen. */ - $elements_in_stack = count($this->stack); - - if($this->stack[$elements_in_stack - 1]->nodeName === 'option' && - $this->stack[$elements_in_stack - 2]->nodeName === 'optgroup') { - $this->inSelect(array( - 'name' => 'option', - 'type' => HTML5::ENDTAG - )); - } - - /* If the current node is an optgroup element, then pop that node - from the stack of open elements. Otherwise, this is a parse error, - ignore the token. */ - if($this->stack[$elements_in_stack - 1] === 'optgroup') { - array_pop($this->stack); - } - - /* An end tag token whose tag name is "option" */ - } elseif($token['type'] === HTML5::ENDTAG && - $token['name'] === 'option') { - /* If the current node is an option element, then pop that node - from the stack of open elements. Otherwise, this is a parse error, - ignore the token. */ - if(end($this->stack)->nodeName === 'option') { - array_pop($this->stack); - } - - /* An end tag whose tag name is "select" */ - } elseif($token['type'] === HTML5::ENDTAG && - $token['name'] === 'select') { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. (innerHTML case) */ - if(!$this->elementInScope($token['name'], true)) { - // w/e - - /* Otherwise: */ - } else { - /* Pop elements from the stack of open elements until a select - element has been popped from the stack. */ - while(true) { - $current = end($this->stack)->nodeName; - array_pop($this->stack); - - if($current === 'select') { - break; - } - } - - /* Reset the insertion mode appropriately. */ - $this->resetInsertionMode(); - } - - /* A start tag whose tag name is "select" */ - } elseif($token['name'] === 'select' && - $token['type'] === HTML5::STARTTAG) { - /* Parse error. Act as if the token had been an end tag with the - tag name "select" instead. */ - $this->inSelect(array( - 'name' => 'select', - 'type' => HTML5::ENDTAG - )); - - /* An end tag whose tag name is one of: "caption", "table", "tbody", - "tfoot", "thead", "tr", "td", "th" */ - } elseif(in_array($token['name'], array('caption', 'table', 'tbody', - 'tfoot', 'thead', 'tr', 'td', 'th')) && $token['type'] === HTML5::ENDTAG) { - /* Parse error. */ - // w/e - - /* If the stack of open elements has an element in table scope with - the same tag name as that of the token, then act as if an end tag - with the tag name "select" had been seen, and reprocess the token. - Otherwise, ignore the token. */ - if($this->elementInScope($token['name'], true)) { - $this->inSelect(array( - 'name' => 'select', - 'type' => HTML5::ENDTAG - )); - - $this->mainPhase($token); - } - - /* Anything else */ - } else { - /* Parse error. Ignore the token. */ - } - } - - private function afterBody($token) { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - if($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Process the token as it would be processed if the insertion mode - was "in body". */ - $this->inBody($token); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the first element in the stack of open - elements (the html element), with the data attribute set to the - data given in the comment token. */ - $comment = $this->dom->createComment($token['data']); - $this->stack[0]->appendChild($comment); - - /* An end tag with the tag name "html" */ - } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') { - /* If the parser was originally created in order to handle the - setting of an element's innerHTML attribute, this is a parse error; - ignore the token. (The element will be an html element in this - case.) (innerHTML case) */ - - /* Otherwise, switch to the trailing end phase. */ - $this->phase = self::END_PHASE; - - /* Anything else */ - } else { - /* Parse error. Set the insertion mode to "in body" and reprocess - the token. */ - $this->mode = self::IN_BODY; - return $this->inBody($token); - } - } - - private function inFrameset($token) { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ - if($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Append the character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $this->insertComment($token['data']); - - /* A start tag with the tag name "frameset" */ - } elseif($token['name'] === 'frameset' && - $token['type'] === HTML5::STARTTAG) { - $this->insertElement($token); - - /* An end tag with the tag name "frameset" */ - } elseif($token['name'] === 'frameset' && - $token['type'] === HTML5::ENDTAG) { - /* If the current node is the root html element, then this is a - parse error; ignore the token. (innerHTML case) */ - if(end($this->stack)->nodeName === 'html') { - // Ignore - - } else { - /* Otherwise, pop the current node from the stack of open - elements. */ - array_pop($this->stack); - - /* If the parser was not originally created in order to handle - the setting of an element's innerHTML attribute (innerHTML case), - and the current node is no longer a frameset element, then change - the insertion mode to "after frameset". */ - $this->mode = self::AFTR_FRAME; - } - - /* A start tag with the tag name "frame" */ - } elseif($token['name'] === 'frame' && - $token['type'] === HTML5::STARTTAG) { - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Immediately pop the current node off the stack of open elements. */ - array_pop($this->stack); - - /* A start tag with the tag name "noframes" */ - } elseif($token['name'] === 'noframes' && - $token['type'] === HTML5::STARTTAG) { - /* Process the token as if the insertion mode had been "in body". */ - $this->inBody($token); - - /* Anything else */ - } else { - /* Parse error. Ignore the token. */ - } - } - - private function afterFrameset($token) { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ - if($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Append the character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $this->insertComment($token['data']); - - /* An end tag with the tag name "html" */ - } elseif($token['name'] === 'html' && - $token['type'] === HTML5::ENDTAG) { - /* Switch to the trailing end phase. */ - $this->phase = self::END_PHASE; - - /* A start tag with the tag name "noframes" */ - } elseif($token['name'] === 'noframes' && - $token['type'] === HTML5::STARTTAG) { - /* Process the token as if the insertion mode had been "in body". */ - $this->inBody($token); - - /* Anything else */ - } else { - /* Parse error. Ignore the token. */ - } - } - - private function trailingEndPhase($token) { - /* After the main phase, as each token is emitted from the tokenisation - stage, it must be processed as described in this section. */ - - /* A DOCTYPE token */ - if($token['type'] === HTML5::DOCTYPE) { - // Parse error. Ignore the token. - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the Document object with the data - attribute set to the data given in the comment token. */ - $comment = $this->dom->createComment($token['data']); - $this->dom->appendChild($comment); - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - } elseif($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Process the token as it would be processed in the main phase. */ - $this->mainPhase($token); - - /* A character token that is not one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE. Or a start tag token. Or an end tag token. */ - } elseif(($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || - $token['type'] === HTML5::STARTTAG || $token['type'] === HTML5::ENDTAG) { - /* Parse error. Switch back to the main phase and reprocess the - token. */ - $this->phase = self::MAIN_PHASE; - return $this->mainPhase($token); - - /* An end-of-file token */ - } elseif($token['type'] === HTML5::EOF) { - /* OMG DONE!! */ - } - } - - private function insertElement($token, $append = true, $check = false) { - // Proprietary workaround for libxml2's limitations with tag names - if ($check) { - // Slightly modified HTML5 tag-name modification, - // removing anything that's not an ASCII letter, digit, or hyphen - $token['name'] = preg_replace('/[^a-z0-9-]/i', '', $token['name']); - // Remove leading hyphens and numbers - $token['name'] = ltrim($token['name'], '-0..9'); - // In theory, this should ever be needed, but just in case - if ($token['name'] === '') $token['name'] = 'span'; // arbitrary generic choice - } - - $el = $this->dom->createElement($token['name']); - - foreach($token['attr'] as $attr) { - if(!$el->hasAttribute($attr['name'])) { - $el->setAttribute($attr['name'], $attr['value']); - } - } - - $this->appendToRealParent($el); - $this->stack[] = $el; - - return $el; - } - - private function insertText($data) { - $text = $this->dom->createTextNode($data); - $this->appendToRealParent($text); - } - - private function insertComment($data) { - $comment = $this->dom->createComment($data); - $this->appendToRealParent($comment); - } - - private function appendToRealParent($node) { - if($this->foster_parent === null) { - end($this->stack)->appendChild($node); - - } elseif($this->foster_parent !== null) { - /* If the foster parent element is the parent element of the - last table element in the stack of open elements, then the new - node must be inserted immediately before the last table element - in the stack of open elements in the foster parent element; - otherwise, the new node must be appended to the foster parent - element. */ - for($n = count($this->stack) - 1; $n >= 0; $n--) { - if($this->stack[$n]->nodeName === 'table' && - $this->stack[$n]->parentNode !== null) { - $table = $this->stack[$n]; - break; - } - } - - if(isset($table) && $this->foster_parent->isSameNode($table->parentNode)) - $this->foster_parent->insertBefore($node, $table); - else - $this->foster_parent->appendChild($node); - - $this->foster_parent = null; - } - } - - private function elementInScope($el, $table = false) { - if(is_array($el)) { - foreach($el as $element) { - if($this->elementInScope($element, $table)) { - return true; - } - } - - return false; - } - - $leng = count($this->stack); - - for($n = 0; $n < $leng; $n++) { - /* 1. Initialise node to be the current node (the bottommost node of - the stack). */ - $node = $this->stack[$leng - 1 - $n]; - - if($node->tagName === $el) { - /* 2. If node is the target node, terminate in a match state. */ - return true; - - } elseif($node->tagName === 'table') { - /* 3. Otherwise, if node is a table element, terminate in a failure - state. */ - return false; - - } elseif($table === true && in_array($node->tagName, array('caption', 'td', - 'th', 'button', 'marquee', 'object'))) { - /* 4. Otherwise, if the algorithm is the "has an element in scope" - variant (rather than the "has an element in table scope" variant), - and node is one of the following, terminate in a failure state. */ - return false; - - } elseif($node === $node->ownerDocument->documentElement) { - /* 5. Otherwise, if node is an html element (root element), terminate - in a failure state. (This can only happen if the node is the topmost - node of the stack of open elements, and prevents the next step from - being invoked if there are no more elements in the stack.) */ - return false; - } - - /* Otherwise, set node to the previous entry in the stack of open - elements and return to step 2. (This will never fail, since the loop - will always terminate in the previous step if the top of the stack - is reached.) */ - } - } - - private function reconstructActiveFormattingElements() { - /* 1. If there are no entries in the list of active formatting elements, - then there is nothing to reconstruct; stop this algorithm. */ - $formatting_elements = count($this->a_formatting); - - if($formatting_elements === 0) { - return false; - } - - /* 3. Let entry be the last (most recently added) element in the list - of active formatting elements. */ - $entry = end($this->a_formatting); - - /* 2. If the last (most recently added) entry in the list of active - formatting elements is a marker, or if it is an element that is in the - stack of open elements, then there is nothing to reconstruct; stop this - algorithm. */ - if($entry === self::MARKER || in_array($entry, $this->stack, true)) { - return false; - } - - for($a = $formatting_elements - 1; $a >= 0; true) { - /* 4. If there are no entries before entry in the list of active - formatting elements, then jump to step 8. */ - if($a === 0) { - $step_seven = false; - break; - } - - /* 5. Let entry be the entry one earlier than entry in the list of - active formatting elements. */ - $a--; - $entry = $this->a_formatting[$a]; - - /* 6. If entry is neither a marker nor an element that is also in - thetack of open elements, go to step 4. */ - if($entry === self::MARKER || in_array($entry, $this->stack, true)) { - break; - } - } - - while(true) { - /* 7. Let entry be the element one later than entry in the list of - active formatting elements. */ - if(isset($step_seven) && $step_seven === true) { - $a++; - $entry = $this->a_formatting[$a]; - } - - /* 8. Perform a shallow clone of the element entry to obtain clone. */ - $clone = $entry->cloneNode(); - - /* 9. Append clone to the current node and push it onto the stack - of open elements so that it is the new current node. */ - end($this->stack)->appendChild($clone); - $this->stack[] = $clone; - - /* 10. Replace the entry for entry in the list with an entry for - clone. */ - $this->a_formatting[$a] = $clone; - - /* 11. If the entry for clone in the list of active formatting - elements is not the last entry in the list, return to step 7. */ - if(end($this->a_formatting) !== $clone) { - $step_seven = true; - } else { - break; - } - } - } - - private function clearTheActiveFormattingElementsUpToTheLastMarker() { - /* When the steps below require the UA to clear the list of active - formatting elements up to the last marker, the UA must perform the - following steps: */ - - while(true) { - /* 1. Let entry be the last (most recently added) entry in the list - of active formatting elements. */ - $entry = end($this->a_formatting); - - /* 2. Remove entry from the list of active formatting elements. */ - array_pop($this->a_formatting); - - /* 3. If entry was a marker, then stop the algorithm at this point. - The list has been cleared up to the last marker. */ - if($entry === self::MARKER) { - break; - } - } - } - - private function generateImpliedEndTags($exclude = array()) { - /* When the steps below require the UA to generate implied end tags, - then, if the current node is a dd element, a dt element, an li element, - a p element, a td element, a th element, or a tr element, the UA must - act as if an end tag with the respective tag name had been seen and - then generate implied end tags again. */ - $node = end($this->stack); - $elements = array_diff(array('dd', 'dt', 'li', 'p', 'td', 'th', 'tr'), $exclude); - - while(in_array(end($this->stack)->nodeName, $elements)) { - array_pop($this->stack); - } - } - - private function getElementCategory($node) { - $name = $node->tagName; - if(in_array($name, $this->special)) - return self::SPECIAL; - - elseif(in_array($name, $this->scoping)) - return self::SCOPING; - - elseif(in_array($name, $this->formatting)) - return self::FORMATTING; - - else - return self::PHRASING; - } - - private function clearStackToTableContext($elements) { - /* When the steps above require the UA to clear the stack back to a - table context, it means that the UA must, while the current node is not - a table element or an html element, pop elements from the stack of open - elements. If this causes any elements to be popped from the stack, then - this is a parse error. */ - while(true) { - $node = end($this->stack)->nodeName; - - if(in_array($node, $elements)) { - break; - } else { - array_pop($this->stack); - } - } - } - - private function resetInsertionMode() { - /* 1. Let last be false. */ - $last = false; - $leng = count($this->stack); - - for($n = $leng - 1; $n >= 0; $n--) { - /* 2. Let node be the last node in the stack of open elements. */ - $node = $this->stack[$n]; - - /* 3. If node is the first node in the stack of open elements, then - set last to true. If the element whose innerHTML attribute is being - set is neither a td element nor a th element, then set node to the - element whose innerHTML attribute is being set. (innerHTML case) */ - if($this->stack[0]->isSameNode($node)) { - $last = true; - } - - /* 4. If node is a select element, then switch the insertion mode to - "in select" and abort these steps. (innerHTML case) */ - if($node->nodeName === 'select') { - $this->mode = self::IN_SELECT; - break; - - /* 5. If node is a td or th element, then switch the insertion mode - to "in cell" and abort these steps. */ - } elseif($node->nodeName === 'td' || $node->nodeName === 'th') { - $this->mode = self::IN_CELL; - break; - - /* 6. If node is a tr element, then switch the insertion mode to - "in row" and abort these steps. */ - } elseif($node->nodeName === 'tr') { - $this->mode = self::IN_ROW; - break; - - /* 7. If node is a tbody, thead, or tfoot element, then switch the - insertion mode to "in table body" and abort these steps. */ - } elseif(in_array($node->nodeName, array('tbody', 'thead', 'tfoot'))) { - $this->mode = self::IN_TBODY; - break; - - /* 8. If node is a caption element, then switch the insertion mode - to "in caption" and abort these steps. */ - } elseif($node->nodeName === 'caption') { - $this->mode = self::IN_CAPTION; - break; - - /* 9. If node is a colgroup element, then switch the insertion mode - to "in column group" and abort these steps. (innerHTML case) */ - } elseif($node->nodeName === 'colgroup') { - $this->mode = self::IN_CGROUP; - break; - - /* 10. If node is a table element, then switch the insertion mode - to "in table" and abort these steps. */ - } elseif($node->nodeName === 'table') { - $this->mode = self::IN_TABLE; - break; - - /* 11. If node is a head element, then switch the insertion mode - to "in body" ("in body"! not "in head"!) and abort these steps. - (innerHTML case) */ - } elseif($node->nodeName === 'head') { - $this->mode = self::IN_BODY; - break; - - /* 12. If node is a body element, then switch the insertion mode to - "in body" and abort these steps. */ - } elseif($node->nodeName === 'body') { - $this->mode = self::IN_BODY; - break; - - /* 13. If node is a frameset element, then switch the insertion - mode to "in frameset" and abort these steps. (innerHTML case) */ - } elseif($node->nodeName === 'frameset') { - $this->mode = self::IN_FRAME; - break; - - /* 14. If node is an html element, then: if the head element - pointer is null, switch the insertion mode to "before head", - otherwise, switch the insertion mode to "after head". In either - case, abort these steps. (innerHTML case) */ - } elseif($node->nodeName === 'html') { - $this->mode = ($this->head_pointer === null) - ? self::BEFOR_HEAD - : self::AFTER_HEAD; - - break; - - /* 15. If last is true, then set the insertion mode to "in body" - and abort these steps. (innerHTML case) */ - } elseif($last) { - $this->mode = self::IN_BODY; - break; - } - } - } - - private function closeCell() { - /* If the stack of open elements has a td or th element in table scope, - then act as if an end tag token with that tag name had been seen. */ - foreach(array('td', 'th') as $cell) { - if($this->elementInScope($cell, true)) { - $this->inCell(array( - 'name' => $cell, - 'type' => HTML5::ENDTAG - )); - - break; - } - } - } - - public function save() { - return $this->dom; - } -} -?> +normalize($html, $config, $context); + $new_html = $this->wrapHTML($new_html, $config, $context); + try { + $parser = new HTML5($new_html); + $doc = $parser->save(); + } catch (DOMException $e) { + // Uh oh, it failed. Punt to DirectLex. + $lexer = new HTMLPurifier_Lexer_DirectLex(); + $context->register('PH5PError', $e); // save the error, so we can detect it + return $lexer->tokenizeHTML($html, $config, $context); // use original HTML + } + $tokens = array(); + $this->tokenizeDOM( + $doc->getElementsByTagName('html')->item(0)-> // + getElementsByTagName('body')->item(0)-> // + getElementsByTagName('div')->item(0) //
              + , $tokens); + return $tokens; + } + +} + +/* + +Copyright 2007 Jeroen van der Meer + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +class HTML5 { + private $data; + private $char; + private $EOF; + private $state; + private $tree; + private $token; + private $content_model; + private $escape = false; + private $entities = array('AElig;','AElig','AMP;','AMP','Aacute;','Aacute', + 'Acirc;','Acirc','Agrave;','Agrave','Alpha;','Aring;','Aring','Atilde;', + 'Atilde','Auml;','Auml','Beta;','COPY;','COPY','Ccedil;','Ccedil','Chi;', + 'Dagger;','Delta;','ETH;','ETH','Eacute;','Eacute','Ecirc;','Ecirc','Egrave;', + 'Egrave','Epsilon;','Eta;','Euml;','Euml','GT;','GT','Gamma;','Iacute;', + 'Iacute','Icirc;','Icirc','Igrave;','Igrave','Iota;','Iuml;','Iuml','Kappa;', + 'LT;','LT','Lambda;','Mu;','Ntilde;','Ntilde','Nu;','OElig;','Oacute;', + 'Oacute','Ocirc;','Ocirc','Ograve;','Ograve','Omega;','Omicron;','Oslash;', + 'Oslash','Otilde;','Otilde','Ouml;','Ouml','Phi;','Pi;','Prime;','Psi;', + 'QUOT;','QUOT','REG;','REG','Rho;','Scaron;','Sigma;','THORN;','THORN', + 'TRADE;','Tau;','Theta;','Uacute;','Uacute','Ucirc;','Ucirc','Ugrave;', + 'Ugrave','Upsilon;','Uuml;','Uuml','Xi;','Yacute;','Yacute','Yuml;','Zeta;', + 'aacute;','aacute','acirc;','acirc','acute;','acute','aelig;','aelig', + 'agrave;','agrave','alefsym;','alpha;','amp;','amp','and;','ang;','apos;', + 'aring;','aring','asymp;','atilde;','atilde','auml;','auml','bdquo;','beta;', + 'brvbar;','brvbar','bull;','cap;','ccedil;','ccedil','cedil;','cedil', + 'cent;','cent','chi;','circ;','clubs;','cong;','copy;','copy','crarr;', + 'cup;','curren;','curren','dArr;','dagger;','darr;','deg;','deg','delta;', + 'diams;','divide;','divide','eacute;','eacute','ecirc;','ecirc','egrave;', + 'egrave','empty;','emsp;','ensp;','epsilon;','equiv;','eta;','eth;','eth', + 'euml;','euml','euro;','exist;','fnof;','forall;','frac12;','frac12', + 'frac14;','frac14','frac34;','frac34','frasl;','gamma;','ge;','gt;','gt', + 'hArr;','harr;','hearts;','hellip;','iacute;','iacute','icirc;','icirc', + 'iexcl;','iexcl','igrave;','igrave','image;','infin;','int;','iota;', + 'iquest;','iquest','isin;','iuml;','iuml','kappa;','lArr;','lambda;','lang;', + 'laquo;','laquo','larr;','lceil;','ldquo;','le;','lfloor;','lowast;','loz;', + 'lrm;','lsaquo;','lsquo;','lt;','lt','macr;','macr','mdash;','micro;','micro', + 'middot;','middot','minus;','mu;','nabla;','nbsp;','nbsp','ndash;','ne;', + 'ni;','not;','not','notin;','nsub;','ntilde;','ntilde','nu;','oacute;', + 'oacute','ocirc;','ocirc','oelig;','ograve;','ograve','oline;','omega;', + 'omicron;','oplus;','or;','ordf;','ordf','ordm;','ordm','oslash;','oslash', + 'otilde;','otilde','otimes;','ouml;','ouml','para;','para','part;','permil;', + 'perp;','phi;','pi;','piv;','plusmn;','plusmn','pound;','pound','prime;', + 'prod;','prop;','psi;','quot;','quot','rArr;','radic;','rang;','raquo;', + 'raquo','rarr;','rceil;','rdquo;','real;','reg;','reg','rfloor;','rho;', + 'rlm;','rsaquo;','rsquo;','sbquo;','scaron;','sdot;','sect;','sect','shy;', + 'shy','sigma;','sigmaf;','sim;','spades;','sub;','sube;','sum;','sup1;', + 'sup1','sup2;','sup2','sup3;','sup3','sup;','supe;','szlig;','szlig','tau;', + 'there4;','theta;','thetasym;','thinsp;','thorn;','thorn','tilde;','times;', + 'times','trade;','uArr;','uacute;','uacute','uarr;','ucirc;','ucirc', + 'ugrave;','ugrave','uml;','uml','upsih;','upsilon;','uuml;','uuml','weierp;', + 'xi;','yacute;','yacute','yen;','yen','yuml;','yuml','zeta;','zwj;','zwnj;'); + + const PCDATA = 0; + const RCDATA = 1; + const CDATA = 2; + const PLAINTEXT = 3; + + const DOCTYPE = 0; + const STARTTAG = 1; + const ENDTAG = 2; + const COMMENT = 3; + const CHARACTR = 4; + const EOF = 5; + + public function __construct($data) { + + $this->data = $data; + $this->char = -1; + $this->EOF = strlen($data); + $this->tree = new HTML5TreeConstructer; + $this->content_model = self::PCDATA; + + $this->state = 'data'; + + while($this->state !== null) { + $this->{$this->state.'State'}(); + } + } + + public function save() { + return $this->tree->save(); + } + + private function char() { + return ($this->char < $this->EOF) + ? $this->data[$this->char] + : false; + } + + private function character($s, $l = 0) { + if($s + $l < $this->EOF) { + if($l === 0) { + return $this->data[$s]; + } else { + return substr($this->data, $s, $l); + } + } + } + + private function characters($char_class, $start) { + return preg_replace('#^(['.$char_class.']+).*#s', '\\1', substr($this->data, $start)); + } + + private function dataState() { + // Consume the next input character + $this->char++; + $char = $this->char(); + + if($char === '&' && ($this->content_model === self::PCDATA || $this->content_model === self::RCDATA)) { + /* U+0026 AMPERSAND (&) + When the content model flag is set to one of the PCDATA or RCDATA + states: switch to the entity data state. Otherwise: treat it as per + the "anything else" entry below. */ + $this->state = 'entityData'; + + } elseif($char === '-') { + /* If the content model flag is set to either the RCDATA state or + the CDATA state, and the escape flag is false, and there are at + least three characters before this one in the input stream, and the + last four characters in the input stream, including this one, are + U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+002D HYPHEN-MINUS, + and U+002D HYPHEN-MINUS (""), + set the escape flag to false. */ + if(($this->content_model === self::RCDATA || + $this->content_model === self::CDATA) && $this->escape === true && + $this->character($this->char, 3) === '-->') { + $this->escape = false; + } + + /* In any case, emit the input character as a character token. + Stay in the data state. */ + $this->emitToken(array( + 'type' => self::CHARACTR, + 'data' => $char + )); + + } elseif($this->char === $this->EOF) { + /* EOF + Emit an end-of-file token. */ + $this->EOF(); + + } elseif($this->content_model === self::PLAINTEXT) { + /* When the content model flag is set to the PLAINTEXT state + THIS DIFFERS GREATLY FROM THE SPEC: Get the remaining characters of + the text and emit it as a character token. */ + $this->emitToken(array( + 'type' => self::CHARACTR, + 'data' => substr($this->data, $this->char) + )); + + $this->EOF(); + + } else { + /* Anything else + THIS DIFFERS GREATLY FROM THE SPEC: Get as many character that + otherwise would also be treated as a character token and emit it + as a single character token. Stay in the data state. */ + $len = strcspn($this->data, '<&', $this->char); + $char = substr($this->data, $this->char, $len); + $this->char += $len - 1; + + $this->emitToken(array( + 'type' => self::CHARACTR, + 'data' => $char + )); + + $this->state = 'data'; + } + } + + private function entityDataState() { + // Attempt to consume an entity. + $entity = $this->entity(); + + // If nothing is returned, emit a U+0026 AMPERSAND character token. + // Otherwise, emit the character token that was returned. + $char = (!$entity) ? '&' : $entity; + $this->emitToken(array( + 'type' => self::CHARACTR, + 'data' => $char + )); + + // Finally, switch to the data state. + $this->state = 'data'; + } + + private function tagOpenState() { + switch($this->content_model) { + case self::RCDATA: + case self::CDATA: + /* If the next input character is a U+002F SOLIDUS (/) character, + consume it and switch to the close tag open state. If the next + input character is not a U+002F SOLIDUS (/) character, emit a + U+003C LESS-THAN SIGN character token and switch to the data + state to process the next input character. */ + if($this->character($this->char + 1) === '/') { + $this->char++; + $this->state = 'closeTagOpen'; + + } else { + $this->emitToken(array( + 'type' => self::CHARACTR, + 'data' => '<' + )); + + $this->state = 'data'; + } + break; + + case self::PCDATA: + // If the content model flag is set to the PCDATA state + // Consume the next input character: + $this->char++; + $char = $this->char(); + + if($char === '!') { + /* U+0021 EXCLAMATION MARK (!) + Switch to the markup declaration open state. */ + $this->state = 'markupDeclarationOpen'; + + } elseif($char === '/') { + /* U+002F SOLIDUS (/) + Switch to the close tag open state. */ + $this->state = 'closeTagOpen'; + + } elseif(preg_match('/^[A-Za-z]$/', $char)) { + /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z + Create a new start tag token, set its tag name to the lowercase + version of the input character (add 0x0020 to the character's code + point), then switch to the tag name state. (Don't emit the token + yet; further details will be filled in before it is emitted.) */ + $this->token = array( + 'name' => strtolower($char), + 'type' => self::STARTTAG, + 'attr' => array() + ); + + $this->state = 'tagName'; + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Parse error. Emit a U+003C LESS-THAN SIGN character token and a + U+003E GREATER-THAN SIGN character token. Switch to the data state. */ + $this->emitToken(array( + 'type' => self::CHARACTR, + 'data' => '<>' + )); + + $this->state = 'data'; + + } elseif($char === '?') { + /* U+003F QUESTION MARK (?) + Parse error. Switch to the bogus comment state. */ + $this->state = 'bogusComment'; + + } else { + /* Anything else + Parse error. Emit a U+003C LESS-THAN SIGN character token and + reconsume the current input character in the data state. */ + $this->emitToken(array( + 'type' => self::CHARACTR, + 'data' => '<' + )); + + $this->char--; + $this->state = 'data'; + } + break; + } + } + + private function closeTagOpenState() { + $next_node = strtolower($this->characters('A-Za-z', $this->char + 1)); + $the_same = count($this->tree->stack) > 0 && $next_node === end($this->tree->stack)->nodeName; + + if(($this->content_model === self::RCDATA || $this->content_model === self::CDATA) && + (!$the_same || ($the_same && (!preg_match('/[\t\n\x0b\x0c >\/]/', + $this->character($this->char + 1 + strlen($next_node))) || $this->EOF === $this->char)))) { + /* If the content model flag is set to the RCDATA or CDATA states then + examine the next few characters. If they do not match the tag name of + the last start tag token emitted (case insensitively), or if they do but + they are not immediately followed by one of the following characters: + * U+0009 CHARACTER TABULATION + * U+000A LINE FEED (LF) + * U+000B LINE TABULATION + * U+000C FORM FEED (FF) + * U+0020 SPACE + * U+003E GREATER-THAN SIGN (>) + * U+002F SOLIDUS (/) + * EOF + ...then there is a parse error. Emit a U+003C LESS-THAN SIGN character + token, a U+002F SOLIDUS character token, and switch to the data state + to process the next input character. */ + $this->emitToken(array( + 'type' => self::CHARACTR, + 'data' => 'state = 'data'; + + } else { + /* Otherwise, if the content model flag is set to the PCDATA state, + or if the next few characters do match that tag name, consume the + next input character: */ + $this->char++; + $char = $this->char(); + + if(preg_match('/^[A-Za-z]$/', $char)) { + /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z + Create a new end tag token, set its tag name to the lowercase version + of the input character (add 0x0020 to the character's code point), then + switch to the tag name state. (Don't emit the token yet; further details + will be filled in before it is emitted.) */ + $this->token = array( + 'name' => strtolower($char), + 'type' => self::ENDTAG + ); + + $this->state = 'tagName'; + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Parse error. Switch to the data state. */ + $this->state = 'data'; + + } elseif($this->char === $this->EOF) { + /* EOF + Parse error. Emit a U+003C LESS-THAN SIGN character token and a U+002F + SOLIDUS character token. Reconsume the EOF character in the data state. */ + $this->emitToken(array( + 'type' => self::CHARACTR, + 'data' => 'char--; + $this->state = 'data'; + + } else { + /* Parse error. Switch to the bogus comment state. */ + $this->state = 'bogusComment'; + } + } + } + + private function tagNameState() { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } elseif($char === '/') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Switch to the before + attribute name state. */ + $this->state = 'beforeAttributeName'; + + } else { + /* Anything else + Append the current input character to the current tag token's tag name. + Stay in the tag name state. */ + $this->token['name'] .= strtolower($char); + $this->state = 'tagName'; + } + } + + private function beforeAttributeNameState() { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif($char === '/') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Stay in the before + attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Start a new attribute in the current tag token. Set that attribute's + name to the current input character, and its value to the empty string. + Switch to the attribute name state. */ + $this->token['attr'][] = array( + 'name' => strtolower($char), + 'value' => null + ); + + $this->state = 'attributeName'; + } + } + + private function attributeNameState() { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before attribute name state. */ + $this->state = 'afterAttributeName'; + + } elseif($char === '=') { + /* U+003D EQUALS SIGN (=) + Switch to the before attribute value state. */ + $this->state = 'beforeAttributeValue'; + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif($char === '/' && $this->character($this->char + 1) !== '>') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Switch to the before + attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's name. + Stay in the attribute name state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['name'] .= strtolower($char); + + $this->state = 'attributeName'; + } + } + + private function afterAttributeNameState() { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the after attribute name state. */ + $this->state = 'afterAttributeName'; + + } elseif($char === '=') { + /* U+003D EQUALS SIGN (=) + Switch to the before attribute value state. */ + $this->state = 'beforeAttributeValue'; + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif($char === '/' && $this->character($this->char + 1) !== '>') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Switch to the + before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Start a new attribute in the current tag token. Set that attribute's + name to the current input character, and its value to the empty string. + Switch to the attribute name state. */ + $this->token['attr'][] = array( + 'name' => strtolower($char), + 'value' => null + ); + + $this->state = 'attributeName'; + } + } + + private function beforeAttributeValueState() { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before attribute value state. */ + $this->state = 'beforeAttributeValue'; + + } elseif($char === '"') { + /* U+0022 QUOTATION MARK (") + Switch to the attribute value (double-quoted) state. */ + $this->state = 'attributeValueDoubleQuoted'; + + } elseif($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the attribute value (unquoted) state and reconsume + this input character. */ + $this->char--; + $this->state = 'attributeValueUnquoted'; + + } elseif($char === '\'') { + /* U+0027 APOSTROPHE (') + Switch to the attribute value (single-quoted) state. */ + $this->state = 'attributeValueSingleQuoted'; + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Switch to the attribute value (unquoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueUnquoted'; + } + } + + private function attributeValueDoubleQuotedState() { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if($char === '"') { + /* U+0022 QUOTATION MARK (") + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the entity in attribute value state. */ + $this->entityInAttributeValueState('double'); + + } elseif($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the character + in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Stay in the attribute value (double-quoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueDoubleQuoted'; + } + } + + private function attributeValueSingleQuotedState() { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if($char === '\'') { + /* U+0022 QUOTATION MARK (') + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the entity in attribute value state. */ + $this->entityInAttributeValueState('single'); + + } elseif($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the character + in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Stay in the attribute value (single-quoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueSingleQuoted'; + } + } + + private function attributeValueUnquotedState() { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the entity in attribute value state. */ + $this->entityInAttributeValueState(); + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Stay in the attribute value (unquoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueUnquoted'; + } + } + + private function entityInAttributeValueState() { + // Attempt to consume an entity. + $entity = $this->entity(); + + // If nothing is returned, append a U+0026 AMPERSAND character to the + // current attribute's value. Otherwise, emit the character token that + // was returned. + $char = (!$entity) + ? '&' + : $entity; + + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + } + + private function bogusCommentState() { + /* Consume every character up to the first U+003E GREATER-THAN SIGN + character (>) or the end of the file (EOF), whichever comes first. Emit + a comment token whose data is the concatenation of all the characters + starting from and including the character that caused the state machine + to switch into the bogus comment state, up to and including the last + consumed character before the U+003E character, if any, or up to the + end of the file otherwise. (If the comment was started by the end of + the file (EOF), the token is empty.) */ + $data = $this->characters('^>', $this->char); + $this->emitToken(array( + 'data' => $data, + 'type' => self::COMMENT + )); + + $this->char += strlen($data); + + /* Switch to the data state. */ + $this->state = 'data'; + + /* If the end of the file was reached, reconsume the EOF character. */ + if($this->char === $this->EOF) { + $this->char = $this->EOF - 1; + } + } + + private function markupDeclarationOpenState() { + /* If the next two characters are both U+002D HYPHEN-MINUS (-) + characters, consume those two characters, create a comment token whose + data is the empty string, and switch to the comment state. */ + if($this->character($this->char + 1, 2) === '--') { + $this->char += 2; + $this->state = 'comment'; + $this->token = array( + 'data' => null, + 'type' => self::COMMENT + ); + + /* Otherwise if the next seven chacacters are a case-insensitive match + for the word "DOCTYPE", then consume those characters and switch to the + DOCTYPE state. */ + } elseif(strtolower($this->character($this->char + 1, 7)) === 'doctype') { + $this->char += 7; + $this->state = 'doctype'; + + /* Otherwise, is is a parse error. Switch to the bogus comment state. + The next character that is consumed, if any, is the first character + that will be in the comment. */ + } else { + $this->char++; + $this->state = 'bogusComment'; + } + } + + private function commentState() { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + /* U+002D HYPHEN-MINUS (-) */ + if($char === '-') { + /* Switch to the comment dash state */ + $this->state = 'commentDash'; + + /* EOF */ + } elseif($this->char === $this->EOF) { + /* Parse error. Emit the comment token. Reconsume the EOF character + in the data state. */ + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + /* Anything else */ + } else { + /* Append the input character to the comment token's data. Stay in + the comment state. */ + $this->token['data'] .= $char; + } + } + + private function commentDashState() { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + /* U+002D HYPHEN-MINUS (-) */ + if($char === '-') { + /* Switch to the comment end state */ + $this->state = 'commentEnd'; + + /* EOF */ + } elseif($this->char === $this->EOF) { + /* Parse error. Emit the comment token. Reconsume the EOF character + in the data state. */ + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + /* Anything else */ + } else { + /* Append a U+002D HYPHEN-MINUS (-) character and the input + character to the comment token's data. Switch to the comment state. */ + $this->token['data'] .= '-'.$char; + $this->state = 'comment'; + } + } + + private function commentEndState() { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif($char === '-') { + $this->token['data'] .= '-'; + + } elseif($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + $this->token['data'] .= '--'.$char; + $this->state = 'comment'; + } + } + + private function doctypeState() { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + $this->state = 'beforeDoctypeName'; + + } else { + $this->char--; + $this->state = 'beforeDoctypeName'; + } + } + + private function beforeDoctypeNameState() { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + // Stay in the before DOCTYPE name state. + + } elseif(preg_match('/^[a-z]$/', $char)) { + $this->token = array( + 'name' => strtoupper($char), + 'type' => self::DOCTYPE, + 'error' => true + ); + + $this->state = 'doctypeName'; + + } elseif($char === '>') { + $this->emitToken(array( + 'name' => null, + 'type' => self::DOCTYPE, + 'error' => true + )); + + $this->state = 'data'; + + } elseif($this->char === $this->EOF) { + $this->emitToken(array( + 'name' => null, + 'type' => self::DOCTYPE, + 'error' => true + )); + + $this->char--; + $this->state = 'data'; + + } else { + $this->token = array( + 'name' => $char, + 'type' => self::DOCTYPE, + 'error' => true + ); + + $this->state = 'doctypeName'; + } + } + + private function doctypeNameState() { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + $this->state = 'AfterDoctypeName'; + + } elseif($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif(preg_match('/^[a-z]$/', $char)) { + $this->token['name'] .= strtoupper($char); + + } elseif($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + $this->token['name'] .= $char; + } + + $this->token['error'] = ($this->token['name'] === 'HTML') + ? false + : true; + } + + private function afterDoctypeNameState() { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + // Stay in the DOCTYPE name state. + + } elseif($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + $this->token['error'] = true; + $this->state = 'bogusDoctype'; + } + } + + private function bogusDoctypeState() { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + // Stay in the bogus DOCTYPE state. + } + } + + private function entity() { + $start = $this->char; + + // This section defines how to consume an entity. This definition is + // used when parsing entities in text and in attributes. + + // The behaviour depends on the identity of the next character (the + // one immediately after the U+0026 AMPERSAND character): + + switch($this->character($this->char + 1)) { + // U+0023 NUMBER SIGN (#) + case '#': + + // The behaviour further depends on the character after the + // U+0023 NUMBER SIGN: + switch($this->character($this->char + 1)) { + // U+0078 LATIN SMALL LETTER X + // U+0058 LATIN CAPITAL LETTER X + case 'x': + case 'X': + // Follow the steps below, but using the range of + // characters U+0030 DIGIT ZERO through to U+0039 DIGIT + // NINE, U+0061 LATIN SMALL LETTER A through to U+0066 + // LATIN SMALL LETTER F, and U+0041 LATIN CAPITAL LETTER + // A, through to U+0046 LATIN CAPITAL LETTER F (in other + // words, 0-9, A-F, a-f). + $char = 1; + $char_class = '0-9A-Fa-f'; + break; + + // Anything else + default: + // Follow the steps below, but using the range of + // characters U+0030 DIGIT ZERO through to U+0039 DIGIT + // NINE (i.e. just 0-9). + $char = 0; + $char_class = '0-9'; + break; + } + + // Consume as many characters as match the range of characters + // given above. + $this->char++; + $e_name = $this->characters($char_class, $this->char + $char + 1); + $entity = $this->character($start, $this->char); + $cond = strlen($e_name) > 0; + + // The rest of the parsing happens bellow. + break; + + // Anything else + default: + // Consume the maximum number of characters possible, with the + // consumed characters case-sensitively matching one of the + // identifiers in the first column of the entities table. + $e_name = $this->characters('0-9A-Za-z;', $this->char + 1); + $len = strlen($e_name); + + for($c = 1; $c <= $len; $c++) { + $id = substr($e_name, 0, $c); + $this->char++; + + if(in_array($id, $this->entities)) { + if ($e_name[$c-1] !== ';') { + if ($c < $len && $e_name[$c] == ';') { + $this->char++; // consume extra semicolon + } + } + $entity = $id; + break; + } + } + + $cond = isset($entity); + // The rest of the parsing happens bellow. + break; + } + + if(!$cond) { + // If no match can be made, then this is a parse error. No + // characters are consumed, and nothing is returned. + $this->char = $start; + return false; + } + + // Return a character token for the character corresponding to the + // entity name (as given by the second column of the entities table). + return html_entity_decode('&'.$entity.';', ENT_QUOTES, 'UTF-8'); + } + + private function emitToken($token) { + $emit = $this->tree->emitToken($token); + + if(is_int($emit)) { + $this->content_model = $emit; + + } elseif($token['type'] === self::ENDTAG) { + $this->content_model = self::PCDATA; + } + } + + private function EOF() { + $this->state = null; + $this->tree->emitToken(array( + 'type' => self::EOF + )); + } +} + +class HTML5TreeConstructer { + public $stack = array(); + + private $phase; + private $mode; + private $dom; + private $foster_parent = null; + private $a_formatting = array(); + + private $head_pointer = null; + private $form_pointer = null; + + private $scoping = array('button','caption','html','marquee','object','table','td','th'); + private $formatting = array('a','b','big','em','font','i','nobr','s','small','strike','strong','tt','u'); + private $special = array('address','area','base','basefont','bgsound', + 'blockquote','body','br','center','col','colgroup','dd','dir','div','dl', + 'dt','embed','fieldset','form','frame','frameset','h1','h2','h3','h4','h5', + 'h6','head','hr','iframe','image','img','input','isindex','li','link', + 'listing','menu','meta','noembed','noframes','noscript','ol','optgroup', + 'option','p','param','plaintext','pre','script','select','spacer','style', + 'tbody','textarea','tfoot','thead','title','tr','ul','wbr'); + + // The different phases. + const INIT_PHASE = 0; + const ROOT_PHASE = 1; + const MAIN_PHASE = 2; + const END_PHASE = 3; + + // The different insertion modes for the main phase. + const BEFOR_HEAD = 0; + const IN_HEAD = 1; + const AFTER_HEAD = 2; + const IN_BODY = 3; + const IN_TABLE = 4; + const IN_CAPTION = 5; + const IN_CGROUP = 6; + const IN_TBODY = 7; + const IN_ROW = 8; + const IN_CELL = 9; + const IN_SELECT = 10; + const AFTER_BODY = 11; + const IN_FRAME = 12; + const AFTR_FRAME = 13; + + // The different types of elements. + const SPECIAL = 0; + const SCOPING = 1; + const FORMATTING = 2; + const PHRASING = 3; + + const MARKER = 0; + + public function __construct() { + $this->phase = self::INIT_PHASE; + $this->mode = self::BEFOR_HEAD; + $this->dom = new DOMDocument; + + $this->dom->encoding = 'UTF-8'; + $this->dom->preserveWhiteSpace = true; + $this->dom->substituteEntities = true; + $this->dom->strictErrorChecking = false; + } + + // Process tag tokens + public function emitToken($token) { + switch($this->phase) { + case self::INIT_PHASE: return $this->initPhase($token); break; + case self::ROOT_PHASE: return $this->rootElementPhase($token); break; + case self::MAIN_PHASE: return $this->mainPhase($token); break; + case self::END_PHASE : return $this->trailingEndPhase($token); break; + } + } + + private function initPhase($token) { + /* Initially, the tree construction stage must handle each token + emitted from the tokenisation stage as follows: */ + + /* A DOCTYPE token that is marked as being in error + A comment token + A start tag token + An end tag token + A character token that is not one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE + An end-of-file token */ + if((isset($token['error']) && $token['error']) || + $token['type'] === HTML5::COMMENT || + $token['type'] === HTML5::STARTTAG || + $token['type'] === HTML5::ENDTAG || + $token['type'] === HTML5::EOF || + ($token['type'] === HTML5::CHARACTR && isset($token['data']) && + !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']))) { + /* This specification does not define how to handle this case. In + particular, user agents may ignore the entirety of this specification + altogether for such documents, and instead invoke special parse modes + with a greater emphasis on backwards compatibility. */ + + $this->phase = self::ROOT_PHASE; + return $this->rootElementPhase($token); + + /* A DOCTYPE token marked as being correct */ + } elseif(isset($token['error']) && !$token['error']) { + /* Append a DocumentType node to the Document node, with the name + attribute set to the name given in the DOCTYPE token (which will be + "HTML"), and the other attributes specific to DocumentType objects + set to null, empty lists, or the empty string as appropriate. */ + $doctype = new DOMDocumentType(null, null, 'HTML'); + + /* Then, switch to the root element phase of the tree construction + stage. */ + $this->phase = self::ROOT_PHASE; + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + } elseif(isset($token['data']) && preg_match('/^[\t\n\x0b\x0c ]+$/', + $token['data'])) { + /* Append that character to the Document node. */ + $text = $this->dom->createTextNode($token['data']); + $this->dom->appendChild($text); + } + } + + private function rootElementPhase($token) { + /* After the initial phase, as each token is emitted from the tokenisation + stage, it must be processed as described in this section. */ + + /* A DOCTYPE token */ + if($token['type'] === HTML5::DOCTYPE) { + // Parse error. Ignore the token. + + /* A comment token */ + } elseif($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the Document object with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + $this->dom->appendChild($comment); + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + } elseif($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { + /* Append that character to the Document node. */ + $text = $this->dom->createTextNode($token['data']); + $this->dom->appendChild($text); + + /* A character token that is not one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED + (FF), or U+0020 SPACE + A start tag token + An end tag token + An end-of-file token */ + } elseif(($token['type'] === HTML5::CHARACTR && + !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || + $token['type'] === HTML5::STARTTAG || + $token['type'] === HTML5::ENDTAG || + $token['type'] === HTML5::EOF) { + /* Create an HTMLElement node with the tag name html, in the HTML + namespace. Append it to the Document object. Switch to the main + phase and reprocess the current token. */ + $html = $this->dom->createElement('html'); + $this->dom->appendChild($html); + $this->stack[] = $html; + + $this->phase = self::MAIN_PHASE; + return $this->mainPhase($token); + } + } + + private function mainPhase($token) { + /* Tokens in the main phase must be handled as follows: */ + + /* A DOCTYPE token */ + if($token['type'] === HTML5::DOCTYPE) { + // Parse error. Ignore the token. + + /* A start tag token with the tag name "html" */ + } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'html') { + /* If this start tag token was not the first start tag token, then + it is a parse error. */ + + /* For each attribute on the token, check to see if the attribute + is already present on the top element of the stack of open elements. + If it is not, add the attribute and its corresponding value to that + element. */ + foreach($token['attr'] as $attr) { + if(!$this->stack[0]->hasAttribute($attr['name'])) { + $this->stack[0]->setAttribute($attr['name'], $attr['value']); + } + } + + /* An end-of-file token */ + } elseif($token['type'] === HTML5::EOF) { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* Anything else. */ + } else { + /* Depends on the insertion mode: */ + switch($this->mode) { + case self::BEFOR_HEAD: return $this->beforeHead($token); break; + case self::IN_HEAD: return $this->inHead($token); break; + case self::AFTER_HEAD: return $this->afterHead($token); break; + case self::IN_BODY: return $this->inBody($token); break; + case self::IN_TABLE: return $this->inTable($token); break; + case self::IN_CAPTION: return $this->inCaption($token); break; + case self::IN_CGROUP: return $this->inColumnGroup($token); break; + case self::IN_TBODY: return $this->inTableBody($token); break; + case self::IN_ROW: return $this->inRow($token); break; + case self::IN_CELL: return $this->inCell($token); break; + case self::IN_SELECT: return $this->inSelect($token); break; + case self::AFTER_BODY: return $this->afterBody($token); break; + case self::IN_FRAME: return $this->inFrameset($token); break; + case self::AFTR_FRAME: return $this->afterFrameset($token); break; + case self::END_PHASE: return $this->trailingEndPhase($token); break; + } + } + } + + private function beforeHead($token) { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data attribute + set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag token with the tag name "head" */ + } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') { + /* Create an element for the token, append the new element to the + current node and push it onto the stack of open elements. */ + $element = $this->insertElement($token); + + /* Set the head element pointer to this new element node. */ + $this->head_pointer = $element; + + /* Change the insertion mode to "in head". */ + $this->mode = self::IN_HEAD; + + /* A start tag token whose tag name is one of: "base", "link", "meta", + "script", "style", "title". Or an end tag with the tag name "html". + Or a character token that is not one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE. Or any other start tag token */ + } elseif($token['type'] === HTML5::STARTTAG || + ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') || + ($token['type'] === HTML5::CHARACTR && !preg_match('/^[\t\n\x0b\x0c ]$/', + $token['data']))) { + /* Act as if a start tag token with the tag name "head" and no + attributes had been seen, then reprocess the current token. */ + $this->beforeHead(array( + 'name' => 'head', + 'type' => HTML5::STARTTAG, + 'attr' => array() + )); + + return $this->inHead($token); + + /* Any other end tag */ + } elseif($token['type'] === HTML5::ENDTAG) { + /* Parse error. Ignore the token. */ + } + } + + private function inHead($token) { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE. + + THIS DIFFERS FROM THE SPEC: If the current node is either a title, style + or script element, append the character to the current node regardless + of its content. */ + if(($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || ( + $token['type'] === HTML5::CHARACTR && in_array(end($this->stack)->nodeName, + array('title', 'style', 'script')))) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data attribute + set to the data given in the comment token. */ + $this->insertComment($token['data']); + + } elseif($token['type'] === HTML5::ENDTAG && + in_array($token['name'], array('title', 'style', 'script'))) { + array_pop($this->stack); + return HTML5::PCDATA; + + /* A start tag with the tag name "title" */ + } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'title') { + /* Create an element for the token and append the new element to the + node pointed to by the head element pointer, or, if that is null + (innerHTML case), to the current node. */ + if($this->head_pointer !== null) { + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + + } else { + $element = $this->insertElement($token); + } + + /* Switch the tokeniser's content model flag to the RCDATA state. */ + return HTML5::RCDATA; + + /* A start tag with the tag name "style" */ + } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'style') { + /* Create an element for the token and append the new element to the + node pointed to by the head element pointer, or, if that is null + (innerHTML case), to the current node. */ + if($this->head_pointer !== null) { + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + + } else { + $this->insertElement($token); + } + + /* Switch the tokeniser's content model flag to the CDATA state. */ + return HTML5::CDATA; + + /* A start tag with the tag name "script" */ + } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'script') { + /* Create an element for the token. */ + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + + /* Switch the tokeniser's content model flag to the CDATA state. */ + return HTML5::CDATA; + + /* A start tag with the tag name "base", "link", or "meta" */ + } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], + array('base', 'link', 'meta'))) { + /* Create an element for the token and append the new element to the + node pointed to by the head element pointer, or, if that is null + (innerHTML case), to the current node. */ + if($this->head_pointer !== null) { + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + array_pop($this->stack); + + } else { + $this->insertElement($token); + } + + /* An end tag with the tag name "head" */ + } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'head') { + /* If the current node is a head element, pop the current node off + the stack of open elements. */ + if($this->head_pointer->isSameNode(end($this->stack))) { + array_pop($this->stack); + + /* Otherwise, this is a parse error. */ + } else { + // k + } + + /* Change the insertion mode to "after head". */ + $this->mode = self::AFTER_HEAD; + + /* A start tag with the tag name "head" or an end tag except "html". */ + } elseif(($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') || + ($token['type'] === HTML5::ENDTAG && $token['name'] !== 'html')) { + // Parse error. Ignore the token. + + /* Anything else */ + } else { + /* If the current node is a head element, act as if an end tag + token with the tag name "head" had been seen. */ + if($this->head_pointer->isSameNode(end($this->stack))) { + $this->inHead(array( + 'name' => 'head', + 'type' => HTML5::ENDTAG + )); + + /* Otherwise, change the insertion mode to "after head". */ + } else { + $this->mode = self::AFTER_HEAD; + } + + /* Then, reprocess the current token. */ + return $this->afterHead($token); + } + } + + private function afterHead($token) { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data attribute + set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag token with the tag name "body" */ + } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'body') { + /* Insert a body element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in body". */ + $this->mode = self::IN_BODY; + + /* A start tag token with the tag name "frameset" */ + } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'frameset') { + /* Insert a frameset element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in frameset". */ + $this->mode = self::IN_FRAME; + + /* A start tag token whose tag name is one of: "base", "link", "meta", + "script", "style", "title" */ + } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], + array('base', 'link', 'meta', 'script', 'style', 'title'))) { + /* Parse error. Switch the insertion mode back to "in head" and + reprocess the token. */ + $this->mode = self::IN_HEAD; + return $this->inHead($token); + + /* Anything else */ + } else { + /* Act as if a start tag token with the tag name "body" and no + attributes had been seen, and then reprocess the current token. */ + $this->afterHead(array( + 'name' => 'body', + 'type' => HTML5::STARTTAG, + 'attr' => array() + )); + + return $this->inBody($token); + } + } + + private function inBody($token) { + /* Handle the token as follows: */ + + switch($token['type']) { + /* A character token */ + case HTML5::CHARACTR: + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Append the token's character to the current node. */ + $this->insertText($token['data']); + break; + + /* A comment token */ + case HTML5::COMMENT: + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + break; + + case HTML5::STARTTAG: + switch($token['name']) { + /* A start tag token whose tag name is one of: "script", + "style" */ + case 'script': case 'style': + /* Process the token as if the insertion mode had been "in + head". */ + return $this->inHead($token); + break; + + /* A start tag token whose tag name is one of: "base", "link", + "meta", "title" */ + case 'base': case 'link': case 'meta': case 'title': + /* Parse error. Process the token as if the insertion mode + had been "in head". */ + return $this->inHead($token); + break; + + /* A start tag token with the tag name "body" */ + case 'body': + /* Parse error. If the second element on the stack of open + elements is not a body element, or, if the stack of open + elements has only one node on it, then ignore the token. + (innerHTML case) */ + if(count($this->stack) === 1 || $this->stack[1]->nodeName !== 'body') { + // Ignore + + /* Otherwise, for each attribute on the token, check to see + if the attribute is already present on the body element (the + second element) on the stack of open elements. If it is not, + add the attribute and its corresponding value to that + element. */ + } else { + foreach($token['attr'] as $attr) { + if(!$this->stack[1]->hasAttribute($attr['name'])) { + $this->stack[1]->setAttribute($attr['name'], $attr['value']); + } + } + } + break; + + /* A start tag whose tag name is one of: "address", + "blockquote", "center", "dir", "div", "dl", "fieldset", + "listing", "menu", "ol", "p", "ul" */ + case 'address': case 'blockquote': case 'center': case 'dir': + case 'div': case 'dl': case 'fieldset': case 'listing': + case 'menu': case 'ol': case 'p': case 'ul': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been + seen. */ + if($this->elementInScope('p')) { + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + )); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + break; + + /* A start tag whose tag name is "form" */ + case 'form': + /* If the form element pointer is not null, ignore the + token with a parse error. */ + if($this->form_pointer !== null) { + // Ignore. + + /* Otherwise: */ + } else { + /* If the stack of open elements has a p element in + scope, then act as if an end tag with the tag name p + had been seen. */ + if($this->elementInScope('p')) { + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + )); + } + + /* Insert an HTML element for the token, and set the + form element pointer to point to the element created. */ + $element = $this->insertElement($token); + $this->form_pointer = $element; + } + break; + + /* A start tag whose tag name is "li", "dd" or "dt" */ + case 'li': case 'dd': case 'dt': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been + seen. */ + if($this->elementInScope('p')) { + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + )); + } + + $stack_length = count($this->stack) - 1; + + for($n = $stack_length; 0 <= $n; $n--) { + /* 1. Initialise node to be the current node (the + bottommost node of the stack). */ + $stop = false; + $node = $this->stack[$n]; + $cat = $this->getElementCategory($node->tagName); + + /* 2. If node is an li, dd or dt element, then pop all + the nodes from the current node up to node, including + node, then stop this algorithm. */ + if($token['name'] === $node->tagName || ($token['name'] !== 'li' + && ($node->tagName === 'dd' || $node->tagName === 'dt'))) { + for($x = $stack_length; $x >= $n ; $x--) { + array_pop($this->stack); + } + + break; + } + + /* 3. If node is not in the formatting category, and is + not in the phrasing category, and is not an address or + div element, then stop this algorithm. */ + if($cat !== self::FORMATTING && $cat !== self::PHRASING && + $node->tagName !== 'address' && $node->tagName !== 'div') { + break; + } + } + + /* Finally, insert an HTML element with the same tag + name as the token's. */ + $this->insertElement($token); + break; + + /* A start tag token whose tag name is "plaintext" */ + case 'plaintext': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been + seen. */ + if($this->elementInScope('p')) { + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + )); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + return HTML5::PLAINTEXT; + break; + + /* A start tag whose tag name is one of: "h1", "h2", "h3", "h4", + "h5", "h6" */ + case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been seen. */ + if($this->elementInScope('p')) { + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + )); + } + + /* If the stack of open elements has in scope an element whose + tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then + this is a parse error; pop elements from the stack until an + element with one of those tag names has been popped from the + stack. */ + while($this->elementInScope(array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'))) { + array_pop($this->stack); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + break; + + /* A start tag whose tag name is "a" */ + case 'a': + /* If the list of active formatting elements contains + an element whose tag name is "a" between the end of the + list and the last marker on the list (or the start of + the list if there is no marker on the list), then this + is a parse error; act as if an end tag with the tag name + "a" had been seen, then remove that element from the list + of active formatting elements and the stack of open + elements if the end tag didn't already remove it (it + might not have if the element is not in table scope). */ + $leng = count($this->a_formatting); + + for($n = $leng - 1; $n >= 0; $n--) { + if($this->a_formatting[$n] === self::MARKER) { + break; + + } elseif($this->a_formatting[$n]->nodeName === 'a') { + $this->emitToken(array( + 'name' => 'a', + 'type' => HTML5::ENDTAG + )); + break; + } + } + + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $el = $this->insertElement($token); + + /* Add that element to the list of active formatting + elements. */ + $this->a_formatting[] = $el; + break; + + /* A start tag whose tag name is one of: "b", "big", "em", "font", + "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ + case 'b': case 'big': case 'em': case 'font': case 'i': + case 'nobr': case 's': case 'small': case 'strike': + case 'strong': case 'tt': case 'u': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $el = $this->insertElement($token); + + /* Add that element to the list of active formatting + elements. */ + $this->a_formatting[] = $el; + break; + + /* A start tag token whose tag name is "button" */ + case 'button': + /* If the stack of open elements has a button element in scope, + then this is a parse error; act as if an end tag with the tag + name "button" had been seen, then reprocess the token. (We don't + do that. Unnecessary.) */ + if($this->elementInScope('button')) { + $this->inBody(array( + 'name' => 'button', + 'type' => HTML5::ENDTAG + )); + } + + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Insert a marker at the end of the list of active + formatting elements. */ + $this->a_formatting[] = self::MARKER; + break; + + /* A start tag token whose tag name is one of: "marquee", "object" */ + case 'marquee': case 'object': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Insert a marker at the end of the list of active + formatting elements. */ + $this->a_formatting[] = self::MARKER; + break; + + /* A start tag token whose tag name is "xmp" */ + case 'xmp': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Switch the content model flag to the CDATA state. */ + return HTML5::CDATA; + break; + + /* A start tag whose tag name is "table" */ + case 'table': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been seen. */ + if($this->elementInScope('p')) { + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + )); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in table". */ + $this->mode = self::IN_TABLE; + break; + + /* A start tag whose tag name is one of: "area", "basefont", + "bgsound", "br", "embed", "img", "param", "spacer", "wbr" */ + case 'area': case 'basefont': case 'bgsound': case 'br': + case 'embed': case 'img': case 'param': case 'spacer': + case 'wbr': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Immediately pop the current node off the stack of open elements. */ + array_pop($this->stack); + break; + + /* A start tag whose tag name is "hr" */ + case 'hr': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been seen. */ + if($this->elementInScope('p')) { + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + )); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Immediately pop the current node off the stack of open elements. */ + array_pop($this->stack); + break; + + /* A start tag whose tag name is "image" */ + case 'image': + /* Parse error. Change the token's tag name to "img" and + reprocess it. (Don't ask.) */ + $token['name'] = 'img'; + return $this->inBody($token); + break; + + /* A start tag whose tag name is "input" */ + case 'input': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an input element for the token. */ + $element = $this->insertElement($token, false); + + /* If the form element pointer is not null, then associate the + input element with the form element pointed to by the form + element pointer. */ + $this->form_pointer !== null + ? $this->form_pointer->appendChild($element) + : end($this->stack)->appendChild($element); + + /* Pop that input element off the stack of open elements. */ + array_pop($this->stack); + break; + + /* A start tag whose tag name is "isindex" */ + case 'isindex': + /* Parse error. */ + // w/e + + /* If the form element pointer is not null, + then ignore the token. */ + if($this->form_pointer === null) { + /* Act as if a start tag token with the tag name "form" had + been seen. */ + $this->inBody(array( + 'name' => 'body', + 'type' => HTML5::STARTTAG, + 'attr' => array() + )); + + /* Act as if a start tag token with the tag name "hr" had + been seen. */ + $this->inBody(array( + 'name' => 'hr', + 'type' => HTML5::STARTTAG, + 'attr' => array() + )); + + /* Act as if a start tag token with the tag name "p" had + been seen. */ + $this->inBody(array( + 'name' => 'p', + 'type' => HTML5::STARTTAG, + 'attr' => array() + )); + + /* Act as if a start tag token with the tag name "label" + had been seen. */ + $this->inBody(array( + 'name' => 'label', + 'type' => HTML5::STARTTAG, + 'attr' => array() + )); + + /* Act as if a stream of character tokens had been seen. */ + $this->insertText('This is a searchable index. '. + 'Insert your search keywords here: '); + + /* Act as if a start tag token with the tag name "input" + had been seen, with all the attributes from the "isindex" + token, except with the "name" attribute set to the value + "isindex" (ignoring any explicit "name" attribute). */ + $attr = $token['attr']; + $attr[] = array('name' => 'name', 'value' => 'isindex'); + + $this->inBody(array( + 'name' => 'input', + 'type' => HTML5::STARTTAG, + 'attr' => $attr + )); + + /* Act as if a stream of character tokens had been seen + (see below for what they should say). */ + $this->insertText('This is a searchable index. '. + 'Insert your search keywords here: '); + + /* Act as if an end tag token with the tag name "label" + had been seen. */ + $this->inBody(array( + 'name' => 'label', + 'type' => HTML5::ENDTAG + )); + + /* Act as if an end tag token with the tag name "p" had + been seen. */ + $this->inBody(array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + )); + + /* Act as if a start tag token with the tag name "hr" had + been seen. */ + $this->inBody(array( + 'name' => 'hr', + 'type' => HTML5::ENDTAG + )); + + /* Act as if an end tag token with the tag name "form" had + been seen. */ + $this->inBody(array( + 'name' => 'form', + 'type' => HTML5::ENDTAG + )); + } + break; + + /* A start tag whose tag name is "textarea" */ + case 'textarea': + $this->insertElement($token); + + /* Switch the tokeniser's content model flag to the + RCDATA state. */ + return HTML5::RCDATA; + break; + + /* A start tag whose tag name is one of: "iframe", "noembed", + "noframes" */ + case 'iframe': case 'noembed': case 'noframes': + $this->insertElement($token); + + /* Switch the tokeniser's content model flag to the CDATA state. */ + return HTML5::CDATA; + break; + + /* A start tag whose tag name is "select" */ + case 'select': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in select". */ + $this->mode = self::IN_SELECT; + break; + + /* A start or end tag whose tag name is one of: "caption", "col", + "colgroup", "frame", "frameset", "head", "option", "optgroup", + "tbody", "td", "tfoot", "th", "thead", "tr". */ + case 'caption': case 'col': case 'colgroup': case 'frame': + case 'frameset': case 'head': case 'option': case 'optgroup': + case 'tbody': case 'td': case 'tfoot': case 'th': case 'thead': + case 'tr': + // Parse error. Ignore the token. + break; + + /* A start or end tag whose tag name is one of: "event-source", + "section", "nav", "article", "aside", "header", "footer", + "datagrid", "command" */ + case 'event-source': case 'section': case 'nav': case 'article': + case 'aside': case 'header': case 'footer': case 'datagrid': + case 'command': + // Work in progress! + break; + + /* A start tag token not covered by the previous entries */ + default: + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + $this->insertElement($token, true, true); + break; + } + break; + + case HTML5::ENDTAG: + switch($token['name']) { + /* An end tag with the tag name "body" */ + case 'body': + /* If the second element in the stack of open elements is + not a body element, this is a parse error. Ignore the token. + (innerHTML case) */ + if(count($this->stack) < 2 || $this->stack[1]->nodeName !== 'body') { + // Ignore. + + /* If the current node is not the body element, then this + is a parse error. */ + } elseif(end($this->stack)->nodeName !== 'body') { + // Parse error. + } + + /* Change the insertion mode to "after body". */ + $this->mode = self::AFTER_BODY; + break; + + /* An end tag with the tag name "html" */ + case 'html': + /* Act as if an end tag with tag name "body" had been seen, + then, if that token wasn't ignored, reprocess the current + token. */ + $this->inBody(array( + 'name' => 'body', + 'type' => HTML5::ENDTAG + )); + + return $this->afterBody($token); + break; + + /* An end tag whose tag name is one of: "address", "blockquote", + "center", "dir", "div", "dl", "fieldset", "listing", "menu", + "ol", "pre", "ul" */ + case 'address': case 'blockquote': case 'center': case 'dir': + case 'div': case 'dl': case 'fieldset': case 'listing': + case 'menu': case 'ol': case 'pre': case 'ul': + /* If the stack of open elements has an element in scope + with the same tag name as that of the token, then generate + implied end tags. */ + if($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(); + + /* Now, if the current node is not an element with + the same tag name as that of the token, then this + is a parse error. */ + // w/e + + /* If the stack of open elements has an element in + scope with the same tag name as that of the token, + then pop elements from this stack until an element + with that tag name has been popped from the stack. */ + for($n = count($this->stack) - 1; $n >= 0; $n--) { + if($this->stack[$n]->nodeName === $token['name']) { + $n = -1; + } + + array_pop($this->stack); + } + } + break; + + /* An end tag whose tag name is "form" */ + case 'form': + /* If the stack of open elements has an element in scope + with the same tag name as that of the token, then generate + implied end tags. */ + if($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(); + + } + + if(end($this->stack)->nodeName !== $token['name']) { + /* Now, if the current node is not an element with the + same tag name as that of the token, then this is a parse + error. */ + // w/e + + } else { + /* Otherwise, if the current node is an element with + the same tag name as that of the token pop that element + from the stack. */ + array_pop($this->stack); + } + + /* In any case, set the form element pointer to null. */ + $this->form_pointer = null; + break; + + /* An end tag whose tag name is "p" */ + case 'p': + /* If the stack of open elements has a p element in scope, + then generate implied end tags, except for p elements. */ + if($this->elementInScope('p')) { + $this->generateImpliedEndTags(array('p')); + + /* If the current node is not a p element, then this is + a parse error. */ + // k + + /* If the stack of open elements has a p element in + scope, then pop elements from this stack until the stack + no longer has a p element in scope. */ + for($n = count($this->stack) - 1; $n >= 0; $n--) { + if($this->elementInScope('p')) { + array_pop($this->stack); + + } else { + break; + } + } + } + break; + + /* An end tag whose tag name is "dd", "dt", or "li" */ + case 'dd': case 'dt': case 'li': + /* If the stack of open elements has an element in scope + whose tag name matches the tag name of the token, then + generate implied end tags, except for elements with the + same tag name as the token. */ + if($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(array($token['name'])); + + /* If the current node is not an element with the same + tag name as the token, then this is a parse error. */ + // w/e + + /* If the stack of open elements has an element in scope + whose tag name matches the tag name of the token, then + pop elements from this stack until an element with that + tag name has been popped from the stack. */ + for($n = count($this->stack) - 1; $n >= 0; $n--) { + if($this->stack[$n]->nodeName === $token['name']) { + $n = -1; + } + + array_pop($this->stack); + } + } + break; + + /* An end tag whose tag name is one of: "h1", "h2", "h3", "h4", + "h5", "h6" */ + case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': + $elements = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'); + + /* If the stack of open elements has in scope an element whose + tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then + generate implied end tags. */ + if($this->elementInScope($elements)) { + $this->generateImpliedEndTags(); + + /* Now, if the current node is not an element with the same + tag name as that of the token, then this is a parse error. */ + // w/e + + /* If the stack of open elements has in scope an element + whose tag name is one of "h1", "h2", "h3", "h4", "h5", or + "h6", then pop elements from the stack until an element + with one of those tag names has been popped from the stack. */ + while($this->elementInScope($elements)) { + array_pop($this->stack); + } + } + break; + + /* An end tag whose tag name is one of: "a", "b", "big", "em", + "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ + case 'a': case 'b': case 'big': case 'em': case 'font': + case 'i': case 'nobr': case 's': case 'small': case 'strike': + case 'strong': case 'tt': case 'u': + /* 1. Let the formatting element be the last element in + the list of active formatting elements that: + * is between the end of the list and the last scope + marker in the list, if any, or the start of the list + otherwise, and + * has the same tag name as the token. + */ + while(true) { + for($a = count($this->a_formatting) - 1; $a >= 0; $a--) { + if($this->a_formatting[$a] === self::MARKER) { + break; + + } elseif($this->a_formatting[$a]->tagName === $token['name']) { + $formatting_element = $this->a_formatting[$a]; + $in_stack = in_array($formatting_element, $this->stack, true); + $fe_af_pos = $a; + break; + } + } + + /* If there is no such node, or, if that node is + also in the stack of open elements but the element + is not in scope, then this is a parse error. Abort + these steps. The token is ignored. */ + if(!isset($formatting_element) || ($in_stack && + !$this->elementInScope($token['name']))) { + break; + + /* Otherwise, if there is such a node, but that node + is not in the stack of open elements, then this is a + parse error; remove the element from the list, and + abort these steps. */ + } elseif(isset($formatting_element) && !$in_stack) { + unset($this->a_formatting[$fe_af_pos]); + $this->a_formatting = array_merge($this->a_formatting); + break; + } + + /* 2. Let the furthest block be the topmost node in the + stack of open elements that is lower in the stack + than the formatting element, and is not an element in + the phrasing or formatting categories. There might + not be one. */ + $fe_s_pos = array_search($formatting_element, $this->stack, true); + $length = count($this->stack); + + for($s = $fe_s_pos + 1; $s < $length; $s++) { + $category = $this->getElementCategory($this->stack[$s]->nodeName); + + if($category !== self::PHRASING && $category !== self::FORMATTING) { + $furthest_block = $this->stack[$s]; + } + } + + /* 3. If there is no furthest block, then the UA must + skip the subsequent steps and instead just pop all + the nodes from the bottom of the stack of open + elements, from the current node up to the formatting + element, and remove the formatting element from the + list of active formatting elements. */ + if(!isset($furthest_block)) { + for($n = $length - 1; $n >= $fe_s_pos; $n--) { + array_pop($this->stack); + } + + unset($this->a_formatting[$fe_af_pos]); + $this->a_formatting = array_merge($this->a_formatting); + break; + } + + /* 4. Let the common ancestor be the element + immediately above the formatting element in the stack + of open elements. */ + $common_ancestor = $this->stack[$fe_s_pos - 1]; + + /* 5. If the furthest block has a parent node, then + remove the furthest block from its parent node. */ + if($furthest_block->parentNode !== null) { + $furthest_block->parentNode->removeChild($furthest_block); + } + + /* 6. Let a bookmark note the position of the + formatting element in the list of active formatting + elements relative to the elements on either side + of it in the list. */ + $bookmark = $fe_af_pos; + + /* 7. Let node and last node be the furthest block. + Follow these steps: */ + $node = $furthest_block; + $last_node = $furthest_block; + + while(true) { + for($n = array_search($node, $this->stack, true) - 1; $n >= 0; $n--) { + /* 7.1 Let node be the element immediately + prior to node in the stack of open elements. */ + $node = $this->stack[$n]; + + /* 7.2 If node is not in the list of active + formatting elements, then remove node from + the stack of open elements and then go back + to step 1. */ + if(!in_array($node, $this->a_formatting, true)) { + unset($this->stack[$n]); + $this->stack = array_merge($this->stack); + + } else { + break; + } + } + + /* 7.3 Otherwise, if node is the formatting + element, then go to the next step in the overall + algorithm. */ + if($node === $formatting_element) { + break; + + /* 7.4 Otherwise, if last node is the furthest + block, then move the aforementioned bookmark to + be immediately after the node in the list of + active formatting elements. */ + } elseif($last_node === $furthest_block) { + $bookmark = array_search($node, $this->a_formatting, true) + 1; + } + + /* 7.5 If node has any children, perform a + shallow clone of node, replace the entry for + node in the list of active formatting elements + with an entry for the clone, replace the entry + for node in the stack of open elements with an + entry for the clone, and let node be the clone. */ + if($node->hasChildNodes()) { + $clone = $node->cloneNode(); + $s_pos = array_search($node, $this->stack, true); + $a_pos = array_search($node, $this->a_formatting, true); + + $this->stack[$s_pos] = $clone; + $this->a_formatting[$a_pos] = $clone; + $node = $clone; + } + + /* 7.6 Insert last node into node, first removing + it from its previous parent node if any. */ + if($last_node->parentNode !== null) { + $last_node->parentNode->removeChild($last_node); + } + + $node->appendChild($last_node); + + /* 7.7 Let last node be node. */ + $last_node = $node; + } + + /* 8. Insert whatever last node ended up being in + the previous step into the common ancestor node, + first removing it from its previous parent node if + any. */ + if($last_node->parentNode !== null) { + $last_node->parentNode->removeChild($last_node); + } + + $common_ancestor->appendChild($last_node); + + /* 9. Perform a shallow clone of the formatting + element. */ + $clone = $formatting_element->cloneNode(); + + /* 10. Take all of the child nodes of the furthest + block and append them to the clone created in the + last step. */ + while($furthest_block->hasChildNodes()) { + $child = $furthest_block->firstChild; + $furthest_block->removeChild($child); + $clone->appendChild($child); + } + + /* 11. Append that clone to the furthest block. */ + $furthest_block->appendChild($clone); + + /* 12. Remove the formatting element from the list + of active formatting elements, and insert the clone + into the list of active formatting elements at the + position of the aforementioned bookmark. */ + $fe_af_pos = array_search($formatting_element, $this->a_formatting, true); + unset($this->a_formatting[$fe_af_pos]); + $this->a_formatting = array_merge($this->a_formatting); + + $af_part1 = array_slice($this->a_formatting, 0, $bookmark - 1); + $af_part2 = array_slice($this->a_formatting, $bookmark, count($this->a_formatting)); + $this->a_formatting = array_merge($af_part1, array($clone), $af_part2); + + /* 13. Remove the formatting element from the stack + of open elements, and insert the clone into the stack + of open elements immediately after (i.e. in a more + deeply nested position than) the position of the + furthest block in that stack. */ + $fe_s_pos = array_search($formatting_element, $this->stack, true); + $fb_s_pos = array_search($furthest_block, $this->stack, true); + unset($this->stack[$fe_s_pos]); + + $s_part1 = array_slice($this->stack, 0, $fb_s_pos); + $s_part2 = array_slice($this->stack, $fb_s_pos + 1, count($this->stack)); + $this->stack = array_merge($s_part1, array($clone), $s_part2); + + /* 14. Jump back to step 1 in this series of steps. */ + unset($formatting_element, $fe_af_pos, $fe_s_pos, $furthest_block); + } + break; + + /* An end tag token whose tag name is one of: "button", + "marquee", "object" */ + case 'button': case 'marquee': case 'object': + /* If the stack of open elements has an element in scope whose + tag name matches the tag name of the token, then generate implied + tags. */ + if($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(); + + /* Now, if the current node is not an element with the same + tag name as the token, then this is a parse error. */ + // k + + /* Now, if the stack of open elements has an element in scope + whose tag name matches the tag name of the token, then pop + elements from the stack until that element has been popped from + the stack, and clear the list of active formatting elements up + to the last marker. */ + for($n = count($this->stack) - 1; $n >= 0; $n--) { + if($this->stack[$n]->nodeName === $token['name']) { + $n = -1; + } + + array_pop($this->stack); + } + + $marker = end(array_keys($this->a_formatting, self::MARKER, true)); + + for($n = count($this->a_formatting) - 1; $n > $marker; $n--) { + array_pop($this->a_formatting); + } + } + break; + + /* Or an end tag whose tag name is one of: "area", "basefont", + "bgsound", "br", "embed", "hr", "iframe", "image", "img", + "input", "isindex", "noembed", "noframes", "param", "select", + "spacer", "table", "textarea", "wbr" */ + case 'area': case 'basefont': case 'bgsound': case 'br': + case 'embed': case 'hr': case 'iframe': case 'image': + case 'img': case 'input': case 'isindex': case 'noembed': + case 'noframes': case 'param': case 'select': case 'spacer': + case 'table': case 'textarea': case 'wbr': + // Parse error. Ignore the token. + break; + + /* An end tag token not covered by the previous entries */ + default: + for($n = count($this->stack) - 1; $n >= 0; $n--) { + /* Initialise node to be the current node (the bottommost + node of the stack). */ + $node = end($this->stack); + + /* If node has the same tag name as the end tag token, + then: */ + if($token['name'] === $node->nodeName) { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* If the tag name of the end tag token does not + match the tag name of the current node, this is a + parse error. */ + // k + + /* Pop all the nodes from the current node up to + node, including node, then stop this algorithm. */ + for($x = count($this->stack) - $n; $x >= $n; $x--) { + array_pop($this->stack); + } + + } else { + $category = $this->getElementCategory($node); + + if($category !== self::SPECIAL && $category !== self::SCOPING) { + /* Otherwise, if node is in neither the formatting + category nor the phrasing category, then this is a + parse error. Stop this algorithm. The end tag token + is ignored. */ + return false; + } + } + } + break; + } + break; + } + } + + private function inTable($token) { + $clear = array('html', 'table'); + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { + /* Append the character to the current node. */ + $text = $this->dom->createTextNode($token['data']); + end($this->stack)->appendChild($text); + + /* A comment token */ + } elseif($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + end($this->stack)->appendChild($comment); + + /* A start tag whose tag name is "caption" */ + } elseif($token['type'] === HTML5::STARTTAG && + $token['name'] === 'caption') { + /* Clear the stack back to a table context. */ + $this->clearStackToTableContext($clear); + + /* Insert a marker at the end of the list of active + formatting elements. */ + $this->a_formatting[] = self::MARKER; + + /* Insert an HTML element for the token, then switch the + insertion mode to "in caption". */ + $this->insertElement($token); + $this->mode = self::IN_CAPTION; + + /* A start tag whose tag name is "colgroup" */ + } elseif($token['type'] === HTML5::STARTTAG && + $token['name'] === 'colgroup') { + /* Clear the stack back to a table context. */ + $this->clearStackToTableContext($clear); + + /* Insert an HTML element for the token, then switch the + insertion mode to "in column group". */ + $this->insertElement($token); + $this->mode = self::IN_CGROUP; + + /* A start tag whose tag name is "col" */ + } elseif($token['type'] === HTML5::STARTTAG && + $token['name'] === 'col') { + $this->inTable(array( + 'name' => 'colgroup', + 'type' => HTML5::STARTTAG, + 'attr' => array() + )); + + $this->inColumnGroup($token); + + /* A start tag whose tag name is one of: "tbody", "tfoot", "thead" */ + } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], + array('tbody', 'tfoot', 'thead'))) { + /* Clear the stack back to a table context. */ + $this->clearStackToTableContext($clear); + + /* Insert an HTML element for the token, then switch the insertion + mode to "in table body". */ + $this->insertElement($token); + $this->mode = self::IN_TBODY; + + /* A start tag whose tag name is one of: "td", "th", "tr" */ + } elseif($token['type'] === HTML5::STARTTAG && + in_array($token['name'], array('td', 'th', 'tr'))) { + /* Act as if a start tag token with the tag name "tbody" had been + seen, then reprocess the current token. */ + $this->inTable(array( + 'name' => 'tbody', + 'type' => HTML5::STARTTAG, + 'attr' => array() + )); + + return $this->inTableBody($token); + + /* A start tag whose tag name is "table" */ + } elseif($token['type'] === HTML5::STARTTAG && + $token['name'] === 'table') { + /* Parse error. Act as if an end tag token with the tag name "table" + had been seen, then, if that token wasn't ignored, reprocess the + current token. */ + $this->inTable(array( + 'name' => 'table', + 'type' => HTML5::ENDTAG + )); + + return $this->mainPhase($token); + + /* An end tag whose tag name is "table" */ + } elseif($token['type'] === HTML5::ENDTAG && + $token['name'] === 'table') { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if(!$this->elementInScope($token['name'], true)) { + return false; + + /* Otherwise: */ + } else { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* Now, if the current node is not a table element, then this + is a parse error. */ + // w/e + + /* Pop elements from this stack until a table element has been + popped from the stack. */ + while(true) { + $current = end($this->stack)->nodeName; + array_pop($this->stack); + + if($current === 'table') { + break; + } + } + + /* Reset the insertion mode appropriately. */ + $this->resetInsertionMode(); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], + array('body', 'caption', 'col', 'colgroup', 'html', 'tbody', 'td', + 'tfoot', 'th', 'thead', 'tr'))) { + // Parse error. Ignore the token. + + /* Anything else */ + } else { + /* Parse error. Process the token as if the insertion mode was "in + body", with the following exception: */ + + /* If the current node is a table, tbody, tfoot, thead, or tr + element, then, whenever a node would be inserted into the current + node, it must instead be inserted into the foster parent element. */ + if(in_array(end($this->stack)->nodeName, + array('table', 'tbody', 'tfoot', 'thead', 'tr'))) { + /* The foster parent element is the parent element of the last + table element in the stack of open elements, if there is a + table element and it has such a parent element. If there is no + table element in the stack of open elements (innerHTML case), + then the foster parent element is the first element in the + stack of open elements (the html element). Otherwise, if there + is a table element in the stack of open elements, but the last + table element in the stack of open elements has no parent, or + its parent node is not an element, then the foster parent + element is the element before the last table element in the + stack of open elements. */ + for($n = count($this->stack) - 1; $n >= 0; $n--) { + if($this->stack[$n]->nodeName === 'table') { + $table = $this->stack[$n]; + break; + } + } + + if(isset($table) && $table->parentNode !== null) { + $this->foster_parent = $table->parentNode; + + } elseif(!isset($table)) { + $this->foster_parent = $this->stack[0]; + + } elseif(isset($table) && ($table->parentNode === null || + $table->parentNode->nodeType !== XML_ELEMENT_NODE)) { + $this->foster_parent = $this->stack[$n - 1]; + } + } + + $this->inBody($token); + } + } + + private function inCaption($token) { + /* An end tag whose tag name is "caption" */ + if($token['type'] === HTML5::ENDTAG && $token['name'] === 'caption') { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if(!$this->elementInScope($token['name'], true)) { + // Ignore + + /* Otherwise: */ + } else { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* Now, if the current node is not a caption element, then this + is a parse error. */ + // w/e + + /* Pop elements from this stack until a caption element has + been popped from the stack. */ + while(true) { + $node = end($this->stack)->nodeName; + array_pop($this->stack); + + if($node === 'caption') { + break; + } + } + + /* Clear the list of active formatting elements up to the last + marker. */ + $this->clearTheActiveFormattingElementsUpToTheLastMarker(); + + /* Switch the insertion mode to "in table". */ + $this->mode = self::IN_TABLE; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "td", "tfoot", "th", "thead", "tr", or an end tag whose tag + name is "table" */ + } elseif(($token['type'] === HTML5::STARTTAG && in_array($token['name'], + array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', + 'thead', 'tr'))) || ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'table')) { + /* Parse error. Act as if an end tag with the tag name "caption" + had been seen, then, if that token wasn't ignored, reprocess the + current token. */ + $this->inCaption(array( + 'name' => 'caption', + 'type' => HTML5::ENDTAG + )); + + return $this->inTable($token); + + /* An end tag whose tag name is one of: "body", "col", "colgroup", + "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], + array('body', 'col', 'colgroup', 'html', 'tbody', 'tfoot', 'th', + 'thead', 'tr'))) { + // Parse error. Ignore the token. + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in body". */ + $this->inBody($token); + } + } + + private function inColumnGroup($token) { + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { + /* Append the character to the current node. */ + $text = $this->dom->createTextNode($token['data']); + end($this->stack)->appendChild($text); + + /* A comment token */ + } elseif($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + end($this->stack)->appendChild($comment); + + /* A start tag whose tag name is "col" */ + } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'col') { + /* Insert a col element for the token. Immediately pop the current + node off the stack of open elements. */ + $this->insertElement($token); + array_pop($this->stack); + + /* An end tag whose tag name is "colgroup" */ + } elseif($token['type'] === HTML5::ENDTAG && + $token['name'] === 'colgroup') { + /* If the current node is the root html element, then this is a + parse error, ignore the token. (innerHTML case) */ + if(end($this->stack)->nodeName === 'html') { + // Ignore + + /* Otherwise, pop the current node (which will be a colgroup + element) from the stack of open elements. Switch the insertion + mode to "in table". */ + } else { + array_pop($this->stack); + $this->mode = self::IN_TABLE; + } + + /* An end tag whose tag name is "col" */ + } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'col') { + /* Parse error. Ignore the token. */ + + /* Anything else */ + } else { + /* Act as if an end tag with the tag name "colgroup" had been seen, + and then, if that token wasn't ignored, reprocess the current token. */ + $this->inColumnGroup(array( + 'name' => 'colgroup', + 'type' => HTML5::ENDTAG + )); + + return $this->inTable($token); + } + } + + private function inTableBody($token) { + $clear = array('tbody', 'tfoot', 'thead', 'html'); + + /* A start tag whose tag name is "tr" */ + if($token['type'] === HTML5::STARTTAG && $token['name'] === 'tr') { + /* Clear the stack back to a table body context. */ + $this->clearStackToTableContext($clear); + + /* Insert a tr element for the token, then switch the insertion + mode to "in row". */ + $this->insertElement($token); + $this->mode = self::IN_ROW; + + /* A start tag whose tag name is one of: "th", "td" */ + } elseif($token['type'] === HTML5::STARTTAG && + ($token['name'] === 'th' || $token['name'] === 'td')) { + /* Parse error. Act as if a start tag with the tag name "tr" had + been seen, then reprocess the current token. */ + $this->inTableBody(array( + 'name' => 'tr', + 'type' => HTML5::STARTTAG, + 'attr' => array() + )); + + return $this->inRow($token); + + /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ + } elseif($token['type'] === HTML5::ENDTAG && + in_array($token['name'], array('tbody', 'tfoot', 'thead'))) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. */ + if(!$this->elementInScope($token['name'], true)) { + // Ignore + + /* Otherwise: */ + } else { + /* Clear the stack back to a table body context. */ + $this->clearStackToTableContext($clear); + + /* Pop the current node from the stack of open elements. Switch + the insertion mode to "in table". */ + array_pop($this->stack); + $this->mode = self::IN_TABLE; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "tfoot", "thead", or an end tag whose tag name is "table" */ + } elseif(($token['type'] === HTML5::STARTTAG && in_array($token['name'], + array('caption', 'col', 'colgroup', 'tbody', 'tfoor', 'thead'))) || + ($token['type'] === HTML5::STARTTAG && $token['name'] === 'table')) { + /* If the stack of open elements does not have a tbody, thead, or + tfoot element in table scope, this is a parse error. Ignore the + token. (innerHTML case) */ + if(!$this->elementInScope(array('tbody', 'thead', 'tfoot'), true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Clear the stack back to a table body context. */ + $this->clearStackToTableContext($clear); + + /* Act as if an end tag with the same tag name as the current + node ("tbody", "tfoot", or "thead") had been seen, then + reprocess the current token. */ + $this->inTableBody(array( + 'name' => end($this->stack)->nodeName, + 'type' => HTML5::ENDTAG + )); + + return $this->mainPhase($token); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html", "td", "th", "tr" */ + } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], + array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr'))) { + /* Parse error. Ignore the token. */ + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in table". */ + $this->inTable($token); + } + } + + private function inRow($token) { + $clear = array('tr', 'html'); + + /* A start tag whose tag name is one of: "th", "td" */ + if($token['type'] === HTML5::STARTTAG && + ($token['name'] === 'th' || $token['name'] === 'td')) { + /* Clear the stack back to a table row context. */ + $this->clearStackToTableContext($clear); + + /* Insert an HTML element for the token, then switch the insertion + mode to "in cell". */ + $this->insertElement($token); + $this->mode = self::IN_CELL; + + /* Insert a marker at the end of the list of active formatting + elements. */ + $this->a_formatting[] = self::MARKER; + + /* An end tag whose tag name is "tr" */ + } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'tr') { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if(!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Clear the stack back to a table row context. */ + $this->clearStackToTableContext($clear); + + /* Pop the current node (which will be a tr element) from the + stack of open elements. Switch the insertion mode to "in table + body". */ + array_pop($this->stack); + $this->mode = self::IN_TBODY; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "tfoot", "thead", "tr" or an end tag whose tag name is "table" */ + } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], + array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead', 'tr'))) { + /* Act as if an end tag with the tag name "tr" had been seen, then, + if that token wasn't ignored, reprocess the current token. */ + $this->inRow(array( + 'name' => 'tr', + 'type' => HTML5::ENDTAG + )); + + return $this->inCell($token); + + /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ + } elseif($token['type'] === HTML5::ENDTAG && + in_array($token['name'], array('tbody', 'tfoot', 'thead'))) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. */ + if(!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Otherwise, act as if an end tag with the tag name "tr" had + been seen, then reprocess the current token. */ + $this->inRow(array( + 'name' => 'tr', + 'type' => HTML5::ENDTAG + )); + + return $this->inCell($token); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html", "td", "th" */ + } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], + array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr'))) { + /* Parse error. Ignore the token. */ + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in table". */ + $this->inTable($token); + } + } + + private function inCell($token) { + /* An end tag whose tag name is one of: "td", "th" */ + if($token['type'] === HTML5::ENDTAG && + ($token['name'] === 'td' || $token['name'] === 'th')) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as that of the token, then this is a + parse error and the token must be ignored. */ + if(!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Generate implied end tags, except for elements with the same + tag name as the token. */ + $this->generateImpliedEndTags(array($token['name'])); + + /* Now, if the current node is not an element with the same tag + name as the token, then this is a parse error. */ + // k + + /* Pop elements from this stack until an element with the same + tag name as the token has been popped from the stack. */ + while(true) { + $node = end($this->stack)->nodeName; + array_pop($this->stack); + + if($node === $token['name']) { + break; + } + } + + /* Clear the list of active formatting elements up to the last + marker. */ + $this->clearTheActiveFormattingElementsUpToTheLastMarker(); + + /* Switch the insertion mode to "in row". (The current node + will be a tr element at this point.) */ + $this->mode = self::IN_ROW; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], + array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', + 'thead', 'tr'))) { + /* If the stack of open elements does not have a td or th element + in table scope, then this is a parse error; ignore the token. + (innerHTML case) */ + if(!$this->elementInScope(array('td', 'th'), true)) { + // Ignore. + + /* Otherwise, close the cell (see below) and reprocess the current + token. */ + } else { + $this->closeCell(); + return $this->inRow($token); + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], + array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', + 'thead', 'tr'))) { + /* If the stack of open elements does not have a td or th element + in table scope, then this is a parse error; ignore the token. + (innerHTML case) */ + if(!$this->elementInScope(array('td', 'th'), true)) { + // Ignore. + + /* Otherwise, close the cell (see below) and reprocess the current + token. */ + } else { + $this->closeCell(); + return $this->inRow($token); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html" */ + } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], + array('body', 'caption', 'col', 'colgroup', 'html'))) { + /* Parse error. Ignore the token. */ + + /* An end tag whose tag name is one of: "table", "tbody", "tfoot", + "thead", "tr" */ + } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], + array('table', 'tbody', 'tfoot', 'thead', 'tr'))) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as that of the token (which can only + happen for "tbody", "tfoot" and "thead", or, in the innerHTML case), + then this is a parse error and the token must be ignored. */ + if(!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise, close the cell (see below) and reprocess the current + token. */ + } else { + $this->closeCell(); + return $this->inRow($token); + } + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in body". */ + $this->inBody($token); + } + } + + private function inSelect($token) { + /* Handle the token as follows: */ + + /* A character token */ + if($token['type'] === HTML5::CHARACTR) { + /* Append the token's character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag token whose tag name is "option" */ + } elseif($token['type'] === HTML5::STARTTAG && + $token['name'] === 'option') { + /* If the current node is an option element, act as if an end tag + with the tag name "option" had been seen. */ + if(end($this->stack)->nodeName === 'option') { + $this->inSelect(array( + 'name' => 'option', + 'type' => HTML5::ENDTAG + )); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* A start tag token whose tag name is "optgroup" */ + } elseif($token['type'] === HTML5::STARTTAG && + $token['name'] === 'optgroup') { + /* If the current node is an option element, act as if an end tag + with the tag name "option" had been seen. */ + if(end($this->stack)->nodeName === 'option') { + $this->inSelect(array( + 'name' => 'option', + 'type' => HTML5::ENDTAG + )); + } + + /* If the current node is an optgroup element, act as if an end tag + with the tag name "optgroup" had been seen. */ + if(end($this->stack)->nodeName === 'optgroup') { + $this->inSelect(array( + 'name' => 'optgroup', + 'type' => HTML5::ENDTAG + )); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* An end tag token whose tag name is "optgroup" */ + } elseif($token['type'] === HTML5::ENDTAG && + $token['name'] === 'optgroup') { + /* First, if the current node is an option element, and the node + immediately before it in the stack of open elements is an optgroup + element, then act as if an end tag with the tag name "option" had + been seen. */ + $elements_in_stack = count($this->stack); + + if($this->stack[$elements_in_stack - 1]->nodeName === 'option' && + $this->stack[$elements_in_stack - 2]->nodeName === 'optgroup') { + $this->inSelect(array( + 'name' => 'option', + 'type' => HTML5::ENDTAG + )); + } + + /* If the current node is an optgroup element, then pop that node + from the stack of open elements. Otherwise, this is a parse error, + ignore the token. */ + if($this->stack[$elements_in_stack - 1] === 'optgroup') { + array_pop($this->stack); + } + + /* An end tag token whose tag name is "option" */ + } elseif($token['type'] === HTML5::ENDTAG && + $token['name'] === 'option') { + /* If the current node is an option element, then pop that node + from the stack of open elements. Otherwise, this is a parse error, + ignore the token. */ + if(end($this->stack)->nodeName === 'option') { + array_pop($this->stack); + } + + /* An end tag whose tag name is "select" */ + } elseif($token['type'] === HTML5::ENDTAG && + $token['name'] === 'select') { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if(!$this->elementInScope($token['name'], true)) { + // w/e + + /* Otherwise: */ + } else { + /* Pop elements from the stack of open elements until a select + element has been popped from the stack. */ + while(true) { + $current = end($this->stack)->nodeName; + array_pop($this->stack); + + if($current === 'select') { + break; + } + } + + /* Reset the insertion mode appropriately. */ + $this->resetInsertionMode(); + } + + /* A start tag whose tag name is "select" */ + } elseif($token['name'] === 'select' && + $token['type'] === HTML5::STARTTAG) { + /* Parse error. Act as if the token had been an end tag with the + tag name "select" instead. */ + $this->inSelect(array( + 'name' => 'select', + 'type' => HTML5::ENDTAG + )); + + /* An end tag whose tag name is one of: "caption", "table", "tbody", + "tfoot", "thead", "tr", "td", "th" */ + } elseif(in_array($token['name'], array('caption', 'table', 'tbody', + 'tfoot', 'thead', 'tr', 'td', 'th')) && $token['type'] === HTML5::ENDTAG) { + /* Parse error. */ + // w/e + + /* If the stack of open elements has an element in table scope with + the same tag name as that of the token, then act as if an end tag + with the tag name "select" had been seen, and reprocess the token. + Otherwise, ignore the token. */ + if($this->elementInScope($token['name'], true)) { + $this->inSelect(array( + 'name' => 'select', + 'type' => HTML5::ENDTAG + )); + + $this->mainPhase($token); + } + + /* Anything else */ + } else { + /* Parse error. Ignore the token. */ + } + } + + private function afterBody($token) { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { + /* Process the token as it would be processed if the insertion mode + was "in body". */ + $this->inBody($token); + + /* A comment token */ + } elseif($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the first element in the stack of open + elements (the html element), with the data attribute set to the + data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + $this->stack[0]->appendChild($comment); + + /* An end tag with the tag name "html" */ + } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') { + /* If the parser was originally created in order to handle the + setting of an element's innerHTML attribute, this is a parse error; + ignore the token. (The element will be an html element in this + case.) (innerHTML case) */ + + /* Otherwise, switch to the trailing end phase. */ + $this->phase = self::END_PHASE; + + /* Anything else */ + } else { + /* Parse error. Set the insertion mode to "in body" and reprocess + the token. */ + $this->mode = self::IN_BODY; + return $this->inBody($token); + } + } + + private function inFrameset($token) { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ + if($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag with the tag name "frameset" */ + } elseif($token['name'] === 'frameset' && + $token['type'] === HTML5::STARTTAG) { + $this->insertElement($token); + + /* An end tag with the tag name "frameset" */ + } elseif($token['name'] === 'frameset' && + $token['type'] === HTML5::ENDTAG) { + /* If the current node is the root html element, then this is a + parse error; ignore the token. (innerHTML case) */ + if(end($this->stack)->nodeName === 'html') { + // Ignore + + } else { + /* Otherwise, pop the current node from the stack of open + elements. */ + array_pop($this->stack); + + /* If the parser was not originally created in order to handle + the setting of an element's innerHTML attribute (innerHTML case), + and the current node is no longer a frameset element, then change + the insertion mode to "after frameset". */ + $this->mode = self::AFTR_FRAME; + } + + /* A start tag with the tag name "frame" */ + } elseif($token['name'] === 'frame' && + $token['type'] === HTML5::STARTTAG) { + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Immediately pop the current node off the stack of open elements. */ + array_pop($this->stack); + + /* A start tag with the tag name "noframes" */ + } elseif($token['name'] === 'noframes' && + $token['type'] === HTML5::STARTTAG) { + /* Process the token as if the insertion mode had been "in body". */ + $this->inBody($token); + + /* Anything else */ + } else { + /* Parse error. Ignore the token. */ + } + } + + private function afterFrameset($token) { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ + if($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* An end tag with the tag name "html" */ + } elseif($token['name'] === 'html' && + $token['type'] === HTML5::ENDTAG) { + /* Switch to the trailing end phase. */ + $this->phase = self::END_PHASE; + + /* A start tag with the tag name "noframes" */ + } elseif($token['name'] === 'noframes' && + $token['type'] === HTML5::STARTTAG) { + /* Process the token as if the insertion mode had been "in body". */ + $this->inBody($token); + + /* Anything else */ + } else { + /* Parse error. Ignore the token. */ + } + } + + private function trailingEndPhase($token) { + /* After the main phase, as each token is emitted from the tokenisation + stage, it must be processed as described in this section. */ + + /* A DOCTYPE token */ + if($token['type'] === HTML5::DOCTYPE) { + // Parse error. Ignore the token. + + /* A comment token */ + } elseif($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the Document object with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + $this->dom->appendChild($comment); + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + } elseif($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { + /* Process the token as it would be processed in the main phase. */ + $this->mainPhase($token); + + /* A character token that is not one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE. Or a start tag token. Or an end tag token. */ + } elseif(($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || + $token['type'] === HTML5::STARTTAG || $token['type'] === HTML5::ENDTAG) { + /* Parse error. Switch back to the main phase and reprocess the + token. */ + $this->phase = self::MAIN_PHASE; + return $this->mainPhase($token); + + /* An end-of-file token */ + } elseif($token['type'] === HTML5::EOF) { + /* OMG DONE!! */ + } + } + + private function insertElement($token, $append = true, $check = false) { + // Proprietary workaround for libxml2's limitations with tag names + if ($check) { + // Slightly modified HTML5 tag-name modification, + // removing anything that's not an ASCII letter, digit, or hyphen + $token['name'] = preg_replace('/[^a-z0-9-]/i', '', $token['name']); + // Remove leading hyphens and numbers + $token['name'] = ltrim($token['name'], '-0..9'); + // In theory, this should ever be needed, but just in case + if ($token['name'] === '') $token['name'] = 'span'; // arbitrary generic choice + } + + $el = $this->dom->createElement($token['name']); + + foreach($token['attr'] as $attr) { + if(!$el->hasAttribute($attr['name'])) { + $el->setAttribute($attr['name'], $attr['value']); + } + } + + $this->appendToRealParent($el); + $this->stack[] = $el; + + return $el; + } + + private function insertText($data) { + $text = $this->dom->createTextNode($data); + $this->appendToRealParent($text); + } + + private function insertComment($data) { + $comment = $this->dom->createComment($data); + $this->appendToRealParent($comment); + } + + private function appendToRealParent($node) { + if($this->foster_parent === null) { + end($this->stack)->appendChild($node); + + } elseif($this->foster_parent !== null) { + /* If the foster parent element is the parent element of the + last table element in the stack of open elements, then the new + node must be inserted immediately before the last table element + in the stack of open elements in the foster parent element; + otherwise, the new node must be appended to the foster parent + element. */ + for($n = count($this->stack) - 1; $n >= 0; $n--) { + if($this->stack[$n]->nodeName === 'table' && + $this->stack[$n]->parentNode !== null) { + $table = $this->stack[$n]; + break; + } + } + + if(isset($table) && $this->foster_parent->isSameNode($table->parentNode)) + $this->foster_parent->insertBefore($node, $table); + else + $this->foster_parent->appendChild($node); + + $this->foster_parent = null; + } + } + + private function elementInScope($el, $table = false) { + if(is_array($el)) { + foreach($el as $element) { + if($this->elementInScope($element, $table)) { + return true; + } + } + + return false; + } + + $leng = count($this->stack); + + for($n = 0; $n < $leng; $n++) { + /* 1. Initialise node to be the current node (the bottommost node of + the stack). */ + $node = $this->stack[$leng - 1 - $n]; + + if($node->tagName === $el) { + /* 2. If node is the target node, terminate in a match state. */ + return true; + + } elseif($node->tagName === 'table') { + /* 3. Otherwise, if node is a table element, terminate in a failure + state. */ + return false; + + } elseif($table === true && in_array($node->tagName, array('caption', 'td', + 'th', 'button', 'marquee', 'object'))) { + /* 4. Otherwise, if the algorithm is the "has an element in scope" + variant (rather than the "has an element in table scope" variant), + and node is one of the following, terminate in a failure state. */ + return false; + + } elseif($node === $node->ownerDocument->documentElement) { + /* 5. Otherwise, if node is an html element (root element), terminate + in a failure state. (This can only happen if the node is the topmost + node of the stack of open elements, and prevents the next step from + being invoked if there are no more elements in the stack.) */ + return false; + } + + /* Otherwise, set node to the previous entry in the stack of open + elements and return to step 2. (This will never fail, since the loop + will always terminate in the previous step if the top of the stack + is reached.) */ + } + } + + private function reconstructActiveFormattingElements() { + /* 1. If there are no entries in the list of active formatting elements, + then there is nothing to reconstruct; stop this algorithm. */ + $formatting_elements = count($this->a_formatting); + + if($formatting_elements === 0) { + return false; + } + + /* 3. Let entry be the last (most recently added) element in the list + of active formatting elements. */ + $entry = end($this->a_formatting); + + /* 2. If the last (most recently added) entry in the list of active + formatting elements is a marker, or if it is an element that is in the + stack of open elements, then there is nothing to reconstruct; stop this + algorithm. */ + if($entry === self::MARKER || in_array($entry, $this->stack, true)) { + return false; + } + + for($a = $formatting_elements - 1; $a >= 0; true) { + /* 4. If there are no entries before entry in the list of active + formatting elements, then jump to step 8. */ + if($a === 0) { + $step_seven = false; + break; + } + + /* 5. Let entry be the entry one earlier than entry in the list of + active formatting elements. */ + $a--; + $entry = $this->a_formatting[$a]; + + /* 6. If entry is neither a marker nor an element that is also in + thetack of open elements, go to step 4. */ + if($entry === self::MARKER || in_array($entry, $this->stack, true)) { + break; + } + } + + while(true) { + /* 7. Let entry be the element one later than entry in the list of + active formatting elements. */ + if(isset($step_seven) && $step_seven === true) { + $a++; + $entry = $this->a_formatting[$a]; + } + + /* 8. Perform a shallow clone of the element entry to obtain clone. */ + $clone = $entry->cloneNode(); + + /* 9. Append clone to the current node and push it onto the stack + of open elements so that it is the new current node. */ + end($this->stack)->appendChild($clone); + $this->stack[] = $clone; + + /* 10. Replace the entry for entry in the list with an entry for + clone. */ + $this->a_formatting[$a] = $clone; + + /* 11. If the entry for clone in the list of active formatting + elements is not the last entry in the list, return to step 7. */ + if(end($this->a_formatting) !== $clone) { + $step_seven = true; + } else { + break; + } + } + } + + private function clearTheActiveFormattingElementsUpToTheLastMarker() { + /* When the steps below require the UA to clear the list of active + formatting elements up to the last marker, the UA must perform the + following steps: */ + + while(true) { + /* 1. Let entry be the last (most recently added) entry in the list + of active formatting elements. */ + $entry = end($this->a_formatting); + + /* 2. Remove entry from the list of active formatting elements. */ + array_pop($this->a_formatting); + + /* 3. If entry was a marker, then stop the algorithm at this point. + The list has been cleared up to the last marker. */ + if($entry === self::MARKER) { + break; + } + } + } + + private function generateImpliedEndTags($exclude = array()) { + /* When the steps below require the UA to generate implied end tags, + then, if the current node is a dd element, a dt element, an li element, + a p element, a td element, a th element, or a tr element, the UA must + act as if an end tag with the respective tag name had been seen and + then generate implied end tags again. */ + $node = end($this->stack); + $elements = array_diff(array('dd', 'dt', 'li', 'p', 'td', 'th', 'tr'), $exclude); + + while(in_array(end($this->stack)->nodeName, $elements)) { + array_pop($this->stack); + } + } + + private function getElementCategory($node) { + $name = $node->tagName; + if(in_array($name, $this->special)) + return self::SPECIAL; + + elseif(in_array($name, $this->scoping)) + return self::SCOPING; + + elseif(in_array($name, $this->formatting)) + return self::FORMATTING; + + else + return self::PHRASING; + } + + private function clearStackToTableContext($elements) { + /* When the steps above require the UA to clear the stack back to a + table context, it means that the UA must, while the current node is not + a table element or an html element, pop elements from the stack of open + elements. If this causes any elements to be popped from the stack, then + this is a parse error. */ + while(true) { + $node = end($this->stack)->nodeName; + + if(in_array($node, $elements)) { + break; + } else { + array_pop($this->stack); + } + } + } + + private function resetInsertionMode() { + /* 1. Let last be false. */ + $last = false; + $leng = count($this->stack); + + for($n = $leng - 1; $n >= 0; $n--) { + /* 2. Let node be the last node in the stack of open elements. */ + $node = $this->stack[$n]; + + /* 3. If node is the first node in the stack of open elements, then + set last to true. If the element whose innerHTML attribute is being + set is neither a td element nor a th element, then set node to the + element whose innerHTML attribute is being set. (innerHTML case) */ + if($this->stack[0]->isSameNode($node)) { + $last = true; + } + + /* 4. If node is a select element, then switch the insertion mode to + "in select" and abort these steps. (innerHTML case) */ + if($node->nodeName === 'select') { + $this->mode = self::IN_SELECT; + break; + + /* 5. If node is a td or th element, then switch the insertion mode + to "in cell" and abort these steps. */ + } elseif($node->nodeName === 'td' || $node->nodeName === 'th') { + $this->mode = self::IN_CELL; + break; + + /* 6. If node is a tr element, then switch the insertion mode to + "in row" and abort these steps. */ + } elseif($node->nodeName === 'tr') { + $this->mode = self::IN_ROW; + break; + + /* 7. If node is a tbody, thead, or tfoot element, then switch the + insertion mode to "in table body" and abort these steps. */ + } elseif(in_array($node->nodeName, array('tbody', 'thead', 'tfoot'))) { + $this->mode = self::IN_TBODY; + break; + + /* 8. If node is a caption element, then switch the insertion mode + to "in caption" and abort these steps. */ + } elseif($node->nodeName === 'caption') { + $this->mode = self::IN_CAPTION; + break; + + /* 9. If node is a colgroup element, then switch the insertion mode + to "in column group" and abort these steps. (innerHTML case) */ + } elseif($node->nodeName === 'colgroup') { + $this->mode = self::IN_CGROUP; + break; + + /* 10. If node is a table element, then switch the insertion mode + to "in table" and abort these steps. */ + } elseif($node->nodeName === 'table') { + $this->mode = self::IN_TABLE; + break; + + /* 11. If node is a head element, then switch the insertion mode + to "in body" ("in body"! not "in head"!) and abort these steps. + (innerHTML case) */ + } elseif($node->nodeName === 'head') { + $this->mode = self::IN_BODY; + break; + + /* 12. If node is a body element, then switch the insertion mode to + "in body" and abort these steps. */ + } elseif($node->nodeName === 'body') { + $this->mode = self::IN_BODY; + break; + + /* 13. If node is a frameset element, then switch the insertion + mode to "in frameset" and abort these steps. (innerHTML case) */ + } elseif($node->nodeName === 'frameset') { + $this->mode = self::IN_FRAME; + break; + + /* 14. If node is an html element, then: if the head element + pointer is null, switch the insertion mode to "before head", + otherwise, switch the insertion mode to "after head". In either + case, abort these steps. (innerHTML case) */ + } elseif($node->nodeName === 'html') { + $this->mode = ($this->head_pointer === null) + ? self::BEFOR_HEAD + : self::AFTER_HEAD; + + break; + + /* 15. If last is true, then set the insertion mode to "in body" + and abort these steps. (innerHTML case) */ + } elseif($last) { + $this->mode = self::IN_BODY; + break; + } + } + } + + private function closeCell() { + /* If the stack of open elements has a td or th element in table scope, + then act as if an end tag token with that tag name had been seen. */ + foreach(array('td', 'th') as $cell) { + if($this->elementInScope($cell, true)) { + $this->inCell(array( + 'name' => $cell, + 'type' => HTML5::ENDTAG + )); + + break; + } + } + } + + public function save() { + return $this->dom; + } +} +?> diff --git a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Printer.php b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Printer.php index 16acd415740..549e4cea1ec 100644 --- a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Printer.php +++ b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Printer.php @@ -1,218 +1,218 @@ -getAll(); - $context = new HTMLPurifier_Context(); - $this->generator = new HTMLPurifier_Generator($config, $context); - } - - /** - * Main function that renders object or aspect of that object - * @note Parameters vary depending on printer - */ - // function render() {} - - /** - * Returns a start tag - * @param string $tag Tag name - * @param array $attr Attribute array - * @return string - */ - protected function start($tag, $attr = array()) - { - return $this->generator->generateFromToken( - new HTMLPurifier_Token_Start($tag, $attr ? $attr : array()) - ); - } - - /** - * Returns an end tag - * @param string $tag Tag name - * @return string - */ - protected function end($tag) - { - return $this->generator->generateFromToken( - new HTMLPurifier_Token_End($tag) - ); - } - - /** - * Prints a complete element with content inside - * @param string $tag Tag name - * @param string $contents Element contents - * @param array $attr Tag attributes - * @param bool $escape whether or not to escape contents - * @return string - */ - protected function element($tag, $contents, $attr = array(), $escape = true) - { - return $this->start($tag, $attr) . - ($escape ? $this->escape($contents) : $contents) . - $this->end($tag); - } - - /** - * @param string $tag - * @param array $attr - * @return string - */ - protected function elementEmpty($tag, $attr = array()) - { - return $this->generator->generateFromToken( - new HTMLPurifier_Token_Empty($tag, $attr) - ); - } - - /** - * @param string $text - * @return string - */ - protected function text($text) - { - return $this->generator->generateFromToken( - new HTMLPurifier_Token_Text($text) - ); - } - - /** - * Prints a simple key/value row in a table. - * @param string $name Key - * @param mixed $value Value - * @return string - */ - protected function row($name, $value) - { - if (is_bool($value)) { - $value = $value ? 'On' : 'Off'; - } - return - $this->start('tr') . "\n" . - $this->element('th', $name) . "\n" . - $this->element('td', $value) . "\n" . - $this->end('tr'); - } - - /** - * Escapes a string for HTML output. - * @param string $string String to escape - * @return string - */ - protected function escape($string) - { - $string = HTMLPurifier_Encoder::cleanUTF8($string); - $string = htmlspecialchars($string, ENT_COMPAT, 'UTF-8'); - return $string; - } - - /** - * Takes a list of strings and turns them into a single list - * @param string[] $array List of strings - * @param bool $polite Bool whether or not to add an end before the last - * @return string - */ - protected function listify($array, $polite = false) - { - if (empty($array)) { - return 'None'; - } - $ret = ''; - $i = count($array); - foreach ($array as $value) { - $i--; - $ret .= $value; - if ($i > 0 && !($polite && $i == 1)) { - $ret .= ', '; - } - if ($polite && $i == 1) { - $ret .= 'and '; - } - } - return $ret; - } - - /** - * Retrieves the class of an object without prefixes, as well as metadata - * @param object $obj Object to determine class of - * @param string $sec_prefix Further prefix to remove - * @return string - */ - protected function getClass($obj, $sec_prefix = '') - { - static $five = null; - if ($five === null) { - $five = version_compare(PHP_VERSION, '5', '>='); - } - $prefix = 'HTMLPurifier_' . $sec_prefix; - if (!$five) { - $prefix = strtolower($prefix); - } - $class = str_replace($prefix, '', get_class($obj)); - $lclass = strtolower($class); - $class .= '('; - switch ($lclass) { - case 'enum': - $values = array(); - foreach ($obj->valid_values as $value => $bool) { - $values[] = $value; - } - $class .= implode(', ', $values); - break; - case 'css_composite': - $values = array(); - foreach ($obj->defs as $def) { - $values[] = $this->getClass($def, $sec_prefix); - } - $class .= implode(', ', $values); - break; - case 'css_multiple': - $class .= $this->getClass($obj->single, $sec_prefix) . ', '; - $class .= $obj->max; - break; - case 'css_denyelementdecorator': - $class .= $this->getClass($obj->def, $sec_prefix) . ', '; - $class .= $obj->element; - break; - case 'css_importantdecorator': - $class .= $this->getClass($obj->def, $sec_prefix); - if ($obj->allow) { - $class .= ', !important'; - } - break; - } - $class .= ')'; - return $class; - } -} - -// vim: et sw=4 sts=4 +getAll(); + $context = new HTMLPurifier_Context(); + $this->generator = new HTMLPurifier_Generator($config, $context); + } + + /** + * Main function that renders object or aspect of that object + * @note Parameters vary depending on printer + */ + // function render() {} + + /** + * Returns a start tag + * @param string $tag Tag name + * @param array $attr Attribute array + * @return string + */ + protected function start($tag, $attr = array()) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Start($tag, $attr ? $attr : array()) + ); + } + + /** + * Returns an end tag + * @param string $tag Tag name + * @return string + */ + protected function end($tag) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_End($tag) + ); + } + + /** + * Prints a complete element with content inside + * @param string $tag Tag name + * @param string $contents Element contents + * @param array $attr Tag attributes + * @param bool $escape whether or not to escape contents + * @return string + */ + protected function element($tag, $contents, $attr = array(), $escape = true) + { + return $this->start($tag, $attr) . + ($escape ? $this->escape($contents) : $contents) . + $this->end($tag); + } + + /** + * @param string $tag + * @param array $attr + * @return string + */ + protected function elementEmpty($tag, $attr = array()) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Empty($tag, $attr) + ); + } + + /** + * @param string $text + * @return string + */ + protected function text($text) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Text($text) + ); + } + + /** + * Prints a simple key/value row in a table. + * @param string $name Key + * @param mixed $value Value + * @return string + */ + protected function row($name, $value) + { + if (is_bool($value)) { + $value = $value ? 'On' : 'Off'; + } + return + $this->start('tr') . "\n" . + $this->element('th', $name) . "\n" . + $this->element('td', $value) . "\n" . + $this->end('tr'); + } + + /** + * Escapes a string for HTML output. + * @param string $string String to escape + * @return string + */ + protected function escape($string) + { + $string = HTMLPurifier_Encoder::cleanUTF8($string); + $string = htmlspecialchars($string, ENT_COMPAT, 'UTF-8'); + return $string; + } + + /** + * Takes a list of strings and turns them into a single list + * @param string[] $array List of strings + * @param bool $polite Bool whether or not to add an end before the last + * @return string + */ + protected function listify($array, $polite = false) + { + if (empty($array)) { + return 'None'; + } + $ret = ''; + $i = count($array); + foreach ($array as $value) { + $i--; + $ret .= $value; + if ($i > 0 && !($polite && $i == 1)) { + $ret .= ', '; + } + if ($polite && $i == 1) { + $ret .= 'and '; + } + } + return $ret; + } + + /** + * Retrieves the class of an object without prefixes, as well as metadata + * @param object $obj Object to determine class of + * @param string $sec_prefix Further prefix to remove + * @return string + */ + protected function getClass($obj, $sec_prefix = '') + { + static $five = null; + if ($five === null) { + $five = version_compare(PHP_VERSION, '5', '>='); + } + $prefix = 'HTMLPurifier_' . $sec_prefix; + if (!$five) { + $prefix = strtolower($prefix); + } + $class = str_replace($prefix, '', get_class($obj)); + $lclass = strtolower($class); + $class .= '('; + switch ($lclass) { + case 'enum': + $values = array(); + foreach ($obj->valid_values as $value => $bool) { + $values[] = $value; + } + $class .= implode(', ', $values); + break; + case 'css_composite': + $values = array(); + foreach ($obj->defs as $def) { + $values[] = $this->getClass($def, $sec_prefix); + } + $class .= implode(', ', $values); + break; + case 'css_multiple': + $class .= $this->getClass($obj->single, $sec_prefix) . ', '; + $class .= $obj->max; + break; + case 'css_denyelementdecorator': + $class .= $this->getClass($obj->def, $sec_prefix) . ', '; + $class .= $obj->element; + break; + case 'css_importantdecorator': + $class .= $this->getClass($obj->def, $sec_prefix); + if ($obj->allow) { + $class .= ', !important'; + } + break; + } + $class .= ')'; + return $class; + } +} + +// vim: et sw=4 sts=4 diff --git a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Printer/CSSDefinition.php b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Printer/CSSDefinition.php index afc8c18ab96..29505fe12d4 100644 --- a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Printer/CSSDefinition.php +++ b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Printer/CSSDefinition.php @@ -1,44 +1,44 @@ -def = $config->getCSSDefinition(); - $ret = ''; - - $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer')); - $ret .= $this->start('table'); - - $ret .= $this->element('caption', 'Properties ($info)'); - - $ret .= $this->start('thead'); - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Property', array('class' => 'heavy')); - $ret .= $this->element('th', 'Definition', array('class' => 'heavy', 'style' => 'width:auto;')); - $ret .= $this->end('tr'); - $ret .= $this->end('thead'); - - ksort($this->def->info); - foreach ($this->def->info as $property => $obj) { - $name = $this->getClass($obj, 'AttrDef_'); - $ret .= $this->row($property, $name); - } - - $ret .= $this->end('table'); - $ret .= $this->end('div'); - - return $ret; - } -} - -// vim: et sw=4 sts=4 +def = $config->getCSSDefinition(); + $ret = ''; + + $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer')); + $ret .= $this->start('table'); + + $ret .= $this->element('caption', 'Properties ($info)'); + + $ret .= $this->start('thead'); + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Property', array('class' => 'heavy')); + $ret .= $this->element('th', 'Definition', array('class' => 'heavy', 'style' => 'width:auto;')); + $ret .= $this->end('tr'); + $ret .= $this->end('thead'); + + ksort($this->def->info); + foreach ($this->def->info as $property => $obj) { + $name = $this->getClass($obj, 'AttrDef_'); + $ret .= $this->row($property, $name); + } + + $ret .= $this->end('table'); + $ret .= $this->end('div'); + + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Printer/ConfigForm.php b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Printer/ConfigForm.php index 660960f3795..36100ce7384 100644 --- a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Printer/ConfigForm.php +++ b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Printer/ConfigForm.php @@ -1,447 +1,447 @@ -docURL = $doc_url; - $this->name = $name; - $this->compress = $compress; - // initialize sub-printers - $this->fields[0] = new HTMLPurifier_Printer_ConfigForm_default(); - $this->fields[HTMLPurifier_VarParser::BOOL] = new HTMLPurifier_Printer_ConfigForm_bool(); - } - - /** - * Sets default column and row size for textareas in sub-printers - * @param $cols Integer columns of textarea, null to use default - * @param $rows Integer rows of textarea, null to use default - */ - public function setTextareaDimensions($cols = null, $rows = null) - { - if ($cols) { - $this->fields['default']->cols = $cols; - } - if ($rows) { - $this->fields['default']->rows = $rows; - } - } - - /** - * Retrieves styling, in case it is not accessible by webserver - */ - public static function getCSS() - { - return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.css'); - } - - /** - * Retrieves JavaScript, in case it is not accessible by webserver - */ - public static function getJavaScript() - { - return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.js'); - } - - /** - * Returns HTML output for a configuration form - * @param HTMLPurifier_Config|array $config Configuration object of current form state, or an array - * where [0] has an HTML namespace and [1] is being rendered. - * @param array|bool $allowed Optional namespace(s) and directives to restrict form to. - * @param bool $render_controls - * @return string - */ - public function render($config, $allowed = true, $render_controls = true) - { - if (is_array($config) && isset($config[0])) { - $gen_config = $config[0]; - $config = $config[1]; - } else { - $gen_config = $config; - } - - $this->config = $config; - $this->genConfig = $gen_config; - $this->prepareGenerator($gen_config); - - $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $config->def); - $all = array(); - foreach ($allowed as $key) { - list($ns, $directive) = $key; - $all[$ns][$directive] = $config->get($ns . '.' . $directive); - } - - $ret = ''; - $ret .= $this->start('table', array('class' => 'hp-config')); - $ret .= $this->start('thead'); - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Directive', array('class' => 'hp-directive')); - $ret .= $this->element('th', 'Value', array('class' => 'hp-value')); - $ret .= $this->end('tr'); - $ret .= $this->end('thead'); - foreach ($all as $ns => $directives) { - $ret .= $this->renderNamespace($ns, $directives); - } - if ($render_controls) { - $ret .= $this->start('tbody'); - $ret .= $this->start('tr'); - $ret .= $this->start('td', array('colspan' => 2, 'class' => 'controls')); - $ret .= $this->elementEmpty('input', array('type' => 'submit', 'value' => 'Submit')); - $ret .= '[Reset]'; - $ret .= $this->end('td'); - $ret .= $this->end('tr'); - $ret .= $this->end('tbody'); - } - $ret .= $this->end('table'); - return $ret; - } - - /** - * Renders a single namespace - * @param $ns String namespace name - * @param array $directives array of directives to values - * @return string - */ - protected function renderNamespace($ns, $directives) - { - $ret = ''; - $ret .= $this->start('tbody', array('class' => 'namespace')); - $ret .= $this->start('tr'); - $ret .= $this->element('th', $ns, array('colspan' => 2)); - $ret .= $this->end('tr'); - $ret .= $this->end('tbody'); - $ret .= $this->start('tbody'); - foreach ($directives as $directive => $value) { - $ret .= $this->start('tr'); - $ret .= $this->start('th'); - if ($this->docURL) { - $url = str_replace('%s', urlencode("$ns.$directive"), $this->docURL); - $ret .= $this->start('a', array('href' => $url)); - } - $attr = array('for' => "{$this->name}:$ns.$directive"); - - // crop directive name if it's too long - if (!$this->compress || (strlen($directive) < $this->compress)) { - $directive_disp = $directive; - } else { - $directive_disp = substr($directive, 0, $this->compress - 2) . '...'; - $attr['title'] = $directive; - } - - $ret .= $this->element( - 'label', - $directive_disp, - // component printers must create an element with this id - $attr - ); - if ($this->docURL) { - $ret .= $this->end('a'); - } - $ret .= $this->end('th'); - - $ret .= $this->start('td'); - $def = $this->config->def->info["$ns.$directive"]; - if (is_int($def)) { - $allow_null = $def < 0; - $type = abs($def); - } else { - $type = $def->type; - $allow_null = isset($def->allow_null); - } - if (!isset($this->fields[$type])) { - $type = 0; - } // default - $type_obj = $this->fields[$type]; - if ($allow_null) { - $type_obj = new HTMLPurifier_Printer_ConfigForm_NullDecorator($type_obj); - } - $ret .= $type_obj->render($ns, $directive, $value, $this->name, array($this->genConfig, $this->config)); - $ret .= $this->end('td'); - $ret .= $this->end('tr'); - } - $ret .= $this->end('tbody'); - return $ret; - } - -} - -/** - * Printer decorator for directives that accept null - */ -class HTMLPurifier_Printer_ConfigForm_NullDecorator extends HTMLPurifier_Printer -{ - /** - * Printer being decorated - * @type HTMLPurifier_Printer - */ - protected $obj; - - /** - * @param HTMLPurifier_Printer $obj Printer to decorate - */ - public function __construct($obj) - { - parent::__construct(); - $this->obj = $obj; - } - - /** - * @param string $ns - * @param string $directive - * @param string $value - * @param string $name - * @param HTMLPurifier_Config|array $config - * @return string - */ - public function render($ns, $directive, $value, $name, $config) - { - if (is_array($config) && isset($config[0])) { - $gen_config = $config[0]; - $config = $config[1]; - } else { - $gen_config = $config; - } - $this->prepareGenerator($gen_config); - - $ret = ''; - $ret .= $this->start('label', array('for' => "$name:Null_$ns.$directive")); - $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); - $ret .= $this->text(' Null/Disabled'); - $ret .= $this->end('label'); - $attr = array( - 'type' => 'checkbox', - 'value' => '1', - 'class' => 'null-toggle', - 'name' => "$name" . "[Null_$ns.$directive]", - 'id' => "$name:Null_$ns.$directive", - 'onclick' => "toggleWriteability('$name:$ns.$directive',checked)" // INLINE JAVASCRIPT!!!! - ); - if ($this->obj instanceof HTMLPurifier_Printer_ConfigForm_bool) { - // modify inline javascript slightly - $attr['onclick'] = - "toggleWriteability('$name:Yes_$ns.$directive',checked);" . - "toggleWriteability('$name:No_$ns.$directive',checked)"; - } - if ($value === null) { - $attr['checked'] = 'checked'; - } - $ret .= $this->elementEmpty('input', $attr); - $ret .= $this->text(' or '); - $ret .= $this->elementEmpty('br'); - $ret .= $this->obj->render($ns, $directive, $value, $name, array($gen_config, $config)); - return $ret; - } -} - -/** - * Swiss-army knife configuration form field printer - */ -class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer -{ - /** - * @type int - */ - public $cols = 18; - - /** - * @type int - */ - public $rows = 5; - - /** - * @param string $ns - * @param string $directive - * @param string $value - * @param string $name - * @param HTMLPurifier_Config|array $config - * @return string - */ - public function render($ns, $directive, $value, $name, $config) - { - if (is_array($config) && isset($config[0])) { - $gen_config = $config[0]; - $config = $config[1]; - } else { - $gen_config = $config; - } - $this->prepareGenerator($gen_config); - // this should probably be split up a little - $ret = ''; - $def = $config->def->info["$ns.$directive"]; - if (is_int($def)) { - $type = abs($def); - } else { - $type = $def->type; - } - if (is_array($value)) { - switch ($type) { - case HTMLPurifier_VarParser::LOOKUP: - $array = $value; - $value = array(); - foreach ($array as $val => $b) { - $value[] = $val; - } - //TODO does this need a break? - case HTMLPurifier_VarParser::ALIST: - $value = implode(PHP_EOL, $value); - break; - case HTMLPurifier_VarParser::HASH: - $nvalue = ''; - foreach ($value as $i => $v) { - $nvalue .= "$i:$v" . PHP_EOL; - } - $value = $nvalue; - break; - default: - $value = ''; - } - } - if ($type === HTMLPurifier_VarParser::MIXED) { - return 'Not supported'; - $value = serialize($value); - } - $attr = array( - 'name' => "$name" . "[$ns.$directive]", - 'id' => "$name:$ns.$directive" - ); - if ($value === null) { - $attr['disabled'] = 'disabled'; - } - if (isset($def->allowed)) { - $ret .= $this->start('select', $attr); - foreach ($def->allowed as $val => $b) { - $attr = array(); - if ($value == $val) { - $attr['selected'] = 'selected'; - } - $ret .= $this->element('option', $val, $attr); - } - $ret .= $this->end('select'); - } elseif ($type === HTMLPurifier_VarParser::TEXT || - $type === HTMLPurifier_VarParser::ITEXT || - $type === HTMLPurifier_VarParser::ALIST || - $type === HTMLPurifier_VarParser::HASH || - $type === HTMLPurifier_VarParser::LOOKUP) { - $attr['cols'] = $this->cols; - $attr['rows'] = $this->rows; - $ret .= $this->start('textarea', $attr); - $ret .= $this->text($value); - $ret .= $this->end('textarea'); - } else { - $attr['value'] = $value; - $attr['type'] = 'text'; - $ret .= $this->elementEmpty('input', $attr); - } - return $ret; - } -} - -/** - * Bool form field printer - */ -class HTMLPurifier_Printer_ConfigForm_bool extends HTMLPurifier_Printer -{ - /** - * @param string $ns - * @param string $directive - * @param string $value - * @param string $name - * @param HTMLPurifier_Config|array $config - * @return string - */ - public function render($ns, $directive, $value, $name, $config) - { - if (is_array($config) && isset($config[0])) { - $gen_config = $config[0]; - $config = $config[1]; - } else { - $gen_config = $config; - } - $this->prepareGenerator($gen_config); - $ret = ''; - $ret .= $this->start('div', array('id' => "$name:$ns.$directive")); - - $ret .= $this->start('label', array('for' => "$name:Yes_$ns.$directive")); - $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); - $ret .= $this->text(' Yes'); - $ret .= $this->end('label'); - - $attr = array( - 'type' => 'radio', - 'name' => "$name" . "[$ns.$directive]", - 'id' => "$name:Yes_$ns.$directive", - 'value' => '1' - ); - if ($value === true) { - $attr['checked'] = 'checked'; - } - if ($value === null) { - $attr['disabled'] = 'disabled'; - } - $ret .= $this->elementEmpty('input', $attr); - - $ret .= $this->start('label', array('for' => "$name:No_$ns.$directive")); - $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); - $ret .= $this->text(' No'); - $ret .= $this->end('label'); - - $attr = array( - 'type' => 'radio', - 'name' => "$name" . "[$ns.$directive]", - 'id' => "$name:No_$ns.$directive", - 'value' => '0' - ); - if ($value === false) { - $attr['checked'] = 'checked'; - } - if ($value === null) { - $attr['disabled'] = 'disabled'; - } - $ret .= $this->elementEmpty('input', $attr); - - $ret .= $this->end('div'); - - return $ret; - } -} - -// vim: et sw=4 sts=4 +docURL = $doc_url; + $this->name = $name; + $this->compress = $compress; + // initialize sub-printers + $this->fields[0] = new HTMLPurifier_Printer_ConfigForm_default(); + $this->fields[HTMLPurifier_VarParser::BOOL] = new HTMLPurifier_Printer_ConfigForm_bool(); + } + + /** + * Sets default column and row size for textareas in sub-printers + * @param $cols Integer columns of textarea, null to use default + * @param $rows Integer rows of textarea, null to use default + */ + public function setTextareaDimensions($cols = null, $rows = null) + { + if ($cols) { + $this->fields['default']->cols = $cols; + } + if ($rows) { + $this->fields['default']->rows = $rows; + } + } + + /** + * Retrieves styling, in case it is not accessible by webserver + */ + public static function getCSS() + { + return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.css'); + } + + /** + * Retrieves JavaScript, in case it is not accessible by webserver + */ + public static function getJavaScript() + { + return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.js'); + } + + /** + * Returns HTML output for a configuration form + * @param HTMLPurifier_Config|array $config Configuration object of current form state, or an array + * where [0] has an HTML namespace and [1] is being rendered. + * @param array|bool $allowed Optional namespace(s) and directives to restrict form to. + * @param bool $render_controls + * @return string + */ + public function render($config, $allowed = true, $render_controls = true) + { + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + + $this->config = $config; + $this->genConfig = $gen_config; + $this->prepareGenerator($gen_config); + + $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $config->def); + $all = array(); + foreach ($allowed as $key) { + list($ns, $directive) = $key; + $all[$ns][$directive] = $config->get($ns . '.' . $directive); + } + + $ret = ''; + $ret .= $this->start('table', array('class' => 'hp-config')); + $ret .= $this->start('thead'); + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Directive', array('class' => 'hp-directive')); + $ret .= $this->element('th', 'Value', array('class' => 'hp-value')); + $ret .= $this->end('tr'); + $ret .= $this->end('thead'); + foreach ($all as $ns => $directives) { + $ret .= $this->renderNamespace($ns, $directives); + } + if ($render_controls) { + $ret .= $this->start('tbody'); + $ret .= $this->start('tr'); + $ret .= $this->start('td', array('colspan' => 2, 'class' => 'controls')); + $ret .= $this->elementEmpty('input', array('type' => 'submit', 'value' => 'Submit')); + $ret .= '[Reset]'; + $ret .= $this->end('td'); + $ret .= $this->end('tr'); + $ret .= $this->end('tbody'); + } + $ret .= $this->end('table'); + return $ret; + } + + /** + * Renders a single namespace + * @param $ns String namespace name + * @param array $directives array of directives to values + * @return string + */ + protected function renderNamespace($ns, $directives) + { + $ret = ''; + $ret .= $this->start('tbody', array('class' => 'namespace')); + $ret .= $this->start('tr'); + $ret .= $this->element('th', $ns, array('colspan' => 2)); + $ret .= $this->end('tr'); + $ret .= $this->end('tbody'); + $ret .= $this->start('tbody'); + foreach ($directives as $directive => $value) { + $ret .= $this->start('tr'); + $ret .= $this->start('th'); + if ($this->docURL) { + $url = str_replace('%s', urlencode("$ns.$directive"), $this->docURL); + $ret .= $this->start('a', array('href' => $url)); + } + $attr = array('for' => "{$this->name}:$ns.$directive"); + + // crop directive name if it's too long + if (!$this->compress || (strlen($directive) < $this->compress)) { + $directive_disp = $directive; + } else { + $directive_disp = substr($directive, 0, $this->compress - 2) . '...'; + $attr['title'] = $directive; + } + + $ret .= $this->element( + 'label', + $directive_disp, + // component printers must create an element with this id + $attr + ); + if ($this->docURL) { + $ret .= $this->end('a'); + } + $ret .= $this->end('th'); + + $ret .= $this->start('td'); + $def = $this->config->def->info["$ns.$directive"]; + if (is_int($def)) { + $allow_null = $def < 0; + $type = abs($def); + } else { + $type = $def->type; + $allow_null = isset($def->allow_null); + } + if (!isset($this->fields[$type])) { + $type = 0; + } // default + $type_obj = $this->fields[$type]; + if ($allow_null) { + $type_obj = new HTMLPurifier_Printer_ConfigForm_NullDecorator($type_obj); + } + $ret .= $type_obj->render($ns, $directive, $value, $this->name, array($this->genConfig, $this->config)); + $ret .= $this->end('td'); + $ret .= $this->end('tr'); + } + $ret .= $this->end('tbody'); + return $ret; + } + +} + +/** + * Printer decorator for directives that accept null + */ +class HTMLPurifier_Printer_ConfigForm_NullDecorator extends HTMLPurifier_Printer +{ + /** + * Printer being decorated + * @type HTMLPurifier_Printer + */ + protected $obj; + + /** + * @param HTMLPurifier_Printer $obj Printer to decorate + */ + public function __construct($obj) + { + parent::__construct(); + $this->obj = $obj; + } + + /** + * @param string $ns + * @param string $directive + * @param string $value + * @param string $name + * @param HTMLPurifier_Config|array $config + * @return string + */ + public function render($ns, $directive, $value, $name, $config) + { + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + $this->prepareGenerator($gen_config); + + $ret = ''; + $ret .= $this->start('label', array('for' => "$name:Null_$ns.$directive")); + $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); + $ret .= $this->text(' Null/Disabled'); + $ret .= $this->end('label'); + $attr = array( + 'type' => 'checkbox', + 'value' => '1', + 'class' => 'null-toggle', + 'name' => "$name" . "[Null_$ns.$directive]", + 'id' => "$name:Null_$ns.$directive", + 'onclick' => "toggleWriteability('$name:$ns.$directive',checked)" // INLINE JAVASCRIPT!!!! + ); + if ($this->obj instanceof HTMLPurifier_Printer_ConfigForm_bool) { + // modify inline javascript slightly + $attr['onclick'] = + "toggleWriteability('$name:Yes_$ns.$directive',checked);" . + "toggleWriteability('$name:No_$ns.$directive',checked)"; + } + if ($value === null) { + $attr['checked'] = 'checked'; + } + $ret .= $this->elementEmpty('input', $attr); + $ret .= $this->text(' or '); + $ret .= $this->elementEmpty('br'); + $ret .= $this->obj->render($ns, $directive, $value, $name, array($gen_config, $config)); + return $ret; + } +} + +/** + * Swiss-army knife configuration form field printer + */ +class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer +{ + /** + * @type int + */ + public $cols = 18; + + /** + * @type int + */ + public $rows = 5; + + /** + * @param string $ns + * @param string $directive + * @param string $value + * @param string $name + * @param HTMLPurifier_Config|array $config + * @return string + */ + public function render($ns, $directive, $value, $name, $config) + { + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + $this->prepareGenerator($gen_config); + // this should probably be split up a little + $ret = ''; + $def = $config->def->info["$ns.$directive"]; + if (is_int($def)) { + $type = abs($def); + } else { + $type = $def->type; + } + if (is_array($value)) { + switch ($type) { + case HTMLPurifier_VarParser::LOOKUP: + $array = $value; + $value = array(); + foreach ($array as $val => $b) { + $value[] = $val; + } + //TODO does this need a break? + case HTMLPurifier_VarParser::ALIST: + $value = implode(PHP_EOL, $value); + break; + case HTMLPurifier_VarParser::HASH: + $nvalue = ''; + foreach ($value as $i => $v) { + $nvalue .= "$i:$v" . PHP_EOL; + } + $value = $nvalue; + break; + default: + $value = ''; + } + } + if ($type === HTMLPurifier_VarParser::MIXED) { + return 'Not supported'; + $value = serialize($value); + } + $attr = array( + 'name' => "$name" . "[$ns.$directive]", + 'id' => "$name:$ns.$directive" + ); + if ($value === null) { + $attr['disabled'] = 'disabled'; + } + if (isset($def->allowed)) { + $ret .= $this->start('select', $attr); + foreach ($def->allowed as $val => $b) { + $attr = array(); + if ($value == $val) { + $attr['selected'] = 'selected'; + } + $ret .= $this->element('option', $val, $attr); + } + $ret .= $this->end('select'); + } elseif ($type === HTMLPurifier_VarParser::TEXT || + $type === HTMLPurifier_VarParser::ITEXT || + $type === HTMLPurifier_VarParser::ALIST || + $type === HTMLPurifier_VarParser::HASH || + $type === HTMLPurifier_VarParser::LOOKUP) { + $attr['cols'] = $this->cols; + $attr['rows'] = $this->rows; + $ret .= $this->start('textarea', $attr); + $ret .= $this->text($value); + $ret .= $this->end('textarea'); + } else { + $attr['value'] = $value; + $attr['type'] = 'text'; + $ret .= $this->elementEmpty('input', $attr); + } + return $ret; + } +} + +/** + * Bool form field printer + */ +class HTMLPurifier_Printer_ConfigForm_bool extends HTMLPurifier_Printer +{ + /** + * @param string $ns + * @param string $directive + * @param string $value + * @param string $name + * @param HTMLPurifier_Config|array $config + * @return string + */ + public function render($ns, $directive, $value, $name, $config) + { + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + $this->prepareGenerator($gen_config); + $ret = ''; + $ret .= $this->start('div', array('id' => "$name:$ns.$directive")); + + $ret .= $this->start('label', array('for' => "$name:Yes_$ns.$directive")); + $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); + $ret .= $this->text(' Yes'); + $ret .= $this->end('label'); + + $attr = array( + 'type' => 'radio', + 'name' => "$name" . "[$ns.$directive]", + 'id' => "$name:Yes_$ns.$directive", + 'value' => '1' + ); + if ($value === true) { + $attr['checked'] = 'checked'; + } + if ($value === null) { + $attr['disabled'] = 'disabled'; + } + $ret .= $this->elementEmpty('input', $attr); + + $ret .= $this->start('label', array('for' => "$name:No_$ns.$directive")); + $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); + $ret .= $this->text(' No'); + $ret .= $this->end('label'); + + $attr = array( + 'type' => 'radio', + 'name' => "$name" . "[$ns.$directive]", + 'id' => "$name:No_$ns.$directive", + 'value' => '0' + ); + if ($value === false) { + $attr['checked'] = 'checked'; + } + if ($value === null) { + $attr['disabled'] = 'disabled'; + } + $ret .= $this->elementEmpty('input', $attr); + + $ret .= $this->end('div'); + + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Printer/HTMLDefinition.php b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Printer/HTMLDefinition.php index 679d19ba3a1..5f2f2f8a7c2 100644 --- a/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Printer/HTMLDefinition.php +++ b/framework/vendors/htmlpurifier/standalone/HTMLPurifier/Printer/HTMLDefinition.php @@ -1,324 +1,324 @@ -config =& $config; - - $this->def = $config->getHTMLDefinition(); - - $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer')); - - $ret .= $this->renderDoctype(); - $ret .= $this->renderEnvironment(); - $ret .= $this->renderContentSets(); - $ret .= $this->renderInfo(); - - $ret .= $this->end('div'); - - return $ret; - } - - /** - * Renders the Doctype table - * @return string - */ - protected function renderDoctype() - { - $doctype = $this->def->doctype; - $ret = ''; - $ret .= $this->start('table'); - $ret .= $this->element('caption', 'Doctype'); - $ret .= $this->row('Name', $doctype->name); - $ret .= $this->row('XML', $doctype->xml ? 'Yes' : 'No'); - $ret .= $this->row('Default Modules', implode($doctype->modules, ', ')); - $ret .= $this->row('Default Tidy Modules', implode($doctype->tidyModules, ', ')); - $ret .= $this->end('table'); - return $ret; - } - - - /** - * Renders environment table, which is miscellaneous info - * @return string - */ - protected function renderEnvironment() - { - $def = $this->def; - - $ret = ''; - - $ret .= $this->start('table'); - $ret .= $this->element('caption', 'Environment'); - - $ret .= $this->row('Parent of fragment', $def->info_parent); - $ret .= $this->renderChildren($def->info_parent_def->child); - $ret .= $this->row('Block wrap name', $def->info_block_wrapper); - - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Global attributes'); - $ret .= $this->element('td', $this->listifyAttr($def->info_global_attr), null, 0); - $ret .= $this->end('tr'); - - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Tag transforms'); - $list = array(); - foreach ($def->info_tag_transform as $old => $new) { - $new = $this->getClass($new, 'TagTransform_'); - $list[] = "<$old> with $new"; - } - $ret .= $this->element('td', $this->listify($list)); - $ret .= $this->end('tr'); - - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Pre-AttrTransform'); - $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_pre)); - $ret .= $this->end('tr'); - - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Post-AttrTransform'); - $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_post)); - $ret .= $this->end('tr'); - - $ret .= $this->end('table'); - return $ret; - } - - /** - * Renders the Content Sets table - * @return string - */ - protected function renderContentSets() - { - $ret = ''; - $ret .= $this->start('table'); - $ret .= $this->element('caption', 'Content Sets'); - foreach ($this->def->info_content_sets as $name => $lookup) { - $ret .= $this->heavyHeader($name); - $ret .= $this->start('tr'); - $ret .= $this->element('td', $this->listifyTagLookup($lookup)); - $ret .= $this->end('tr'); - } - $ret .= $this->end('table'); - return $ret; - } - - /** - * Renders the Elements ($info) table - * @return string - */ - protected function renderInfo() - { - $ret = ''; - $ret .= $this->start('table'); - $ret .= $this->element('caption', 'Elements ($info)'); - ksort($this->def->info); - $ret .= $this->heavyHeader('Allowed tags', 2); - $ret .= $this->start('tr'); - $ret .= $this->element('td', $this->listifyTagLookup($this->def->info), array('colspan' => 2)); - $ret .= $this->end('tr'); - foreach ($this->def->info as $name => $def) { - $ret .= $this->start('tr'); - $ret .= $this->element('th', "<$name>", array('class' => 'heavy', 'colspan' => 2)); - $ret .= $this->end('tr'); - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Inline content'); - $ret .= $this->element('td', $def->descendants_are_inline ? 'Yes' : 'No'); - $ret .= $this->end('tr'); - if (!empty($def->excludes)) { - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Excludes'); - $ret .= $this->element('td', $this->listifyTagLookup($def->excludes)); - $ret .= $this->end('tr'); - } - if (!empty($def->attr_transform_pre)) { - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Pre-AttrTransform'); - $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_pre)); - $ret .= $this->end('tr'); - } - if (!empty($def->attr_transform_post)) { - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Post-AttrTransform'); - $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_post)); - $ret .= $this->end('tr'); - } - if (!empty($def->auto_close)) { - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Auto closed by'); - $ret .= $this->element('td', $this->listifyTagLookup($def->auto_close)); - $ret .= $this->end('tr'); - } - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Allowed attributes'); - $ret .= $this->element('td', $this->listifyAttr($def->attr), array(), 0); - $ret .= $this->end('tr'); - - if (!empty($def->required_attr)) { - $ret .= $this->row('Required attributes', $this->listify($def->required_attr)); - } - - $ret .= $this->renderChildren($def->child); - } - $ret .= $this->end('table'); - return $ret; - } - - /** - * Renders a row describing the allowed children of an element - * @param HTMLPurifier_ChildDef $def HTMLPurifier_ChildDef of pertinent element - * @return string - */ - protected function renderChildren($def) - { - $context = new HTMLPurifier_Context(); - $ret = ''; - $ret .= $this->start('tr'); - $elements = array(); - $attr = array(); - if (isset($def->elements)) { - if ($def->type == 'strictblockquote') { - $def->validateChildren(array(), $this->config, $context); - } - $elements = $def->elements; - } - if ($def->type == 'chameleon') { - $attr['rowspan'] = 2; - } elseif ($def->type == 'empty') { - $elements = array(); - } elseif ($def->type == 'table') { - $elements = array_flip( - array( - 'col', - 'caption', - 'colgroup', - 'thead', - 'tfoot', - 'tbody', - 'tr' - ) - ); - } - $ret .= $this->element('th', 'Allowed children', $attr); - - if ($def->type == 'chameleon') { - - $ret .= $this->element( - 'td', - 'Block: ' . - $this->escape($this->listifyTagLookup($def->block->elements)), - null, - 0 - ); - $ret .= $this->end('tr'); - $ret .= $this->start('tr'); - $ret .= $this->element( - 'td', - 'Inline: ' . - $this->escape($this->listifyTagLookup($def->inline->elements)), - null, - 0 - ); - - } elseif ($def->type == 'custom') { - - $ret .= $this->element( - 'td', - '' . ucfirst($def->type) . ': ' . - $def->dtd_regex - ); - - } else { - $ret .= $this->element( - 'td', - '' . ucfirst($def->type) . ': ' . - $this->escape($this->listifyTagLookup($elements)), - null, - 0 - ); - } - $ret .= $this->end('tr'); - return $ret; - } - - /** - * Listifies a tag lookup table. - * @param array $array Tag lookup array in form of array('tagname' => true) - * @return string - */ - protected function listifyTagLookup($array) - { - ksort($array); - $list = array(); - foreach ($array as $name => $discard) { - if ($name !== '#PCDATA' && !isset($this->def->info[$name])) { - continue; - } - $list[] = $name; - } - return $this->listify($list); - } - - /** - * Listifies a list of objects by retrieving class names and internal state - * @param array $array List of objects - * @return string - * @todo Also add information about internal state - */ - protected function listifyObjectList($array) - { - ksort($array); - $list = array(); - foreach ($array as $obj) { - $list[] = $this->getClass($obj, 'AttrTransform_'); - } - return $this->listify($list); - } - - /** - * Listifies a hash of attributes to AttrDef classes - * @param array $array Array hash in form of array('attrname' => HTMLPurifier_AttrDef) - * @return string - */ - protected function listifyAttr($array) - { - ksort($array); - $list = array(); - foreach ($array as $name => $obj) { - if ($obj === false) { - continue; - } - $list[] = "$name = " . $this->getClass($obj, 'AttrDef_') . ''; - } - return $this->listify($list); - } - - /** - * Creates a heavy header row - * @param string $text - * @param int $num - * @return string - */ - protected function heavyHeader($text, $num = 1) - { - $ret = ''; - $ret .= $this->start('tr'); - $ret .= $this->element('th', $text, array('colspan' => $num, 'class' => 'heavy')); - $ret .= $this->end('tr'); - return $ret; - } -} - -// vim: et sw=4 sts=4 +config =& $config; + + $this->def = $config->getHTMLDefinition(); + + $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer')); + + $ret .= $this->renderDoctype(); + $ret .= $this->renderEnvironment(); + $ret .= $this->renderContentSets(); + $ret .= $this->renderInfo(); + + $ret .= $this->end('div'); + + return $ret; + } + + /** + * Renders the Doctype table + * @return string + */ + protected function renderDoctype() + { + $doctype = $this->def->doctype; + $ret = ''; + $ret .= $this->start('table'); + $ret .= $this->element('caption', 'Doctype'); + $ret .= $this->row('Name', $doctype->name); + $ret .= $this->row('XML', $doctype->xml ? 'Yes' : 'No'); + $ret .= $this->row('Default Modules', implode($doctype->modules, ', ')); + $ret .= $this->row('Default Tidy Modules', implode($doctype->tidyModules, ', ')); + $ret .= $this->end('table'); + return $ret; + } + + + /** + * Renders environment table, which is miscellaneous info + * @return string + */ + protected function renderEnvironment() + { + $def = $this->def; + + $ret = ''; + + $ret .= $this->start('table'); + $ret .= $this->element('caption', 'Environment'); + + $ret .= $this->row('Parent of fragment', $def->info_parent); + $ret .= $this->renderChildren($def->info_parent_def->child); + $ret .= $this->row('Block wrap name', $def->info_block_wrapper); + + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Global attributes'); + $ret .= $this->element('td', $this->listifyAttr($def->info_global_attr), null, 0); + $ret .= $this->end('tr'); + + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Tag transforms'); + $list = array(); + foreach ($def->info_tag_transform as $old => $new) { + $new = $this->getClass($new, 'TagTransform_'); + $list[] = "<$old> with $new"; + } + $ret .= $this->element('td', $this->listify($list)); + $ret .= $this->end('tr'); + + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Pre-AttrTransform'); + $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_pre)); + $ret .= $this->end('tr'); + + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Post-AttrTransform'); + $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_post)); + $ret .= $this->end('tr'); + + $ret .= $this->end('table'); + return $ret; + } + + /** + * Renders the Content Sets table + * @return string + */ + protected function renderContentSets() + { + $ret = ''; + $ret .= $this->start('table'); + $ret .= $this->element('caption', 'Content Sets'); + foreach ($this->def->info_content_sets as $name => $lookup) { + $ret .= $this->heavyHeader($name); + $ret .= $this->start('tr'); + $ret .= $this->element('td', $this->listifyTagLookup($lookup)); + $ret .= $this->end('tr'); + } + $ret .= $this->end('table'); + return $ret; + } + + /** + * Renders the Elements ($info) table + * @return string + */ + protected function renderInfo() + { + $ret = ''; + $ret .= $this->start('table'); + $ret .= $this->element('caption', 'Elements ($info)'); + ksort($this->def->info); + $ret .= $this->heavyHeader('Allowed tags', 2); + $ret .= $this->start('tr'); + $ret .= $this->element('td', $this->listifyTagLookup($this->def->info), array('colspan' => 2)); + $ret .= $this->end('tr'); + foreach ($this->def->info as $name => $def) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', "<$name>", array('class' => 'heavy', 'colspan' => 2)); + $ret .= $this->end('tr'); + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Inline content'); + $ret .= $this->element('td', $def->descendants_are_inline ? 'Yes' : 'No'); + $ret .= $this->end('tr'); + if (!empty($def->excludes)) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Excludes'); + $ret .= $this->element('td', $this->listifyTagLookup($def->excludes)); + $ret .= $this->end('tr'); + } + if (!empty($def->attr_transform_pre)) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Pre-AttrTransform'); + $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_pre)); + $ret .= $this->end('tr'); + } + if (!empty($def->attr_transform_post)) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Post-AttrTransform'); + $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_post)); + $ret .= $this->end('tr'); + } + if (!empty($def->auto_close)) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Auto closed by'); + $ret .= $this->element('td', $this->listifyTagLookup($def->auto_close)); + $ret .= $this->end('tr'); + } + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Allowed attributes'); + $ret .= $this->element('td', $this->listifyAttr($def->attr), array(), 0); + $ret .= $this->end('tr'); + + if (!empty($def->required_attr)) { + $ret .= $this->row('Required attributes', $this->listify($def->required_attr)); + } + + $ret .= $this->renderChildren($def->child); + } + $ret .= $this->end('table'); + return $ret; + } + + /** + * Renders a row describing the allowed children of an element + * @param HTMLPurifier_ChildDef $def HTMLPurifier_ChildDef of pertinent element + * @return string + */ + protected function renderChildren($def) + { + $context = new HTMLPurifier_Context(); + $ret = ''; + $ret .= $this->start('tr'); + $elements = array(); + $attr = array(); + if (isset($def->elements)) { + if ($def->type == 'strictblockquote') { + $def->validateChildren(array(), $this->config, $context); + } + $elements = $def->elements; + } + if ($def->type == 'chameleon') { + $attr['rowspan'] = 2; + } elseif ($def->type == 'empty') { + $elements = array(); + } elseif ($def->type == 'table') { + $elements = array_flip( + array( + 'col', + 'caption', + 'colgroup', + 'thead', + 'tfoot', + 'tbody', + 'tr' + ) + ); + } + $ret .= $this->element('th', 'Allowed children', $attr); + + if ($def->type == 'chameleon') { + + $ret .= $this->element( + 'td', + 'Block: ' . + $this->escape($this->listifyTagLookup($def->block->elements)), + null, + 0 + ); + $ret .= $this->end('tr'); + $ret .= $this->start('tr'); + $ret .= $this->element( + 'td', + 'Inline: ' . + $this->escape($this->listifyTagLookup($def->inline->elements)), + null, + 0 + ); + + } elseif ($def->type == 'custom') { + + $ret .= $this->element( + 'td', + '' . ucfirst($def->type) . ': ' . + $def->dtd_regex + ); + + } else { + $ret .= $this->element( + 'td', + '' . ucfirst($def->type) . ': ' . + $this->escape($this->listifyTagLookup($elements)), + null, + 0 + ); + } + $ret .= $this->end('tr'); + return $ret; + } + + /** + * Listifies a tag lookup table. + * @param array $array Tag lookup array in form of array('tagname' => true) + * @return string + */ + protected function listifyTagLookup($array) + { + ksort($array); + $list = array(); + foreach ($array as $name => $discard) { + if ($name !== '#PCDATA' && !isset($this->def->info[$name])) { + continue; + } + $list[] = $name; + } + return $this->listify($list); + } + + /** + * Listifies a list of objects by retrieving class names and internal state + * @param array $array List of objects + * @return string + * @todo Also add information about internal state + */ + protected function listifyObjectList($array) + { + ksort($array); + $list = array(); + foreach ($array as $obj) { + $list[] = $this->getClass($obj, 'AttrTransform_'); + } + return $this->listify($list); + } + + /** + * Listifies a hash of attributes to AttrDef classes + * @param array $array Array hash in form of array('attrname' => HTMLPurifier_AttrDef) + * @return string + */ + protected function listifyAttr($array) + { + ksort($array); + $list = array(); + foreach ($array as $name => $obj) { + if ($obj === false) { + continue; + } + $list[] = "$name = " . $this->getClass($obj, 'AttrDef_') . ''; + } + return $this->listify($list); + } + + /** + * Creates a heavy header row + * @param string $text + * @param int $num + * @return string + */ + protected function heavyHeader($text, $num = 1) + { + $ret = ''; + $ret .= $this->start('tr'); + $ret .= $this->element('th', $text, array('colspan' => $num, 'class' => 'heavy')); + $ret .= $this->end('tr'); + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/framework/views/fr/error.php b/framework/views/fr/error.php index 3df6624cc44..ea2bf712e13 100644 --- a/framework/views/fr/error.php +++ b/framework/views/fr/error.php @@ -1,36 +1,36 @@ - - - - - -Erreur <?php echo $data['code']; ?> - - - -

              Erreur

              -

              -

              -L'erreur ci-dessus est apparue pendant que le serveur Web traitait votre requĂŞte. -

              -

              -Si vous pensez qu'il s'agit d'une erreur du serveur, veuillez contacter . -

              -

              -Merci. -

              -
              - -
              - + + + + + +Erreur <?php echo $data['code']; ?> + + + +

              Erreur

              +

              +

              +L'erreur ci-dessus est apparue pendant que le serveur Web traitait votre requĂŞte. +

              +

              +Si vous pensez qu'il s'agit d'une erreur du serveur, veuillez contacter . +

              +

              +Merci. +

              +
              + +
              + \ No newline at end of file diff --git a/framework/views/fr/error400.php b/framework/views/fr/error400.php index 31a32e396bf..efa4e612cb0 100644 --- a/framework/views/fr/error400.php +++ b/framework/views/fr/error400.php @@ -1,34 +1,34 @@ - - - - - -Demande incorrecte - - - -

              Demande Incorrecte

              -

              -

              -La demande n'a pas pu être interprétée par le serveur, due à une mauvaise syntaxe. -Merci de ne pas répeter la requete sans modifications. -

              -

              -Si vous pensez qu'il s'agit d'une erreur du serveur, veuillez contacter . -

              -
              - -
              - + + + + + +Demande incorrecte + + + +

              Demande Incorrecte

              +

              +

              +La demande n'a pas pu être interprétée par le serveur, due à une mauvaise syntaxe. +Merci de ne pas répeter la requete sans modifications. +

              +

              +Si vous pensez qu'il s'agit d'une erreur du serveur, veuillez contacter . +

              +
              + +
              + \ No newline at end of file diff --git a/framework/views/fr/error403.php b/framework/views/fr/error403.php index f7ad53cbbf2..625c2360312 100644 --- a/framework/views/fr/error403.php +++ b/framework/views/fr/error403.php @@ -1,33 +1,33 @@ - - - - - -Accès interdit - - - -

              Accès interdit

              -

              -

              -Vous n'avez pas les autorisations nécessaires pour accéder à cette page. -

              -

              -Si vous pensez qu'il s'agit d'une erreur du serveur, veuillez contacter . -

              -
              - -
              - + + + + + +Accès interdit + + + +

              Accès interdit

              +

              +

              +Vous n'avez pas les autorisations nécessaires pour accéder à cette page. +

              +

              +Si vous pensez qu'il s'agit d'une erreur du serveur, veuillez contacter . +

              +
              + +
              + \ No newline at end of file diff --git a/framework/views/fr/error404.php b/framework/views/fr/error404.php index 1d7fb2fccc9..5eb9d3bfb8c 100644 --- a/framework/views/fr/error404.php +++ b/framework/views/fr/error404.php @@ -1,34 +1,34 @@ - - - - - -Page Non trouvée - - - -

              Page Non trouvée

              -

              -

              -L'URL demandée n'existe pas sur ce serveur. -Si vous avez saisi l'URL manuellement, vérifiez la, et réessayez. -

              -

              -Si vous pensez qu'il s'agit d'une erreur du serveur, veuillez contacter . -

              -
              - -
              - + + + + + +Page Non trouvée + + + +

              Page Non trouvée

              +

              +

              +L'URL demandée n'existe pas sur ce serveur. +Si vous avez saisi l'URL manuellement, vérifiez la, et réessayez. +

              +

              +Si vous pensez qu'il s'agit d'une erreur du serveur, veuillez contacter . +

              +
              + +
              + \ No newline at end of file diff --git a/framework/views/fr/error500.php b/framework/views/fr/error500.php index fe6b5f0658c..e40513a39a7 100644 --- a/framework/views/fr/error500.php +++ b/framework/views/fr/error500.php @@ -1,34 +1,34 @@ - - - - - -Erreur interne du serveur - - - -

              Erreur interne du serveur

              -

              -

              -Une erreur interne est apparue lorsque le serveur web traitait votre requete. -Veuillez contacter pour signaler ce problème. -

              -

              -Merci. -

              -
              - -
              - + + + + + +Erreur interne du serveur + + + +

              Erreur interne du serveur

              +

              +

              +Une erreur interne est apparue lorsque le serveur web traitait votre requete. +Veuillez contacter pour signaler ce problème. +

              +

              +Merci. +

              +
              + +
              + \ No newline at end of file diff --git a/framework/views/fr/error503.php b/framework/views/fr/error503.php index 918e0e41216..ba9d11d8f06 100644 --- a/framework/views/fr/error503.php +++ b/framework/views/fr/error503.php @@ -1,32 +1,32 @@ - - - - - -Service non disponible - - - -

              Service non disponible

              -

              -Notre système est momentanément indisponible. Merci de réessayer plus tard. -

              -

              -Merci. -

              -
              - -
              - + + + + + +Service non disponible + + + +

              Service non disponible

              +

              +Notre système est momentanément indisponible. Merci de réessayer plus tard. +

              +

              +Merci. +

              +
              + +
              + \ No newline at end of file diff --git a/framework/views/fr/log-firebug.php b/framework/views/fr/log-firebug.php index 3a5539c4e2e..244e2f92adb 100644 --- a/framework/views/fr/log-firebug.php +++ b/framework/views/fr/log-firebug.php @@ -1,23 +1,23 @@ - \ No newline at end of file diff --git a/framework/views/fr/log.php b/framework/views/fr/log.php index 17516914282..1d8ab0b6fdd 100644 --- a/framework/views/fr/log.php +++ b/framework/views/fr/log.php @@ -1,40 +1,40 @@ - - - - - - - - - - - -'#DFFFE0', - CLogger::LEVEL_INFO=>'#FFFFDF', - CLogger::LEVEL_WARNING=>'#FFDFE5', - CLogger::LEVEL_ERROR=>'#FFC0CB', -); -foreach($data as $index=>$log) -{ - $color=($index%2)?'#F5F5F5':'#FFFFFF'; - if(isset($colors[$log[1]])) - $color=$colors[$log[1]]; - $message='
              '.CHtml::encode(wordwrap($log[0])).'
              '; - $time=date('H:i:s.',$log[3]).(int)(($log[3]-(int)$log[3])*1000000); - - echo << - - - - - -EOD; -} -?> -
              - Journal d'application -
              HeureNiveauCategorieMessage
              {$time}{$log[1]}{$log[2]}{$message}
              + + + + + + + + + + + +'#DFFFE0', + CLogger::LEVEL_INFO=>'#FFFFDF', + CLogger::LEVEL_WARNING=>'#FFDFE5', + CLogger::LEVEL_ERROR=>'#FFC0CB', +); +foreach($data as $index=>$log) +{ + $color=($index%2)?'#F5F5F5':'#FFFFFF'; + if(isset($colors[$log[1]])) + $color=$colors[$log[1]]; + $message='
              '.CHtml::encode(wordwrap($log[0])).'
              '; + $time=date('H:i:s.',$log[3]).(int)(($log[3]-(int)$log[3])*1000000); + + echo << + + + + + +EOD; +} +?> +
              + Journal d'application +
              HeureNiveauCategorieMessage
              {$time}{$log[1]}{$log[2]}{$message}
              \ No newline at end of file diff --git a/framework/views/fr/profile-callstack-firebug.php b/framework/views/fr/profile-callstack-firebug.php index e9ae1766618..cfe1a269604 100644 --- a/framework/views/fr/profile-callstack-firebug.php +++ b/framework/views/fr/profile-callstack-firebug.php @@ -1,19 +1,19 @@ - \ No newline at end of file diff --git a/framework/views/fr/profile-callstack.php b/framework/views/fr/profile-callstack.php index 5aa9095ff46..adbd96ffe49 100644 --- a/framework/views/fr/profile-callstack.php +++ b/framework/views/fr/profile-callstack.php @@ -1,30 +1,30 @@ - - - - - - - - - -$entry) -{ - $color=($index%2)?'#F5F5F5':'#FFFFFF'; - list($proc,$time,$level)=$entry; - $proc=CHtml::encode($proc); - $time=sprintf('%0.5f',$time); - $spaces=str_repeat(' ',$level*8); - - echo << - - - -EOD; -} -?> -
              - Rapport de l'analyse de la pile d'appel -
              FonctionDurée (s)
              {$spaces}{$proc}{$time}
              - + + + + + + + + + +$entry) +{ + $color=($index%2)?'#F5F5F5':'#FFFFFF'; + list($proc,$time,$level)=$entry; + $proc=CHtml::encode($proc); + $time=sprintf('%0.5f',$time); + $spaces=str_repeat(' ',$level*8); + + echo << + + + +EOD; +} +?> +
              + Rapport de l'analyse de la pile d'appel +
              FonctionDurée (s)
              {$spaces}{$proc}{$time}
              + diff --git a/framework/views/fr/profile-summary-firebug.php b/framework/views/fr/profile-summary-firebug.php index 01afc08e2d2..4b9454c5a60 100644 --- a/framework/views/fr/profile-summary-firebug.php +++ b/framework/views/fr/profile-summary-firebug.php @@ -1,22 +1,22 @@ - + diff --git a/framework/views/fr/profile-summary.php b/framework/views/fr/profile-summary.php index 9501431d1b9..fb2c21bae8d 100644 --- a/framework/views/fr/profile-summary.php +++ b/framework/views/fr/profile-summary.php @@ -1,41 +1,41 @@ - - - - - - - - - - - - - -$entry) -{ - $color=($index%2)?'#F5F5F5':'#FFFFFF'; - $proc=CHtml::encode($entry[0]); - $min=sprintf('%0.5f',$entry[2]); - $max=sprintf('%0.5f',$entry[3]); - $total=sprintf('%0.5f',$entry[4]); - $average=sprintf('%0.5f',$entry[4]/$entry[1]); - - echo << - - - - - - - -EOD; -} -?> -
              - Sommaire du rapport de profilage - (Durée: getExecutionTime()); ?>s, - Memoire: getMemoryUsage()/1024); ?>KB) -
              FonctionNbTotal (s)Moy. (s)Min. (s)Max. (s)
              {$proc}{$entry[1]}{$total}{$average}{$min}{$max}
              - + + + + + + + + + + + + + +$entry) +{ + $color=($index%2)?'#F5F5F5':'#FFFFFF'; + $proc=CHtml::encode($entry[0]); + $min=sprintf('%0.5f',$entry[2]); + $max=sprintf('%0.5f',$entry[3]); + $total=sprintf('%0.5f',$entry[4]); + $average=sprintf('%0.5f',$entry[4]/$entry[1]); + + echo << + + + + + + + +EOD; +} +?> +
              + Sommaire du rapport de profilage + (Durée: getExecutionTime()); ?>s, + Memoire: getMemoryUsage()/1024); ?>KB) +
              FonctionNbTotal (s)Moy. (s)Min. (s)Max. (s)
              {$proc}{$entry[1]}{$total}{$average}{$min}{$max}
              + diff --git a/framework/views/nl/profile-summary-firebug.php b/framework/views/nl/profile-summary-firebug.php index fd90c1a3b40..24584e9b957 100644 --- a/framework/views/nl/profile-summary-firebug.php +++ b/framework/views/nl/profile-summary-firebug.php @@ -1,20 +1,20 @@ - \ No newline at end of file diff --git a/framework/views/nl/profile-summary.php b/framework/views/nl/profile-summary.php index 6d904dec9e8..a6d9dca40ea 100644 --- a/framework/views/nl/profile-summary.php +++ b/framework/views/nl/profile-summary.php @@ -1,41 +1,41 @@ - - - - - - - - - - - - - -$entry) -{ - $color=($index%2)?'#F5F5F5':'#FFFFFF'; - $proc=CHtml::encode($entry[0]); - $min=sprintf('%0.5f',$entry[2]); - $max=sprintf('%0.5f',$entry[3]); - $total=sprintf('%0.5f',$entry[4]); - $average=sprintf('%0.5f',$entry[4]/$entry[1]); - - echo << - - - - - - - -EOD; -} -?> -
              - Profiling Samenvatting Rapport - (Tijd: getExecutionTime()); ?>s, - Geheugen: getMemoryUsage()/1024); ?>KB) -
              ProcedureAantalTotaal (s)Gem. (s)Min. (s)Max. (s)
              {$proc}{$entry[1]}{$total}{$average}{$min}{$max}
              + + + + + + + + + + + + + +$entry) +{ + $color=($index%2)?'#F5F5F5':'#FFFFFF'; + $proc=CHtml::encode($entry[0]); + $min=sprintf('%0.5f',$entry[2]); + $max=sprintf('%0.5f',$entry[3]); + $total=sprintf('%0.5f',$entry[4]); + $average=sprintf('%0.5f',$entry[4]/$entry[1]); + + echo << + + + + + + + +EOD; +} +?> +
              + Profiling Samenvatting Rapport + (Tijd: getExecutionTime()); ?>s, + Geheugen: getMemoryUsage()/1024); ?>KB) +
              ProcedureAantalTotaal (s)Gem. (s)Min. (s)Max. (s)
              {$proc}{$entry[1]}{$total}{$average}{$min}{$max}
              \ No newline at end of file diff --git a/framework/views/sk/log.php b/framework/views/sk/log.php index a8c704c7618..caab08a2278 100644 --- a/framework/views/sk/log.php +++ b/framework/views/sk/log.php @@ -1,46 +1,46 @@ - - - - - - - - - - - -'#DFFFE0', - CLogger::LEVEL_INFO=>'#FFFFDF', - CLogger::LEVEL_WARNING=>'#FFDFE5', - CLogger::LEVEL_ERROR=>'#FFC0CB', -); -foreach($data as $index=>$log) -{ - $color=($index%2)?'#F5F5F5':'#FFFFFF'; - if(isset($colors[$log[1]])) - $color=$colors[$log[1]]; - $message='
              '.CHtml::encode(wordwrap($log[0])).'
              '; - $time=date('H:i:s.',$log[3]).(int)(($log[3]-(int)$log[3])*1000000); - $time .= "
              [+".round(($log[3]-YII_BEGIN_TIME)*1000, 0).'] ms'; - - echo << - - - - - -EOD; -} -?> -
              - Log aplikácie - -
              ÄŚasĂšroveĹKategĂłriaZpráva
              {$time}{$log[1]}{$log[2]}{$message}
              + + + + + + + + + + + +'#DFFFE0', + CLogger::LEVEL_INFO=>'#FFFFDF', + CLogger::LEVEL_WARNING=>'#FFDFE5', + CLogger::LEVEL_ERROR=>'#FFC0CB', +); +foreach($data as $index=>$log) +{ + $color=($index%2)?'#F5F5F5':'#FFFFFF'; + if(isset($colors[$log[1]])) + $color=$colors[$log[1]]; + $message='
              '.CHtml::encode(wordwrap($log[0])).'
              '; + $time=date('H:i:s.',$log[3]).(int)(($log[3]-(int)$log[3])*1000000); + $time .= "
              [+".round(($log[3]-YII_BEGIN_TIME)*1000, 0).'] ms'; + + echo << + + + + + +EOD; +} +?> +
              + Log aplikácie + +
              ÄŚasĂšroveĹKategĂłriaZpráva
              {$time}{$log[1]}{$log[2]}{$message}
              \ No newline at end of file diff --git a/framework/views/zh_cn/profile-callstack.php b/framework/views/zh_cn/profile-callstack.php index 61b3006555f..60c5a96abe0 100644 --- a/framework/views/zh_cn/profile-callstack.php +++ b/framework/views/zh_cn/profile-callstack.php @@ -8,21 +8,21 @@ 步骤 时间 (秒) - $entry ) { - $color = ($index % 2) ? '#F5F5F5' : '#FFFFFF'; - list ( $proc, $time, $level ) = $entry; - $proc = CHtml::encode ( $proc ); - $time = sprintf ( '%0.5f', $time ); - $spaces = str_repeat ( ' ', $level * 8 ); - + $entry ) { + $color = ($index % 2) ? '#F5F5F5' : '#FFFFFF'; + list ( $proc, $time, $level ) = $entry; + $proc = CHtml::encode ( $proc ); + $time = sprintf ( '%0.5f', $time ); + $spaces = str_repeat ( ' ', $level * 8 ); + echo << {$spaces}{$proc} {$time} -EOD; -} +EOD; +} ?> \ No newline at end of file diff --git a/framework/web/CCacheHttpSession.php b/framework/web/CCacheHttpSession.php index 5b48fae478c..4067ea45b20 100644 --- a/framework/web/CCacheHttpSession.php +++ b/framework/web/CCacheHttpSession.php @@ -97,7 +97,7 @@ public function writeSession($id,$data) */ public function destroySession($id) { - return $this->_cache->delete($this->calculateKey($id)); + return $this->_cache->delete($this->calculateKey($id)); } /** @@ -107,6 +107,6 @@ public function destroySession($id) */ protected function calculateKey($id) { - return self::CACHE_KEY_PREFIX.$id; + return self::CACHE_KEY_PREFIX.$id; } } diff --git a/framework/web/CDbHttpSession.php b/framework/web/CDbHttpSession.php index a5ecf918649..3e5ea062478 100644 --- a/framework/web/CDbHttpSession.php +++ b/framework/web/CDbHttpSession.php @@ -246,7 +246,7 @@ public function writeSession($id,$data) { $expire=time()+$this->getTimeout(); $db=$this->getDbConnection(); - if($db->getDriverName()=='pgsql' ) + if($db->getDriverName()=='pgsql') $data=new CDbExpression("convert_to(".$db->quoteValue($data).", 'UTF8')"); if($db->getDriverName()=='sqlsrv' || $db->getDriverName()=='mssql' || $db->getDriverName()=='dblib') $data=new CDbExpression('CONVERT(VARBINARY(MAX), '.$db->quoteValue($data).')'); diff --git a/framework/web/CHttpRequest.php b/framework/web/CHttpRequest.php index 2075d8b70e3..64863280dd4 100644 --- a/framework/web/CHttpRequest.php +++ b/framework/web/CHttpRequest.php @@ -63,6 +63,12 @@ */ class CHttpRequest extends CApplicationComponent { + /** + * @var boolean whether the parsing of JSON REST requests should return associative arrays for object data. + * @see getRestParams + * @since 1.1.17 + */ + public $jsonAsArray = true; /** * @var boolean whether cookies should be validated to ensure they are not tampered. Defaults to false. */ @@ -287,7 +293,9 @@ public function getRestParams() if($this->_restParams===null) { $result=array(); - if(function_exists('mb_parse_str')) + if (strncmp($this->getContentType(), 'application/json', 16) === 0) + $result = CJSON::decode($this->getRawBody(), $this->jsonAsArray); + elseif(function_exists('mb_parse_str')) mb_parse_str($this->getRawBody(), $result); else parse_str($this->getRawBody(), $result); @@ -473,9 +481,9 @@ public function getPathInfo() else throw new CException(Yii::t('yii','CHttpRequest is unable to determine the path info of the request.')); - if($pathInfo==='/') + if($pathInfo==='/' || $pathInfo===false) $pathInfo=''; - elseif($pathInfo[0]==='/') + elseif($pathInfo!=='' && $pathInfo[0]==='/') $pathInfo=substr($pathInfo,1); if(($posEnd=strlen($pathInfo)-1)>0 && $pathInfo[$posEnd]==='/') @@ -587,8 +595,8 @@ public function getRequestType() { if(isset($_POST['_method'])) return strtoupper($_POST['_method']); - elseif(isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) - return strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']); + elseif(isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) + return strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']); return strtoupper(isset($_SERVER['REQUEST_METHOD'])?$_SERVER['REQUEST_METHOD']:'GET'); } @@ -767,6 +775,27 @@ public function getAcceptTypes() { return isset($_SERVER['HTTP_ACCEPT'])?$_SERVER['HTTP_ACCEPT']:null; } + + /** + * Returns request content-type + * The Content-Type header field indicates the MIME type of the data + * contained in {@link getRawBody()} or, in the case of the HEAD method, the + * media type that would have been sent had the request been a GET. + * @return string request content-type. Null is returned if this information is not available. + * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17 + * HTTP 1.1 header field definitions + * @since 1.1.17 + */ + public function getContentType() + { + if (isset($_SERVER["CONTENT_TYPE"])) { + return $_SERVER["CONTENT_TYPE"]; + } elseif (isset($_SERVER["HTTP_CONTENT_TYPE"])) { + //fix bug https://bugs.php.net/bug.php?id=66606 + return $_SERVER["HTTP_CONTENT_TYPE"]; + } + return null; + } private $_port; @@ -1051,7 +1080,7 @@ public function getPreferredLanguage($languages=array()) foreach ($languages as $language) { $language=CLocale::getCanonicalID($language); // en_us==en_us, en==en_us, en_us==en - if($language===$acceptedLanguage || strpos($acceptedLanguage,$language.'_')===0 || strpos($language,$acceptedLanguage.'_')===0) { + if($language===$preferredLanguage || strpos($preferredLanguage,$language.'_')===0 || strpos($language,$preferredLanguage.'_')===0) { return $language; } } @@ -1135,7 +1164,7 @@ public function sendFile($fileName,$content,$mimeType=null,$terminate=true) header('Content-Length: '.$length); header("Content-Disposition: attachment; filename=\"$fileName\""); header('Content-Transfer-Encoding: binary'); - $content=function_exists('mb_substr') ? mb_substr($content,$contentStart,$length) : substr($content,$contentStart,$length); + $content=function_exists('mb_substr') ? mb_substr($content,$contentStart,$length,'8bit') : substr($content,$contentStart,$length); if($terminate) { diff --git a/framework/web/CUploadedFile.php b/framework/web/CUploadedFile.php index 8e9ccf05d75..d0276c37ce4 100644 --- a/framework/web/CUploadedFile.php +++ b/framework/web/CUploadedFile.php @@ -189,6 +189,9 @@ public function __toString() * @param boolean $deleteTempFile whether to delete the temporary file after saving. * If true, you will not be able to save the uploaded file again in the current request. * @return boolean true whether the file is saved successfully + * + * In some exceptional cases such as not enough permissions to write to the path specified + * PHP warning is triggered. */ public function saveAs($file,$deleteTempFile=true) { diff --git a/framework/web/CUrlManager.php b/framework/web/CUrlManager.php index 688a3ab0dd9..2918d0a6375 100644 --- a/framework/web/CUrlManager.php +++ b/framework/web/CUrlManager.php @@ -637,6 +637,14 @@ class CUrlRule extends CBaseUrlRule */ public $hasHostInfo; + /** + * Callback for preg_replace_callback in counstructor + */ + protected function escapeRegexpSpecialChars($matches) + { + return preg_quote($matches[0]); + } + /** * Constructor. * @param string $route the route of the URL (controller/action) @@ -658,7 +666,6 @@ public function __construct($route,$pattern) $this->route=trim($route,'/'); $tr2['/']=$tr['/']='\\/'; - $tr['.']='\\.'; if(strpos($route,'<')!==false && preg_match_all('/<(\w+)>/',$route,$matches2)) { @@ -689,7 +696,10 @@ public function __construct($route,$pattern) $this->append=$p!==$pattern; $p=trim($p,'/'); $this->template=preg_replace('/<(\w+):?.*?>/','<$1>',$p); - $this->pattern='/^'.strtr($this->template,$tr).'\/'; + $p=$this->template; + if(!$this->parsingOnly) + $p=preg_replace_callback('/(?<=^|>)[^<]+(?=<|$)/',array($this,'escapeRegexpSpecialChars'),$p); + $this->pattern='/^'.strtr($p,$tr).'\/'; if($this->append) $this->pattern.='/u'; else diff --git a/framework/web/CWebApplication.php b/framework/web/CWebApplication.php index 8d63a12b70f..2debb4aabd1 100644 --- a/framework/web/CWebApplication.php +++ b/framework/web/CWebApplication.php @@ -311,7 +311,7 @@ public function createController($route,$owner=null) { if($owner===null) $owner=$this; - if(($route=trim($route,'/'))==='') + if((array)$route===$route || ($route=trim($route,'/'))==='') $route=$owner->defaultController; $caseSensitive=$this->getUrlManager()->caseSensitive; diff --git a/framework/web/CWebModule.php b/framework/web/CWebModule.php index aeabf88f562..3289c89a63c 100644 --- a/framework/web/CWebModule.php +++ b/framework/web/CWebModule.php @@ -95,7 +95,7 @@ public function getVersion() /** * @return string the directory that contains the controller classes. Defaults to 'moduleDir/controllers' where - * moduleDir is the directory containing the module class. + * moduleDir is the directory containing the module class. */ public function getControllerPath() { diff --git a/framework/web/auth/CAuthManager.php b/framework/web/auth/CAuthManager.php index 7c703757c05..a869e05c866 100644 --- a/framework/web/auth/CAuthManager.php +++ b/framework/web/auth/CAuthManager.php @@ -146,7 +146,21 @@ public function getOperations($userId=null) */ public function executeBizRule($bizRule,$params,$data) { - return $bizRule==='' || $bizRule===null || ($this->showErrors ? eval($bizRule)!=0 : @eval($bizRule)!=0); + if($bizRule==='' || $bizRule===null) + return true; + if ($this->showErrors) + return eval($bizRule)!=0; + else + { + try + { + return @eval($bizRule)!=0; + } + catch (ParseError $e) + { + return false; + } + } } /** diff --git a/framework/web/auth/CDbAuthManager.php b/framework/web/auth/CDbAuthManager.php index 0a254c41d34..a68a40a6942 100644 --- a/framework/web/auth/CDbAuthManager.php +++ b/framework/web/auth/CDbAuthManager.php @@ -94,7 +94,7 @@ protected function checkAccessRecursive($itemName,$userId,$params,$assignments) return false; Yii::trace('Checking permission "'.$item->getName().'"','system.web.auth.CDbAuthManager'); if(!isset($params['userId'])) - $params['userId'] = $userId; + $params['userId'] = $userId; if($this->executeBizRule($item->getBizRule(),$params,$item->getData())) { if(in_array($itemName,$this->defaultRoles)) diff --git a/framework/web/auth/CPhpAuthManager.php b/framework/web/auth/CPhpAuthManager.php index 3584f1dd7f5..96fb489ff01 100644 --- a/framework/web/auth/CPhpAuthManager.php +++ b/framework/web/auth/CPhpAuthManager.php @@ -70,7 +70,7 @@ public function checkAccess($itemName,$userId,$params=array()) $item=$this->_items[$itemName]; Yii::trace('Checking permission "'.$item->getName().'"','system.web.auth.CPhpAuthManager'); if(!isset($params['userId'])) - $params['userId'] = $userId; + $params['userId'] = $userId; if($this->executeBizRule($item->getBizRule(),$params,$item->getData())) { if(in_array($itemName,$this->defaultRoles)) diff --git a/framework/web/form/CForm.php b/framework/web/form/CForm.php index 29372a938d9..d62667d5431 100644 --- a/framework/web/form/CForm.php +++ b/framework/web/form/CForm.php @@ -435,7 +435,7 @@ public function renderBegin() $options['htmlOptions']=$this->attributes; ob_start(); $this->_activeForm=$this->getOwner()->beginWidget($class, $options); - return ob_get_clean() . "
              ".CHtml::hiddenField($this->getUniqueID(),1)."
              \n"; + return ob_get_clean() . "
              ".CHtml::hiddenField($this->getUniqueID(),1)."
              \n"; } } @@ -538,7 +538,7 @@ public function renderElement($element) if($element instanceof CFormInputElement) { if($element->type==='hidden') - return "
              \n".$element->render()."
              \n"; + return "
              \n".$element->render()."
              \n"; else return "
              name}\">\n".$element->render()."
              \n"; } diff --git a/framework/web/helpers/CHtml.php b/framework/web/helpers/CHtml.php index c8e55853c4e..30503842b21 100644 --- a/framework/web/helpers/CHtml.php +++ b/framework/web/helpers/CHtml.php @@ -1510,7 +1510,8 @@ public static function activeLabelEx($model,$attribute,$htmlOptions=array()) { $realAttribute=$attribute; self::resolveName($model,$attribute); // strip off square brackets if any - $htmlOptions['required']=$model->isAttributeRequired($attribute); + if (!isset($htmlOptions['required'])) + $htmlOptions['required']=$model->isAttributeRequired($attribute); return self::activeLabel($model,$realAttribute,$htmlOptions); } diff --git a/framework/web/js/source/jquery.history.js b/framework/web/js/source/jquery.history.js index 628d41d1874..d16813e34f4 100644 --- a/framework/web/js/source/jquery.history.js +++ b/framework/web/js/source/jquery.history.js @@ -1 +1 @@ -(function(a,b){"use strict";var c=a.History=a.History||{},d=a.jQuery;if(typeof c.Adapter!="undefined")throw new Error("History.js Adapter has already been loaded...");c.Adapter={bind:function(a,b,c){d(a).bind(b,c)},trigger:function(a,b,c){d(a).trigger(b,c)},extractEventData:function(a,c,d){var e=c&&c.originalEvent&&c.originalEvent[a]||d&&d[a]||b;return e},onDomLoad:function(a){d(a)}},typeof c.init!="undefined"&&c.init()})(window),function(a,b){"use strict";var c=a.console||b,d=a.document,e=a.navigator,f=a.sessionStorage||!1,g=a.setTimeout,h=a.clearTimeout,i=a.setInterval,j=a.clearInterval,k=a.JSON,l=a.alert,m=a.History=a.History||{},n=a.history;k.stringify=k.stringify||k.encode,k.parse=k.parse||k.decode;if(typeof m.init!="undefined")throw new Error("History.js Core has already been loaded...");m.init=function(){return typeof m.Adapter=="undefined"?!1:(typeof m.initCore!="undefined"&&m.initCore(),typeof m.initHtml4!="undefined"&&m.initHtml4(),!0)},m.initCore=function(){if(typeof m.initCore.initialized!="undefined")return!1;m.initCore.initialized=!0,m.options=m.options||{},m.options.hashChangeInterval=m.options.hashChangeInterval||100,m.options.safariPollInterval=m.options.safariPollInterval||500,m.options.doubleCheckInterval=m.options.doubleCheckInterval||500,m.options.storeInterval=m.options.storeInterval||1e3,m.options.busyDelay=m.options.busyDelay||250,m.options.debug=m.options.debug||!1,m.options.initialTitle=m.options.initialTitle||d.title,m.intervalList=[],m.clearAllIntervals=function(){var a,b=m.intervalList;if(typeof b!="undefined"&&b!==null){for(a=0;a")&&c[0]);return a>4?a:!1}();return a},m.isInternetExplorer=function(){var a=m.isInternetExplorer.cached=typeof m.isInternetExplorer.cached!="undefined"?m.isInternetExplorer.cached:Boolean(m.getInternetExplorerMajorVersion());return a},m.emulated={pushState:!Boolean(a.history&&a.history.pushState&&a.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(e.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(e.userAgent)),hashChange:Boolean(!("onhashchange"in a||"onhashchange"in d)||m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<8)},m.enabled=!m.emulated.pushState,m.bugs={setHash:Boolean(!m.emulated.pushState&&e.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(e.userAgent)),safariPoll:Boolean(!m.emulated.pushState&&e.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(e.userAgent)),ieDoubleCheck:Boolean(m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<7)},m.isEmptyObject=function(a){for(var b in a)return!1;return!0},m.cloneObject=function(a){var b,c;return a?(b=k.stringify(a),c=k.parse(b)):c={},c},m.getRootUrl=function(){var a=d.location.protocol+"//"+(d.location.hostname||d.location.host);if(d.location.port||!1)a+=":"+d.location.port;return a+="/",a},m.getBaseHref=function(){var a=d.getElementsByTagName("base"),b=null,c="";return a.length===1&&(b=a[0],c=b.href.replace(/[^\/]+$/,"")),c=c.replace(/\/+$/,""),c&&(c+="/"),c},m.getBaseUrl=function(){var a=m.getBaseHref()||m.getBasePageUrl()||m.getRootUrl();return a},m.getPageUrl=function(){var a=m.getState(!1,!1),b=(a||{}).url||d.location.href,c;return c=b.replace(/\/+$/,"").replace(/[^\/]+$/,function(a,b,c){return/\./.test(a)?a:a+"/"}),c},m.getBasePageUrl=function(){var a=d.location.href.replace(/[#\?].*/,"").replace(/[^\/]+$/,function(a,b,c){return/[^\/]$/.test(a)?"":a}).replace(/\/+$/,"")+"/";return a},m.getFullUrl=function(a,b){var c=a,d=a.substring(0,1);return b=typeof b=="undefined"?!0:b,/[a-z]+\:\/\//.test(a)||(d==="/"?c=m.getRootUrl()+a.replace(/^\/+/,""):d==="#"?c=m.getPageUrl().replace(/#.*/,"")+a:d==="?"?c=m.getPageUrl().replace(/[\?#].*/,"")+a:b?c=m.getBaseUrl()+a.replace(/^(\.\/)+/,""):c=m.getBasePageUrl()+a.replace(/^(\.\/)+/,"")),c.replace(/\#$/,"")},m.getShortUrl=function(a){var b=a,c=m.getBaseUrl(),d=m.getRootUrl();return m.emulated.pushState&&(b=b.replace(c,"")),b=b.replace(d,"/"),m.isTraditionalAnchor(b)&&(b="./"+b),b=b.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),b},m.store={},m.idToState=m.idToState||{},m.stateToId=m.stateToId||{},m.urlToId=m.urlToId||{},m.storedStates=m.storedStates||[],m.savedStates=m.savedStates||[],m.normalizeStore=function(){m.store.idToState=m.store.idToState||{},m.store.urlToId=m.store.urlToId||{},m.store.stateToId=m.store.stateToId||{}},m.getState=function(a,b){typeof a=="undefined"&&(a=!0),typeof b=="undefined"&&(b=!0);var c=m.getLastSavedState();return!c&&b&&(c=m.createStateObject()),a&&(c=m.cloneObject(c),c.url=c.cleanUrl||c.url),c},m.getIdByState=function(a){var b=m.extractId(a.url),c;if(!b){c=m.getStateString(a);if(typeof m.stateToId[c]!="undefined")b=m.stateToId[c];else if(typeof m.store.stateToId[c]!="undefined")b=m.store.stateToId[c];else{for(;;){b=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof m.idToState[b]=="undefined"&&typeof m.store.idToState[b]=="undefined")break}m.stateToId[c]=b,m.idToState[b]=a}}return b},m.normalizeState=function(a){var b,c;if(!a||typeof a!="object")a={};if(typeof a.normalized!="undefined")return a;if(!a.data||typeof a.data!="object")a.data={};b={},b.normalized=!0,b.title=a.title||"",b.url=m.getFullUrl(m.unescapeString(a.url||d.location.href)),b.hash=m.getShortUrl(b.url),b.data=m.cloneObject(a.data),b.id=m.getIdByState(b),b.cleanUrl=b.url.replace(/\??\&_suid.*/,""),b.url=b.cleanUrl,c=!m.isEmptyObject(b.data);if(b.title||c)b.hash=m.getShortUrl(b.url).replace(/\??\&_suid.*/,""),/\?/.test(b.hash)||(b.hash+="?"),b.hash+="&_suid="+b.id;return b.hashedUrl=m.getFullUrl(b.hash),(m.emulated.pushState||m.bugs.safariPoll)&&m.hasUrlDuplicate(b)&&(b.url=b.hashedUrl),b},m.createStateObject=function(a,b,c){var d={data:a,title:b,url:c};return d=m.normalizeState(d),d},m.getStateById=function(a){a=String(a);var c=m.idToState[a]||m.store.idToState[a]||b;return c},m.getStateString=function(a){var b,c,d;return b=m.normalizeState(a),c={data:b.data,title:a.title,url:a.url},d=k.stringify(c),d},m.getStateId=function(a){var b,c;return b=m.normalizeState(a),c=b.id,c},m.getHashByState=function(a){var b,c;return b=m.normalizeState(a),c=b.hash,c},m.extractId=function(a){var b,c,d;return c=/(.*)\&_suid=([0-9]+)$/.exec(a),d=c?c[1]||a:a,b=c?String(c[2]||""):"",b||!1},m.isTraditionalAnchor=function(a){var b=!/[\/\?\.]/.test(a);return b},m.extractState=function(a,b){var c=null,d,e;return b=b||!1,d=m.extractId(a),d&&(c=m.getStateById(d)),c||(e=m.getFullUrl(a),d=m.getIdByUrl(e)||!1,d&&(c=m.getStateById(d)),!c&&b&&!m.isTraditionalAnchor(a)&&(c=m.createStateObject(null,null,e))),c},m.getIdByUrl=function(a){var c=m.urlToId[a]||m.store.urlToId[a]||b;return c},m.getLastSavedState=function(){return m.savedStates[m.savedStates.length-1]||b},m.getLastStoredState=function(){return m.storedStates[m.storedStates.length-1]||b},m.hasUrlDuplicate=function(a){var b=!1,c;return c=m.extractState(a.url),b=c&&c.id!==a.id,b},m.storeState=function(a){return m.urlToId[a.url]=a.id,m.storedStates.push(m.cloneObject(a)),a},m.isLastSavedState=function(a){var b=!1,c,d,e;return m.savedStates.length&&(c=a.id,d=m.getLastSavedState(),e=d.id,b=c===e),b},m.saveState=function(a){return m.isLastSavedState(a)?!1:(m.savedStates.push(m.cloneObject(a)),!0)},m.getStateByIndex=function(a){var b=null;return typeof a=="undefined"?b=m.savedStates[m.savedStates.length-1]:a<0?b=m.savedStates[m.savedStates.length+a]:b=m.savedStates[a],b},m.getHash=function(){var a=m.unescapeHash(d.location.hash);return a},m.unescapeString=function(b){var c=b,d;for(;;){d=a.unescape(c);if(d===c)break;c=d}return c},m.unescapeHash=function(a){var b=m.normalizeHash(a);return b=m.unescapeString(b),b},m.normalizeHash=function(a){var b=a.replace(/[^#]*#/,"").replace(/#.*/,"");return b},m.setHash=function(a,b){var c,e,f;return b!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.setHash,args:arguments,queue:b}),!1):(c=m.escapeHash(a),m.busy(!0),e=m.extractState(a,!0),e&&!m.emulated.pushState?m.pushState(e.data,e.title,e.url,!1):d.location.hash!==c&&(m.bugs.setHash?(f=m.getPageUrl(),m.pushState(null,null,f+"#"+c,!1)):d.location.hash=c),m)},m.escapeHash=function(b){var c=m.normalizeHash(b);return c=a.escape(c),m.bugs.hashEscape||(c=c.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),c},m.getHashByUrl=function(a){var b=String(a).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return b=m.unescapeHash(b),b},m.setTitle=function(a){var b=a.title,c;b||(c=m.getStateByIndex(0),c&&c.url===a.url&&(b=c.title||m.options.initialTitle));try{d.getElementsByTagName("title")[0].innerHTML=b.replace("<","<").replace(">",">").replace(" & "," & ")}catch(e){}return d.title=b,m},m.queues=[],m.busy=function(a){typeof a!="undefined"?m.busy.flag=a:typeof m.busy.flag=="undefined"&&(m.busy.flag=!1);if(!m.busy.flag){h(m.busy.timeout);var b=function(){var a,c,d;if(m.busy.flag)return;for(a=m.queues.length-1;a>=0;--a){c=m.queues[a];if(c.length===0)continue;d=c.shift(),m.fireQueueItem(d),m.busy.timeout=g(b,m.options.busyDelay)}};m.busy.timeout=g(b,m.options.busyDelay)}return m.busy.flag},m.busy.flag=!1,m.fireQueueItem=function(a){return a.callback.apply(a.scope||m,a.args||[])},m.pushQueue=function(a){return m.queues[a.queue||0]=m.queues[a.queue||0]||[],m.queues[a.queue||0].push(a),m},m.queue=function(a,b){return typeof a=="function"&&(a={callback:a}),typeof b!="undefined"&&(a.queue=b),m.busy()?m.pushQueue(a):m.fireQueueItem(a),m},m.clearQueue=function(){return m.busy.flag=!1,m.queues=[],m},m.stateChanged=!1,m.doubleChecker=!1,m.doubleCheckComplete=function(){return m.stateChanged=!0,m.doubleCheckClear(),m},m.doubleCheckClear=function(){return m.doubleChecker&&(h(m.doubleChecker),m.doubleChecker=!1),m},m.doubleCheck=function(a){return m.stateChanged=!1,m.doubleCheckClear(),m.bugs.ieDoubleCheck&&(m.doubleChecker=g(function(){return m.doubleCheckClear(),m.stateChanged||a(),!0},m.options.doubleCheckInterval)),m},m.safariStatePoll=function(){var b=m.extractState(d.location.href),c;if(!m.isLastSavedState(b))c=b;else return;return c||(c=m.createStateObject()),m.Adapter.trigger(a,"popstate"),m},m.back=function(a){return a!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.back,args:arguments,queue:a}),!1):(m.busy(!0),m.doubleCheck(function(){m.back(!1)}),n.go(-1),!0)},m.forward=function(a){return a!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.forward,args:arguments,queue:a}),!1):(m.busy(!0),m.doubleCheck(function(){m.forward(!1)}),n.go(1),!0)},m.go=function(a,b){var c;if(a>0)for(c=1;c<=a;++c)m.forward(b);else{if(!(a<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(c=-1;c>=a;--c)m.back(b)}return m};if(m.emulated.pushState){var o=function(){};m.pushState=m.pushState||o,m.replaceState=m.replaceState||o}else m.onPopState=function(b,c){var e=!1,f=!1,g,h;return m.doubleCheckComplete(),g=m.getHash(),g?(h=m.extractState(g||d.location.href,!0),h?m.replaceState(h.data,h.title,h.url,!1):(m.Adapter.trigger(a,"anchorchange"),m.busy(!1)),m.expectedStateId=!1,!1):(e=m.Adapter.extractEventData("state",b,c)||!1,e?f=m.getStateById(e):m.expectedStateId?f=m.getStateById(m.expectedStateId):f=m.extractState(d.location.href),f||(f=m.createStateObject(null,null,d.location.href)),m.expectedStateId=!1,m.isLastSavedState(f)?(m.busy(!1),!1):(m.storeState(f),m.saveState(f),m.setTitle(f),m.Adapter.trigger(a,"statechange"),m.busy(!1),!0))},m.Adapter.bind(a,"popstate",m.onPopState),m.pushState=function(b,c,d,e){if(m.getHashByUrl(d)&&m.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(e!==!1&&m.busy())return m.pushQueue({scope:m,callback:m.pushState,args:arguments,queue:e}),!1;m.busy(!0);var f=m.createStateObject(b,c,d);return m.isLastSavedState(f)?m.busy(!1):(m.storeState(f),m.expectedStateId=f.id,n.pushState(f.id,f.title,f.url),m.Adapter.trigger(a,"popstate")),!0},m.replaceState=function(b,c,d,e){if(m.getHashByUrl(d)&&m.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(e!==!1&&m.busy())return m.pushQueue({scope:m,callback:m.replaceState,args:arguments,queue:e}),!1;m.busy(!0);var f=m.createStateObject(b,c,d);return m.isLastSavedState(f)?m.busy(!1):(m.storeState(f),m.expectedStateId=f.id,n.replaceState(f.id,f.title,f.url),m.Adapter.trigger(a,"popstate")),!0};if(f){try{m.store=k.parse(f.getItem("History.store"))||{}}catch(p){m.store={}}m.normalizeStore()}else m.store={},m.normalizeStore();m.Adapter.bind(a,"beforeunload",m.clearAllIntervals),m.Adapter.bind(a,"unload",m.clearAllIntervals),m.saveState(m.storeState(m.extractState(d.location.href,!0))),f&&(m.onUnload=function(){var a,b;try{a=k.parse(f.getItem("History.store"))||{}}catch(c){a={}}a.idToState=a.idToState||{},a.urlToId=a.urlToId||{},a.stateToId=a.stateToId||{};for(b in m.idToState){if(!m.idToState.hasOwnProperty(b))continue;a.idToState[b]=m.idToState[b]}for(b in m.urlToId){if(!m.urlToId.hasOwnProperty(b))continue;a.urlToId[b]=m.urlToId[b]}for(b in m.stateToId){if(!m.stateToId.hasOwnProperty(b))continue;a.stateToId[b]=m.stateToId[b]}m.store=a,m.normalizeStore(),f.setItem("History.store",k.stringify(a))},m.intervalList.push(i(m.onUnload,m.options.storeInterval)),m.Adapter.bind(a,"beforeunload",m.onUnload),m.Adapter.bind(a,"unload",m.onUnload));if(!m.emulated.pushState){m.bugs.safariPoll&&m.intervalList.push(i(m.safariStatePoll,m.options.safariPollInterval));if(e.vendor==="Apple Computer, Inc."||(e.appCodeName||"")==="Mozilla")m.Adapter.bind(a,"hashchange",function(){m.Adapter.trigger(a,"popstate")}),m.getHash()&&m.Adapter.onDomLoad(function(){m.Adapter.trigger(a,"hashchange")})}},m.init()}(window) \ No newline at end of file +(function(e,t){"use strict";var n=e.History=e.History||{},r=e.jQuery;if(typeof n.Adapter!="undefined")throw new Error("History.js Adapter has already been loaded...");n.Adapter={bind:function(e,t,n){r(e).bind(t,n)},trigger:function(e,t,n){r(e).trigger(t,n)},extractEventData:function(e,n,r){var i=n&&n.originalEvent&&n.originalEvent[e]||r&&r[e]||t;return i},onDomLoad:function(e){r(e)}},typeof n.init!="undefined"&&n.init()})(window),function(e,t){"use strict";var n=e.console||t,r=e.document,i=e.navigator,s=!1,o=e.setTimeout,u=e.clearTimeout,a=e.setInterval,f=e.clearInterval,l=e.JSON,c=e.alert,h=e.History=e.History||{},p=e.history;try{s=e.sessionStorage,s.setItem("TEST","1"),s.removeItem("TEST")}catch(d){s=!1}l.stringify=l.stringify||l.encode,l.parse=l.parse||l.decode;if(typeof h.init!="undefined")throw new Error("History.js Core has already been loaded...");h.init=function(e){return typeof h.Adapter=="undefined"?!1:(typeof h.initCore!="undefined"&&h.initCore(),typeof h.initHtml4!="undefined"&&h.initHtml4(),!0)},h.initCore=function(d){if(typeof h.initCore.initialized!="undefined")return!1;h.initCore.initialized=!0,h.options=h.options||{},h.options.hashChangeInterval=h.options.hashChangeInterval||100,h.options.safariPollInterval=h.options.safariPollInterval||500,h.options.doubleCheckInterval=h.options.doubleCheckInterval||500,h.options.disableSuid=h.options.disableSuid||!1,h.options.storeInterval=h.options.storeInterval||1e3,h.options.busyDelay=h.options.busyDelay||250,h.options.debug=h.options.debug||!1,h.options.initialTitle=h.options.initialTitle||r.title,h.options.html4Mode=h.options.html4Mode||!1,h.options.delayInit=h.options.delayInit||!1,h.intervalList=[],h.clearAllIntervals=function(){var e,t=h.intervalList;if(typeof t!="undefined"&&t!==null){for(e=0;e")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window) \ No newline at end of file diff --git a/framework/web/renderers/CPradoViewRenderer.php b/framework/web/renderers/CPradoViewRenderer.php index 14afd4100f9..68bf7c47ce1 100644 --- a/framework/web/renderers/CPradoViewRenderer.php +++ b/framework/web/renderers/CPradoViewRenderer.php @@ -88,7 +88,7 @@ protected function generateViewFile($sourceFile,$viewFile) $this->_input=file_get_contents($sourceFile); $n=preg_match_all('/'.implode('|',$regexRules).'/msS',$this->_input,$matches,PREG_SET_ORDER|PREG_OFFSET_CAPTURE); $textStart=0; - $this->_output="\n"; + $this->_output="\n"; for($i=0;$i<$n;++$i) { $match=&$matches[$i]; diff --git a/framework/web/services/CWebService.php b/framework/web/services/CWebService.php index 66e99f76151..eddc7744071 100644 --- a/framework/web/services/CWebService.php +++ b/framework/web/services/CWebService.php @@ -236,7 +236,9 @@ public function getMethodName() { if($this->_method===null) { - if(isset($HTTP_RAW_POST_DATA)) + // before PHP 5.6 php://input could be read only once + // since PHP 5.6 $HTTP_RAW_POST_DATA is deprecated + if(version_compare(PHP_VERSION, '5.6.0', '<') && isset($HTTP_RAW_POST_DATA)) $request=$HTTP_RAW_POST_DATA; else $request=file_get_contents('php://input'); diff --git a/framework/web/services/CWsdlGenerator.php b/framework/web/services/CWsdlGenerator.php index 6662f5e1503..ee717441e79 100644 --- a/framework/web/services/CWsdlGenerator.php +++ b/framework/web/services/CWsdlGenerator.php @@ -554,7 +554,7 @@ protected function addTypes($dom) $restriction->setAttribute('base','soap-enc:Array'); $attribute=$dom->createElement('xsd:attribute'); $attribute->setAttribute('ref','soap-enc:arrayType'); - $attribute->setAttribute('arrayType',(isset(self::$typeMap[$arrayType]) ? 'xsd:' : 'tns:') .$arrayType.'[]'); + $attribute->setAttribute('wsdl:arrayType',(isset(self::$typeMap[$arrayType]) ? 'xsd:' : 'tns:') .$arrayType.'[]'); $restriction->appendChild($attribute); $complexContent->appendChild($restriction); diff --git a/framework/web/widgets/CContentDecorator.php b/framework/web/widgets/CContentDecorator.php index 0087698d732..4f749be54f5 100644 --- a/framework/web/widgets/CContentDecorator.php +++ b/framework/web/widgets/CContentDecorator.php @@ -46,7 +46,7 @@ class CContentDecorator extends COutputProcessor /** * Processes the captured output. - * This method decorates the output with the specified {@link view}. + * This method decorates the output with the specified {@link view}. * @param string $output the captured output to be processed */ public function processOutput($output) diff --git a/framework/web/widgets/CMarkdown.php b/framework/web/widgets/CMarkdown.php index 76c5fdbf76d..d609618657e 100644 --- a/framework/web/widgets/CMarkdown.php +++ b/framework/web/widgets/CMarkdown.php @@ -46,8 +46,8 @@ class CMarkdown extends COutputProcessor /** * Processes the captured output. - * This method converts the content in markdown syntax to HTML code. - * If {@link purifyOutput} is true, the HTML code will also be purified. + * This method converts the content in markdown syntax to HTML code. + * If {@link purifyOutput} is true, the HTML code will also be purified. * @param string $output the captured output to be processed * @see convert */ diff --git a/framework/web/widgets/CTextHighlighter.php b/framework/web/widgets/CTextHighlighter.php index c4dcb08c08d..0cec493d9e2 100644 --- a/framework/web/widgets/CTextHighlighter.php +++ b/framework/web/widgets/CTextHighlighter.php @@ -67,7 +67,7 @@ class CTextHighlighter extends COutputProcessor /** * Processes the captured output. - * This method highlights the output according to the syntax of the specified {@link language}. + * This method highlights the output according to the syntax of the specified {@link language}. * @param string $output the captured output to be processed */ public function processOutput($output) diff --git a/framework/yii.php b/framework/yii.php index 5ca9704de47..d03becb237b 100644 --- a/framework/yii.php +++ b/framework/yii.php @@ -10,7 +10,8 @@ * @since 1.0 */ -require(dirname(__FILE__).'/YiiBase.php'); +if(!class_exists('YiiBase', false)) + require(dirname(__FILE__).'/YiiBase.php'); /** * Yii is a helper class serving common framework functionalities. diff --git a/framework/yiilite.php b/framework/yiilite.php index 553d571398f..19f5a2601a9 100644 --- a/framework/yiilite.php +++ b/framework/yiilite.php @@ -40,7 +40,7 @@ class YiiBase private static $_logger; public static function getVersion() { - return '1.1.16'; + return '1.1.17'; } public static function createWebApplication($config=null) { @@ -249,7 +249,7 @@ public static function autoload($className,$classMapOnly=false) else // class name with namespace in PHP 5.3 { $namespace=str_replace('\\','.',ltrim($className,'\\')); - if(($path=self::getPathOfAlias($namespace))!==false) + if(($path=self::getPathOfAlias($namespace))!==false && is_file($path.'.php')) include($path.'.php'); else return false; @@ -361,6 +361,7 @@ public static function registerAutoloader($callback, $append=false) 'CApplicationComponent' => '/base/CApplicationComponent.php', 'CBehavior' => '/base/CBehavior.php', 'CComponent' => '/base/CComponent.php', + 'CDbStatePersister' => '/base/CDbStatePersister.php', 'CErrorEvent' => '/base/CErrorEvent.php', 'CErrorHandler' => '/base/CErrorHandler.php', 'CException' => '/base/CException.php', @@ -584,6 +585,8 @@ public static function registerAutoloader($callback, $append=false) ); } spl_autoload_register(array('YiiBase','autoload')); +if(!class_exists('YiiBase', false)) + require(dirname(__FILE__).'/YiiBase.php'); class Yii extends YiiBase { } @@ -1358,6 +1361,10 @@ public function getUrlManager() { return $this->getComponent('urlManager'); } + public function getFormat() + { + return $this->getComponent('format'); + } public function getController() { return null; @@ -1769,7 +1776,7 @@ public function createController($route,$owner=null) { if($owner===null) $owner=$this; - if(($route=trim($route,'/'))==='') + if((array)$route===$route || ($route=trim($route,'/'))==='') $route=$owner->defaultController; $caseSensitive=$this->getUrlManager()->caseSensitive; $route.='/'; @@ -2262,6 +2269,7 @@ public function getIsInitialized() } class CHttpRequest extends CApplicationComponent { + public $jsonAsArray = true; public $enableCookieValidation=false; public $enableCsrfValidation=false; public $csrfTokenName='YII_CSRF_TOKEN'; @@ -2366,7 +2374,9 @@ public function getRestParams() if($this->_restParams===null) { $result=array(); - if(function_exists('mb_parse_str')) + if (strncmp($this->getContentType(), 'application/json', 16) === 0) + $result = CJSON::decode($this->getRawBody(), $this->jsonAsArray); + elseif(function_exists('mb_parse_str')) mb_parse_str($this->getRawBody(), $result); else parse_str($this->getRawBody(), $result); @@ -2475,9 +2485,9 @@ public function getPathInfo() $pathInfo=substr($_SERVER['PHP_SELF'],strlen($scriptUrl)); else throw new CException(Yii::t('yii','CHttpRequest is unable to determine the path info of the request.')); - if($pathInfo==='/') + if($pathInfo==='/' || $pathInfo===false) $pathInfo=''; - elseif($pathInfo[0]==='/') + elseif($pathInfo!=='' && $pathInfo[0]==='/') $pathInfo=substr($pathInfo,1); if(($posEnd=strlen($pathInfo)-1)>0 && $pathInfo[$posEnd]==='/') $pathInfo=substr($pathInfo,0,$posEnd); @@ -2549,8 +2559,8 @@ public function getRequestType() { if(isset($_POST['_method'])) return strtoupper($_POST['_method']); - elseif(isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) - return strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']); + elseif(isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) + return strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']); return strtoupper(isset($_SERVER['REQUEST_METHOD'])?$_SERVER['REQUEST_METHOD']:'GET'); } public function getIsPostRequest() @@ -2628,6 +2638,16 @@ public function getAcceptTypes() { return isset($_SERVER['HTTP_ACCEPT'])?$_SERVER['HTTP_ACCEPT']:null; } + public function getContentType() + { + if (isset($_SERVER["CONTENT_TYPE"])) { + return $_SERVER["CONTENT_TYPE"]; + } elseif (isset($_SERVER["HTTP_CONTENT_TYPE"])) { + //fix bug https://bugs.php.net/bug.php?id=66606 + return $_SERVER["HTTP_CONTENT_TYPE"]; + } + return null; + } private $_port; public function getPort() { @@ -2791,7 +2811,7 @@ public function getPreferredLanguage($languages=array()) foreach ($languages as $language) { $language=CLocale::getCanonicalID($language); // en_us==en_us, en==en_us, en_us==en - if($language===$acceptedLanguage || strpos($acceptedLanguage,$language.'_')===0 || strpos($language,$acceptedLanguage.'_')===0) { + if($language===$preferredLanguage || strpos($preferredLanguage,$language.'_')===0 || strpos($language,$preferredLanguage.'_')===0) { return $language; } } @@ -2855,7 +2875,7 @@ public function sendFile($fileName,$content,$mimeType=null,$terminate=true) header('Content-Length: '.$length); header("Content-Disposition: attachment; filename=\"$fileName\""); header('Content-Transfer-Encoding: binary'); - $content=function_exists('mb_substr') ? mb_substr($content,$contentStart,$length) : substr($content,$contentStart,$length); + $content=function_exists('mb_substr') ? mb_substr($content,$contentStart,$length,'8bit') : substr($content,$contentStart,$length); if($terminate) { // clean up the application first because the file downloading could take long time @@ -3295,6 +3315,10 @@ class CUrlRule extends CBaseUrlRule public $params=array(); public $append; public $hasHostInfo; + protected function escapeRegexpSpecialChars($matches) + { + return preg_quote($matches[0]); + } public function __construct($route,$pattern) { if(is_array($route)) @@ -3310,7 +3334,6 @@ public function __construct($route,$pattern) } $this->route=trim($route,'/'); $tr2['/']=$tr['/']='\\/'; - $tr['.']='\\.'; if(strpos($route,'<')!==false && preg_match_all('/<(\w+)>/',$route,$matches2)) { foreach($matches2[1] as $name) @@ -3337,7 +3360,10 @@ public function __construct($route,$pattern) $this->append=$p!==$pattern; $p=trim($p,'/'); $this->template=preg_replace('/<(\w+):?.*?>/','<$1>',$p); - $this->pattern='/^'.strtr($this->template,$tr).'\/'; + $p=$this->template; + if(!$this->parsingOnly) + $p=preg_replace_callback('/(?<=^|>)[^<]+(?=<|$)/',array($this,'escapeRegexpSpecialChars'),$p); + $this->pattern='/^'.strtr($p,$tr).'\/'; if($this->append) $this->pattern.='/u'; else @@ -5429,7 +5455,8 @@ public static function activeLabelEx($model,$attribute,$htmlOptions=array()) { $realAttribute=$attribute; self::resolveName($model,$attribute); // strip off square brackets if any - $htmlOptions['required']=$model->isAttributeRequired($attribute); + if (!isset($htmlOptions['required'])) + $htmlOptions['required']=$model->isAttributeRequired($attribute); return self::activeLabel($model,$realAttribute,$htmlOptions); } public static function activeTextField($model,$attribute,$htmlOptions=array()) @@ -8278,8 +8305,10 @@ public function mergeWith($criteria,$fromScope=false) $criteria=$criteria->toArray(); if(isset($criteria['select']) && $this->select!==$criteria['select']) { - if($this->select==='*') + if($this->select==='*'||$this->select===false) $this->select=$criteria['select']; + elseif($criteria['select']===false) + $this->select=false; elseif($criteria['select']!=='*') { $select1=is_string($this->select)?preg_split('/\s*,\s*/',trim($this->select),-1,PREG_SPLIT_NO_EMPTY):$this->select; @@ -9092,21 +9121,21 @@ public function dropPrimaryKey($name,$table) } class CSqliteSchema extends CDbSchema { - public $columnTypes=array( - 'pk' => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', - 'bigpk' => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', - 'string' => 'varchar(255)', - 'text' => 'text', - 'integer' => 'integer', - 'bigint' => 'integer', - 'float' => 'float', - 'decimal' => 'decimal', - 'datetime' => 'datetime', - 'timestamp' => 'timestamp', - 'time' => 'time', - 'date' => 'date', - 'binary' => 'blob', - 'boolean' => 'tinyint(1)', + public $columnTypes=array( + 'pk' => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', + 'bigpk' => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', + 'string' => 'varchar(255)', + 'text' => 'text', + 'integer' => 'integer', + 'bigint' => 'integer', + 'float' => 'float', + 'decimal' => 'decimal', + 'datetime' => 'datetime', + 'timestamp' => 'timestamp', + 'time' => 'time', + 'date' => 'date', + 'binary' => 'blob', + 'boolean' => 'tinyint(1)', 'money' => 'decimal(19,4)', ); public function resetSequence($table,$value=null) @@ -9594,7 +9623,7 @@ public function from($tables) { if(strpos($table,'(')===false) { - if(preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/',$table,$matches)) // with alias + if(preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/',$table,$matches)) // with alias $tables[$i]=$this->_connection->quoteTableName($matches[1]).' '.$this->_connection->quoteTableName($matches[2]); else $tables[$i]=$this->_connection->quoteTableName($table); @@ -9968,7 +9997,7 @@ private function joinInternal($type, $table, $conditions='', $params=array()) { if(strpos($table,'(')===false) { - if(preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/',$table,$matches)) // with alias + if(preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/',$table,$matches)) // with alias $table=$this->_connection->quoteTableName($matches[1]).' '.$this->_connection->quoteTableName($matches[2]); else $table=$this->_connection->quoteTableName($table); @@ -10102,7 +10131,7 @@ abstract protected function validateAttribute($object,$attribute); public static function createValidator($name,$object,$attributes,$params=array()) { if(is_string($attributes)) - $attributes=preg_split('/\s*,\s*/',$attributes,-1,PREG_SPLIT_NO_EMPTY); + $attributes=preg_split('/\s*,\s*/',trim($attributes, " \t\n\r\0\x0B,"),-1,PREG_SPLIT_NO_EMPTY); if(isset($params['on'])) { if(is_array($params['on'])) @@ -10444,12 +10473,10 @@ class CListIterator implements Iterator { private $_d; private $_i; - private $_c; public function __construct(&$data) { $this->_d=&$data; $this->_i=0; - $this->_c=count($this->_d); } public function rewind() { @@ -10469,7 +10496,7 @@ public function next() } public function valid() { - return $this->_i<$this->_c; + return $this->_i_d); } } interface IApplicationComponent diff --git a/framework/zii/behaviors/CTimestampBehavior.php b/framework/zii/behaviors/CTimestampBehavior.php index 212c6ecc773..52e774dd888 100644 --- a/framework/zii/behaviors/CTimestampBehavior.php +++ b/framework/zii/behaviors/CTimestampBehavior.php @@ -105,7 +105,16 @@ protected function getTimestampByAttribute($attribute) { if ($this->timestampExpression instanceof CDbExpression) return $this->timestampExpression; elseif ($this->timestampExpression !== null) - return @eval('return '.$this->timestampExpression.';'); + { + try + { + return @eval('return '.$this->timestampExpression.';'); + } + catch (ParseError $e) + { + return false; + } + } $columnType = $this->getOwner()->getTableSchema()->getColumn($attribute)->dbType; return $this->getTimestampByColumnType($columnType); diff --git a/framework/zii/widgets/CBreadcrumbs.php b/framework/zii/widgets/CBreadcrumbs.php index bef6b7a862e..ecfcf317e66 100644 --- a/framework/zii/widgets/CBreadcrumbs.php +++ b/framework/zii/widgets/CBreadcrumbs.php @@ -115,7 +115,7 @@ public function run() echo CHtml::openTag($this->tagName,$this->htmlOptions)."\n"; $links=array(); if($this->homeLink===null) - $definedLinks=array_merge(array(Yii::t('zii','Home') => Yii::app()->homeUrl),$definedLinks); + $definedLinks=array(Yii::t('zii','Home') => Yii::app()->homeUrl)+$definedLinks; elseif($this->homeLink!==false) $links[]=$this->homeLink; foreach($definedLinks as $label=>$url) diff --git a/framework/zii/widgets/assets/gridview/jquery.yiigridview.js b/framework/zii/widgets/assets/gridview/jquery.yiigridview.js index 8b9ffc37b7f..cd19df94f62 100644 --- a/framework/zii/widgets/assets/gridview/jquery.yiigridview.js +++ b/framework/zii/widgets/assets/gridview/jquery.yiigridview.js @@ -72,6 +72,7 @@ return this.each(function () { var eventType, + eventTarget, $grid = $(this), id = $grid.attr('id'), pagerSelector = '#' + id + ' .' + settings.pagerClass.replace(/\s+/g, '.') + ' a', @@ -99,7 +100,7 @@ delete params[settings.ajaxVar]; var updateUrl = $.param.querystring(url[0], params); - window.History.pushState({url: updateUrl}, document.title, decodeURIComponent(updateUrl)); + window.History.pushState({url: updateUrl}, document.title, updateUrl); } } else { $('#' + id).yiiGridView('update', {url: $(this).attr('href')}); @@ -114,11 +115,13 @@ return; // only react to enter key } else { eventType = 'keydown'; + eventTarget = event.target; } } else { - // prevent processing for both keydown and change events - if (eventType === 'keydown') { + // prevent processing for both keydown and change events on the same element + if (eventType === 'keydown' && eventTarget === event.target) { eventType = ''; + eventTarget = null; return; } } @@ -134,7 +137,7 @@ delete params[settings.ajaxVar]; var updateUrl = $.param.querystring(url.substr(0, url.indexOf('?')), params); - window.History.pushState({url: updateUrl}, document.title, decodeURIComponent(updateUrl)); + window.History.pushState({url: updateUrl}, document.title, updateUrl); } else { $('#' + id).yiiGridView('update', {data: data}); } @@ -324,7 +327,10 @@ } } else { if (options.data === undefined) { - options.data = $(settings.filterSelector).serialize(); + options.data = {}; + $.each($(settings.filterSelector).serializeArray(), function () { + options.data[this.name] = this.value; + }); } } if (settings.csrfTokenName && settings.csrfToken) { diff --git a/framework/zii/widgets/assets/listview/jquery.yiilistview.js b/framework/zii/widgets/assets/listview/jquery.yiilistview.js index 20e02467fc5..8fd5e1ee84c 100644 --- a/framework/zii/widgets/assets/listview/jquery.yiilistview.js +++ b/framework/zii/widgets/assets/listview/jquery.yiilistview.js @@ -43,7 +43,7 @@ delete params[settings.ajaxVar]; var updateUrl = $.param.querystring(url[0], params); - window.History.pushState({url: updateUrl}, document.title, decodeURIComponent(updateUrl)); + window.History.pushState({url: updateUrl}, document.title, updateUrl); } } else { $.fn.yiiListView.update(id, {url: $(this).attr('href')}); diff --git a/framework/zii/widgets/grid/CGridView.php b/framework/zii/widgets/grid/CGridView.php index eeb11b77f6b..53680e99edb 100644 --- a/framework/zii/widgets/grid/CGridView.php +++ b/framework/zii/widgets/grid/CGridView.php @@ -631,7 +631,7 @@ public function renderTableRow($row) * * @param CGridColumn $column The Column instance to * @param integer $row - * @since 1.1.17 + * @since 1.1.16 */ protected function renderDataCell($column, $row) { diff --git a/framework/zii/widgets/jui/CJuiDraggable.php b/framework/zii/widgets/jui/CJuiDraggable.php index 640c5c7b08f..72f6cf5396c 100644 --- a/framework/zii/widgets/jui/CJuiDraggable.php +++ b/framework/zii/widgets/jui/CJuiDraggable.php @@ -1,78 +1,78 @@ - - * @link http://www.yiiframework.com/ - * @copyright 2008-2013 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -Yii::import('zii.widgets.jui.CJuiWidget'); - -/** - * CJuiDraggable displays a draggable widget. - * - * CJuiDraggable encapsulates the {@link http://jqueryui.com/draggable/ JUI Draggable} - * plugin. - * - * To use this widget, you may insert the following code in a view: - *
              - * $this->beginWidget('zii.widgets.jui.CJuiDraggable',array(
              - *     // additional javascript options for the draggable plugin
              - *     'options'=>array(
              - *         'scope'=>'myScope',
              - *     ),
              - * ));
              - *     echo 'Your draggable content here';
              - *
              - * $this->endWidget();
              - *
              - * 
              - * - * By configuring the {@link options} property, you may specify the options - * that need to be passed to the JUI Draggable plugin. Please refer to - * the {@link http://api.jqueryui.com/draggable/ JUI Draggable API} documentation - * for possible options (name-value pairs) and - * {@link http://jqueryui.com/draggable/ JUI Draggable page} for general - * description and demo. - * - * @author Sebastian Thierer - * @package zii.widgets.jui - * @since 1.1 - */ -class CJuiDraggable extends CJuiWidget -{ - /** - * @var string the name of the Draggable element. Defaults to 'div'. - */ - public $tagName='div'; - - /** - * Renders the open tag of the draggable element. - * This method also registers the necessary javascript code. - */ - public function init() - { - parent::init(); - - $id=$this->getId(); - if(isset($this->htmlOptions['id'])) - $id=$this->htmlOptions['id']; - else - $this->htmlOptions['id']=$id; - - $options=CJavaScript::encode($this->options); - Yii::app()->getClientScript()->registerScript(__CLASS__.'#'.$id,"jQuery('#{$id}').draggable($options);"); - - echo CHtml::openTag($this->tagName,$this->htmlOptions)."\n"; - } - - /** - * Renders the close tag of the draggable element. - */ - public function run() - { - echo CHtml::closeTag($this->tagName); - } + + * @link http://www.yiiframework.com/ + * @copyright 2008-2013 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +Yii::import('zii.widgets.jui.CJuiWidget'); + +/** + * CJuiDraggable displays a draggable widget. + * + * CJuiDraggable encapsulates the {@link http://jqueryui.com/draggable/ JUI Draggable} + * plugin. + * + * To use this widget, you may insert the following code in a view: + *
              + * $this->beginWidget('zii.widgets.jui.CJuiDraggable',array(
              + *     // additional javascript options for the draggable plugin
              + *     'options'=>array(
              + *         'scope'=>'myScope',
              + *     ),
              + * ));
              + *     echo 'Your draggable content here';
              + *
              + * $this->endWidget();
              + *
              + * 
              + * + * By configuring the {@link options} property, you may specify the options + * that need to be passed to the JUI Draggable plugin. Please refer to + * the {@link http://api.jqueryui.com/draggable/ JUI Draggable API} documentation + * for possible options (name-value pairs) and + * {@link http://jqueryui.com/draggable/ JUI Draggable page} for general + * description and demo. + * + * @author Sebastian Thierer + * @package zii.widgets.jui + * @since 1.1 + */ +class CJuiDraggable extends CJuiWidget +{ + /** + * @var string the name of the Draggable element. Defaults to 'div'. + */ + public $tagName='div'; + + /** + * Renders the open tag of the draggable element. + * This method also registers the necessary javascript code. + */ + public function init() + { + parent::init(); + + $id=$this->getId(); + if(isset($this->htmlOptions['id'])) + $id=$this->htmlOptions['id']; + else + $this->htmlOptions['id']=$id; + + $options=CJavaScript::encode($this->options); + Yii::app()->getClientScript()->registerScript(__CLASS__.'#'.$id,"jQuery('#{$id}').draggable($options);"); + + echo CHtml::openTag($this->tagName,$this->htmlOptions)."\n"; + } + + /** + * Renders the close tag of the draggable element. + */ + public function run() + { + echo CHtml::closeTag($this->tagName); + } } \ No newline at end of file diff --git a/framework/zii/widgets/jui/CJuiDroppable.php b/framework/zii/widgets/jui/CJuiDroppable.php index cafb3fb46c1..b429d5f225e 100644 --- a/framework/zii/widgets/jui/CJuiDroppable.php +++ b/framework/zii/widgets/jui/CJuiDroppable.php @@ -1,78 +1,78 @@ - - * @link http://www.yiiframework.com/ - * @copyright 2008-2013 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -Yii::import('zii.widgets.jui.CJuiWidget'); - -/** - * CJuiDroppable displays a droppable widget. - * - * CJuiDroppable encapsulates the {@link http://jqueryui.com/droppable/ JUI Droppable} - * plugin. - * - * To use this widget, you may insert the following code in a view: - *
              - * $this->beginWidget('zii.widgets.jui.CJuiDroppable',array(
              - *     // additional javascript options for the droppable plugin
              - *     'options'=>array(
              - *         'scope'=>'myScope',
              - *     ),
              - * ));
              - *     echo 'Your droppable content here';
              - *
              - * $this->endWidget();
              - *
              - * 
              - * - * By configuring the {@link options} property, you may specify the options - * that need to be passed to the JUI Droppable plugin. Please refer to - * the {@link http://api.jqueryui.com/droppable/ JUI Droppable API} documentation - * for possible options (name-value pairs) and - * {@link http://jqueryui.com/droppable/ JUI Droppable page} for general - * description and demo. - * - * @author Sebastian Thierer - * @package zii.widgets.jui - * @since 1.1 - */ -class CJuiDroppable extends CJuiWidget -{ - /** - * @var string the HTML tag name of the Droppable element. Defaults to 'div'. - */ - public $tagName='div'; - - /** - * Renders the open tag of the droppable element. - * This method also registers the necessary javascript code. - */ - public function init() - { - parent::init(); - - $id=$this->getId(); - if(isset($this->htmlOptions['id'])) - $id=$this->htmlOptions['id']; - else - $this->htmlOptions['id']=$id; - - $options=CJavaScript::encode($this->options); - Yii::app()->getClientScript()->registerScript(__CLASS__.'#'.$id,"jQuery('#{$id}').droppable($options);"); - - echo CHtml::openTag($this->tagName,$this->htmlOptions)."\n"; - } - - /** - * Renders the close tag of the droppable element. - */ - public function run() - { - echo CHtml::closeTag($this->tagName); - } + + * @link http://www.yiiframework.com/ + * @copyright 2008-2013 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +Yii::import('zii.widgets.jui.CJuiWidget'); + +/** + * CJuiDroppable displays a droppable widget. + * + * CJuiDroppable encapsulates the {@link http://jqueryui.com/droppable/ JUI Droppable} + * plugin. + * + * To use this widget, you may insert the following code in a view: + *
              + * $this->beginWidget('zii.widgets.jui.CJuiDroppable',array(
              + *     // additional javascript options for the droppable plugin
              + *     'options'=>array(
              + *         'scope'=>'myScope',
              + *     ),
              + * ));
              + *     echo 'Your droppable content here';
              + *
              + * $this->endWidget();
              + *
              + * 
              + * + * By configuring the {@link options} property, you may specify the options + * that need to be passed to the JUI Droppable plugin. Please refer to + * the {@link http://api.jqueryui.com/droppable/ JUI Droppable API} documentation + * for possible options (name-value pairs) and + * {@link http://jqueryui.com/droppable/ JUI Droppable page} for general + * description and demo. + * + * @author Sebastian Thierer + * @package zii.widgets.jui + * @since 1.1 + */ +class CJuiDroppable extends CJuiWidget +{ + /** + * @var string the HTML tag name of the Droppable element. Defaults to 'div'. + */ + public $tagName='div'; + + /** + * Renders the open tag of the droppable element. + * This method also registers the necessary javascript code. + */ + public function init() + { + parent::init(); + + $id=$this->getId(); + if(isset($this->htmlOptions['id'])) + $id=$this->htmlOptions['id']; + else + $this->htmlOptions['id']=$id; + + $options=CJavaScript::encode($this->options); + Yii::app()->getClientScript()->registerScript(__CLASS__.'#'.$id,"jQuery('#{$id}').droppable($options);"); + + echo CHtml::openTag($this->tagName,$this->htmlOptions)."\n"; + } + + /** + * Renders the close tag of the droppable element. + */ + public function run() + { + echo CHtml::closeTag($this->tagName); + } } \ No newline at end of file