HTMLPurifier 4.4.0
/home/ezyang/Dev/htmlpurifier/library/HTMLPurifier/Config.php
Go to the documentation of this file.
00001 <?php
00002 
00017 class HTMLPurifier_Config
00018 {
00019 
00023     public $version = '4.4.0';
00024 
00029     public $autoFinalize = true;
00030 
00031     // protected member variables
00032 
00037     protected $serials = array();
00038 
00042     protected $serial;
00043 
00047     protected $parser = null;
00048 
00054     public $def;
00055 
00059     protected $definitions;
00060 
00064     protected $finalized = false;
00065 
00069     protected $plist;
00070 
00075     private $aliasMode;
00076 
00082     public $chatty = true;
00083 
00087     private $lock;
00088 
00093     public function __construct($definition, $parent = null) {
00094         $parent = $parent ? $parent : $definition->defaultPlist;
00095         $this->plist = new HTMLPurifier_PropertyList($parent);
00096         $this->def = $definition; // keep a copy around for checking
00097         $this->parser = new HTMLPurifier_VarParser_Flexible();
00098     }
00099 
00109     public static function create($config, $schema = null) {
00110         if ($config instanceof HTMLPurifier_Config) {
00111             // pass-through
00112             return $config;
00113         }
00114         if (!$schema) {
00115             $ret = HTMLPurifier_Config::createDefault();
00116         } else {
00117             $ret = new HTMLPurifier_Config($schema);
00118         }
00119         if (is_string($config)) $ret->loadIni($config);
00120         elseif (is_array($config)) $ret->loadArray($config);
00121         return $ret;
00122     }
00123 
00130     public static function inherit(HTMLPurifier_Config $config) {
00131         return new HTMLPurifier_Config($config->def, $config->plist);
00132     }
00133 
00138     public static function createDefault() {
00139         $definition = HTMLPurifier_ConfigSchema::instance();
00140         $config = new HTMLPurifier_Config($definition);
00141         return $config;
00142     }
00143 
00148     public function get($key, $a = null) {
00149         if ($a !== null) {
00150             $this->triggerError("Using deprecated API: use \$config->get('$key.$a') instead", E_USER_WARNING);
00151             $key = "$key.$a";
00152         }
00153         if (!$this->finalized) $this->autoFinalize();
00154         if (!isset($this->def->info[$key])) {
00155             // can't add % due to SimpleTest bug
00156             $this->triggerError('Cannot retrieve value of undefined directive ' . htmlspecialchars($key),
00157                 E_USER_WARNING);
00158             return;
00159         }
00160         if (isset($this->def->info[$key]->isAlias)) {
00161             $d = $this->def->info[$key];
00162             $this->triggerError('Cannot get value from aliased directive, use real name ' . $d->key,
00163                 E_USER_ERROR);
00164             return;
00165         }
00166         if ($this->lock) {
00167             list($ns) = explode('.', $key);
00168             if ($ns !== $this->lock) {
00169                 $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);
00170                 return;
00171             }
00172         }
00173         return $this->plist->get($key);
00174     }
00175 
00180     public function getBatch($namespace) {
00181         if (!$this->finalized) $this->autoFinalize();
00182         $full = $this->getAll();
00183         if (!isset($full[$namespace])) {
00184             $this->triggerError('Cannot retrieve undefined namespace ' . htmlspecialchars($namespace),
00185                 E_USER_WARNING);
00186             return;
00187         }
00188         return $full[$namespace];
00189     }
00190 
00198     public function getBatchSerial($namespace) {
00199         if (empty($this->serials[$namespace])) {
00200             $batch = $this->getBatch($namespace);
00201             unset($batch['DefinitionRev']);
00202             $this->serials[$namespace] = md5(serialize($batch));
00203         }
00204         return $this->serials[$namespace];
00205     }
00206 
00211     public function getSerial() {
00212         if (empty($this->serial)) {
00213             $this->serial = md5(serialize($this->getAll()));
00214         }
00215         return $this->serial;
00216     }
00217 
00222     public function getAll() {
00223         if (!$this->finalized) $this->autoFinalize();
00224         $ret = array();
00225         foreach ($this->plist->squash() as $name => $value) {
00226             list($ns, $key) = explode('.', $name, 2);
00227             $ret[$ns][$key] = $value;
00228         }
00229         return $ret;
00230     }
00231 
00237     public function set($key, $value, $a = null) {
00238         if (strpos($key, '.') === false) {
00239             $namespace = $key;
00240             $directive = $value;
00241             $value = $a;
00242             $key = "$key.$directive";
00243             $this->triggerError("Using deprecated API: use \$config->set('$key', ...) instead", E_USER_NOTICE);
00244         } else {
00245             list($namespace) = explode('.', $key);
00246         }
00247         if ($this->isFinalized('Cannot set directive after finalization')) return;
00248         if (!isset($this->def->info[$key])) {
00249             $this->triggerError('Cannot set undefined directive ' . htmlspecialchars($key) . ' to value',
00250                 E_USER_WARNING);
00251             return;
00252         }
00253         $def = $this->def->info[$key];
00254 
00255         if (isset($def->isAlias)) {
00256             if ($this->aliasMode) {
00257                 $this->triggerError('Double-aliases not allowed, please fix '.
00258                     'ConfigSchema bug with' . $key, E_USER_ERROR);
00259                 return;
00260             }
00261             $this->aliasMode = true;
00262             $this->set($def->key, $value);
00263             $this->aliasMode = false;
00264             $this->triggerError("$key is an alias, preferred directive name is {$def->key}", E_USER_NOTICE);
00265             return;
00266         }
00267 
00268         // Raw type might be negative when using the fully optimized form
00269         // of stdclass, which indicates allow_null == true
00270         $rtype = is_int($def) ? $def : $def->type;
00271         if ($rtype < 0) {
00272             $type = -$rtype;
00273             $allow_null = true;
00274         } else {
00275             $type = $rtype;
00276             $allow_null = isset($def->allow_null);
00277         }
00278 
00279         try {
00280             $value = $this->parser->parse($value, $type, $allow_null);
00281         } catch (HTMLPurifier_VarParserException $e) {
00282             $this->triggerError('Value for ' . $key . ' is of invalid type, should be ' . HTMLPurifier_VarParser::getTypeName($type), E_USER_WARNING);
00283             return;
00284         }
00285         if (is_string($value) && is_object($def)) {
00286             // resolve value alias if defined
00287             if (isset($def->aliases[$value])) {
00288                 $value = $def->aliases[$value];
00289             }
00290             // check to see if the value is allowed
00291             if (isset($def->allowed) && !isset($def->allowed[$value])) {
00292                 $this->triggerError('Value not supported, valid values are: ' .
00293                     $this->_listify($def->allowed), E_USER_WARNING);
00294                 return;
00295             }
00296         }
00297         $this->plist->set($key, $value);
00298 
00299         // reset definitions if the directives they depend on changed
00300         // this is a very costly process, so it's discouraged
00301         // with finalization
00302         if ($namespace == 'HTML' || $namespace == 'CSS' || $namespace == 'URI') {
00303             $this->definitions[$namespace] = null;
00304         }
00305 
00306         $this->serials[$namespace] = false;
00307     }
00308 
00312     private function _listify($lookup) {
00313         $list = array();
00314         foreach ($lookup as $name => $b) $list[] = $name;
00315         return implode(', ', $list);
00316     }
00317 
00329     public function getHTMLDefinition($raw = false, $optimized = false) {
00330         return $this->getDefinition('HTML', $raw, $optimized);
00331     }
00332 
00344     public function getCSSDefinition($raw = false, $optimized = false) {
00345         return $this->getDefinition('CSS', $raw, $optimized);
00346     }
00347 
00359     public function getURIDefinition($raw = false, $optimized = false) {
00360         return $this->getDefinition('URI', $raw, $optimized);
00361     }
00362 
00376     public function getDefinition($type, $raw = false, $optimized = false) {
00377         if ($optimized && !$raw) {
00378             throw new HTMLPurifier_Exception("Cannot set optimized = true when raw = false");
00379         }
00380         if (!$this->finalized) $this->autoFinalize();
00381         // temporarily suspend locks, so we can handle recursive definition calls
00382         $lock = $this->lock;
00383         $this->lock = null;
00384         $factory = HTMLPurifier_DefinitionCacheFactory::instance();
00385         $cache = $factory->create($type, $this);
00386         $this->lock = $lock;
00387         if (!$raw) {
00388             // full definition
00389             // ---------------
00390             // check if definition is in memory
00391             if (!empty($this->definitions[$type])) {
00392                 $def = $this->definitions[$type];
00393                 // check if the definition is setup
00394                 if ($def->setup) {
00395                     return $def;
00396                 } else {
00397                     $def->setup($this);
00398                     if ($def->optimized) $cache->add($def, $this);
00399                     return $def;
00400                 }
00401             }
00402             // check if definition is in cache
00403             $def = $cache->get($this);
00404             if ($def) {
00405                 // definition in cache, save to memory and return it
00406                 $this->definitions[$type] = $def;
00407                 return $def;
00408             }
00409             // initialize it
00410             $def = $this->initDefinition($type);
00411             // set it up
00412             $this->lock = $type;
00413             $def->setup($this);
00414             $this->lock = null;
00415             // save in cache
00416             $cache->add($def, $this);
00417             // return it
00418             return $def;
00419         } else {
00420             // raw definition
00421             // --------------
00422             // check preconditions
00423             $def = null;
00424             if ($optimized) {
00425                 if (is_null($this->get($type . '.DefinitionID'))) {
00426                     // fatally error out if definition ID not set
00427                     throw new HTMLPurifier_Exception("Cannot retrieve raw version without specifying %$type.DefinitionID");
00428                 }
00429             }
00430             if (!empty($this->definitions[$type])) {
00431                 $def = $this->definitions[$type];
00432                 if ($def->setup && !$optimized) {
00433                     $extra = $this->chatty ? " (try moving this code block earlier in your initialization)" : "";
00434                     throw new HTMLPurifier_Exception("Cannot retrieve raw definition after it has already been setup" . $extra);
00435                 }
00436                 if ($def->optimized === null) {
00437                     $extra = $this->chatty ? " (try flushing your cache)" : "";
00438                     throw new HTMLPurifier_Exception("Optimization status of definition is unknown" . $extra);
00439                 }
00440                 if ($def->optimized !== $optimized) {
00441                     $msg = $optimized ? "optimized" : "unoptimized";
00442                     $extra = $this->chatty ? " (this backtrace is for the first inconsistent call, which was for a $msg raw definition)" : "";
00443                     throw new HTMLPurifier_Exception("Inconsistent use of optimized and unoptimized raw definition retrievals" . $extra);
00444                 }
00445             }
00446             // check if definition was in memory
00447             if ($def) {
00448                 if ($def->setup) {
00449                     // invariant: $optimized === true (checked above)
00450                     return null;
00451                 } else {
00452                     return $def;
00453                 }
00454             }
00455             // if optimized, check if definition was in cache
00456             // (because we do the memory check first, this formulation
00457             // is prone to cache slamming, but I think
00458             // guaranteeing that either /all/ of the raw
00459             // setup code or /none/ of it is run is more important.)
00460             if ($optimized) {
00461                 // This code path only gets run once; once we put
00462                 // something in $definitions (which is guaranteed by the
00463                 // trailing code), we always short-circuit above.
00464                 $def = $cache->get($this);
00465                 if ($def) {
00466                     // save the full definition for later, but don't
00467                     // return it yet
00468                     $this->definitions[$type] = $def;
00469                     return null;
00470                 }
00471             }
00472             // check invariants for creation
00473             if (!$optimized) {
00474                 if (!is_null($this->get($type . '.DefinitionID'))) {
00475                     if ($this->chatty) {
00476                         $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 <a href='http://htmlpurifier.org/docs/enduser-customize.html#optimized'>Customize</a> for more details", E_USER_WARNING);
00477                     } else {
00478                         $this->triggerError("Useless DefinitionID declaration", E_USER_WARNING);
00479                     }
00480                 }
00481             }
00482             // initialize it
00483             $def = $this->initDefinition($type);
00484             $def->optimized = $optimized;
00485             return $def;
00486         }
00487         throw new HTMLPurifier_Exception("The impossible happened!");
00488     }
00489 
00490     private function initDefinition($type) {
00491         // quick checks failed, let's create the object
00492         if ($type == 'HTML') {
00493             $def = new HTMLPurifier_HTMLDefinition();
00494         } elseif ($type == 'CSS') {
00495             $def = new HTMLPurifier_CSSDefinition();
00496         } elseif ($type == 'URI') {
00497             $def = new HTMLPurifier_URIDefinition();
00498         } else {
00499             throw new HTMLPurifier_Exception("Definition of $type type not supported");
00500         }
00501         $this->definitions[$type] = $def;
00502         return $def;
00503     }
00504 
00505     public function maybeGetRawDefinition($name) {
00506         return $this->getDefinition($name, true, true);
00507     }
00508 
00509     public function maybeGetRawHTMLDefinition() {
00510         return $this->getDefinition('HTML', true, true);
00511     }
00512 
00513     public function maybeGetRawCSSDefinition() {
00514         return $this->getDefinition('CSS', true, true);
00515     }
00516 
00517     public function maybeGetRawURIDefinition() {
00518         return $this->getDefinition('URI', true, true);
00519     }
00520 
00526     public function loadArray($config_array) {
00527         if ($this->isFinalized('Cannot load directives after finalization')) return;
00528         foreach ($config_array as $key => $value) {
00529             $key = str_replace('_', '.', $key);
00530             if (strpos($key, '.') !== false) {
00531                 $this->set($key, $value);
00532             } else {
00533                 $namespace = $key;
00534                 $namespace_values = $value;
00535                 foreach ($namespace_values as $directive => $value) {
00536                     $this->set($namespace .'.'. $directive, $value);
00537                 }
00538             }
00539         }
00540     }
00541 
00548     public static function getAllowedDirectivesForForm($allowed, $schema = null) {
00549         if (!$schema) {
00550             $schema = HTMLPurifier_ConfigSchema::instance();
00551         }
00552         if ($allowed !== true) {
00553              if (is_string($allowed)) $allowed = array($allowed);
00554              $allowed_ns = array();
00555              $allowed_directives = array();
00556              $blacklisted_directives = array();
00557              foreach ($allowed as $ns_or_directive) {
00558                  if (strpos($ns_or_directive, '.') !== false) {
00559                      // directive
00560                      if ($ns_or_directive[0] == '-') {
00561                          $blacklisted_directives[substr($ns_or_directive, 1)] = true;
00562                      } else {
00563                          $allowed_directives[$ns_or_directive] = true;
00564                      }
00565                  } else {
00566                      // namespace
00567                      $allowed_ns[$ns_or_directive] = true;
00568                  }
00569              }
00570         }
00571         $ret = array();
00572         foreach ($schema->info as $key => $def) {
00573             list($ns, $directive) = explode('.', $key, 2);
00574             if ($allowed !== true) {
00575                 if (isset($blacklisted_directives["$ns.$directive"])) continue;
00576                 if (!isset($allowed_directives["$ns.$directive"]) && !isset($allowed_ns[$ns])) continue;
00577             }
00578             if (isset($def->isAlias)) continue;
00579             if ($directive == 'DefinitionID' || $directive == 'DefinitionRev') continue;
00580             $ret[] = array($ns, $directive);
00581         }
00582         return $ret;
00583     }
00584 
00594     public static function loadArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) {
00595         $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $schema);
00596         $config = HTMLPurifier_Config::create($ret, $schema);
00597         return $config;
00598     }
00599 
00604     public function mergeArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true) {
00605          $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $this->def);
00606          $this->loadArray($ret);
00607     }
00608 
00613     public static function prepareArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) {
00614         if ($index !== false) $array = (isset($array[$index]) && is_array($array[$index])) ? $array[$index] : array();
00615         $mq = $mq_fix && function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc();
00616 
00617         $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $schema);
00618         $ret = array();
00619         foreach ($allowed as $key) {
00620             list($ns, $directive) = $key;
00621             $skey = "$ns.$directive";
00622             if (!empty($array["Null_$skey"])) {
00623                 $ret[$ns][$directive] = null;
00624                 continue;
00625             }
00626             if (!isset($array[$skey])) continue;
00627             $value = $mq ? stripslashes($array[$skey]) : $array[$skey];
00628             $ret[$ns][$directive] = $value;
00629         }
00630         return $ret;
00631     }
00632 
00637     public function loadIni($filename) {
00638         if ($this->isFinalized('Cannot load directives after finalization')) return;
00639         $array = parse_ini_file($filename, true);
00640         $this->loadArray($array);
00641     }
00642 
00647     public function isFinalized($error = false) {
00648         if ($this->finalized && $error) {
00649             $this->triggerError($error, E_USER_ERROR);
00650         }
00651         return $this->finalized;
00652     }
00653 
00658     public function autoFinalize() {
00659         if ($this->autoFinalize) {
00660             $this->finalize();
00661         } else {
00662             $this->plist->squash(true);
00663         }
00664     }
00665 
00669     public function finalize() {
00670         $this->finalized = true;
00671         $this->parser = null;
00672     }
00673 
00678     protected function triggerError($msg, $no) {
00679         // determine previous stack frame
00680         $extra = '';
00681         if ($this->chatty) {
00682             $trace = debug_backtrace();
00683             // zip(tail(trace), trace) -- but PHP is not Haskell har har
00684             for ($i = 0, $c = count($trace); $i < $c - 1; $i++) {
00685                 if ($trace[$i + 1]['class'] === 'HTMLPurifier_Config') {
00686                     continue;
00687                 }
00688                 $frame = $trace[$i];
00689                 $extra = " invoked on line {$frame['line']} in file {$frame['file']}";
00690                 break;
00691             }
00692         }
00693         trigger_error($msg . $extra, $no);
00694     }
00695 
00700     public function serialize() {
00701         $this->getDefinition('HTML');
00702         $this->getDefinition('CSS');
00703         $this->getDefinition('URI');
00704         return serialize($this);
00705     }
00706 
00707 }
00708 
00709 // vim: et sw=4 sts=4