1<?php 2 3/** 4 * Class responsible for generating HTMLPurifier_Language objects, managing 5 * caching and fallbacks. 6 * @note Thanks to MediaWiki for the general logic, although this version 7 * has been entirely rewritten 8 * @todo Serialized cache for languages 9 */ 10class HTMLPurifier_LanguageFactory 11{ 12 13 /** 14 * Cache of language code information used to load HTMLPurifier_Language objects. 15 * Structure is: $factory->cache[$language_code][$key] = $value 16 * @type array 17 */ 18 public $cache; 19 20 /** 21 * Valid keys in the HTMLPurifier_Language object. Designates which 22 * variables to slurp out of a message file. 23 * @type array 24 */ 25 public $keys = array('fallback', 'messages', 'errorNames'); 26 27 /** 28 * Instance to validate language codes. 29 * @type HTMLPurifier_AttrDef_Lang 30 * 31 */ 32 protected $validator; 33 34 /** 35 * Cached copy of dirname(__FILE__), directory of current file without 36 * trailing slash. 37 * @type string 38 */ 39 protected $dir; 40 41 /** 42 * Keys whose contents are a hash map and can be merged. 43 * @type array 44 */ 45 protected $mergeable_keys_map = array('messages' => true, 'errorNames' => true); 46 47 /** 48 * Keys whose contents are a list and can be merged. 49 * @value array lookup 50 */ 51 protected $mergeable_keys_list = array(); 52 53 /** 54 * Retrieve sole instance of the factory. 55 * @param HTMLPurifier_LanguageFactory $prototype Optional prototype to overload sole instance with, 56 * or bool true to reset to default factory. 57 * @return HTMLPurifier_LanguageFactory 58 */ 59 public static function instance($prototype = null) 60 { 61 static $instance = null; 62 if ($prototype !== null) { 63 $instance = $prototype; 64 } elseif ($instance === null || $prototype == true) { 65 $instance = new HTMLPurifier_LanguageFactory(); 66 $instance->setup(); 67 } 68 return $instance; 69 } 70 71 /** 72 * Sets up the singleton, much like a constructor 73 * @note Prevents people from getting this outside of the singleton 74 */ 75 public function setup() 76 { 77 $this->validator = new HTMLPurifier_AttrDef_Lang(); 78 $this->dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier'; 79 } 80 81 /** 82 * Creates a language object, handles class fallbacks 83 * @param HTMLPurifier_Config $config 84 * @param HTMLPurifier_Context $context 85 * @param bool|string $code Code to override configuration with. Private parameter. 86 * @return HTMLPurifier_Language 87 */ 88 public function create($config, $context, $code = false) 89 { 90 // validate language code 91 if ($code === false) { 92 $code = $this->validator->validate( 93 $config->get('Core.Language'), 94 $config, 95 $context 96 ); 97 } else { 98 $code = $this->validator->validate($code, $config, $context); 99 } 100 if ($code === false) { 101 $code = 'en'; // malformed code becomes English 102 } 103 104 $pcode = str_replace('-', '_', $code); // make valid PHP classname 105 static $depth = 0; // recursion protection 106 107 if ($code == 'en') { 108 $lang = new HTMLPurifier_Language($config, $context); 109 } else { 110 $class = 'HTMLPurifier_Language_' . $pcode; 111 $file = $this->dir . '/Language/classes/' . $code . '.php'; 112 if (file_exists($file) || class_exists($class, false)) { 113 $lang = new $class($config, $context); 114 } else { 115 // Go fallback 116 $raw_fallback = $this->getFallbackFor($code); 117 $fallback = $raw_fallback ? $raw_fallback : 'en'; 118 $depth++; 119 $lang = $this->create($config, $context, $fallback); 120 if (!$raw_fallback) { 121 $lang->error = true; 122 } 123 $depth--; 124 } 125 } 126 $lang->code = $code; 127 return $lang; 128 } 129 130 /** 131 * Returns the fallback language for language 132 * @note Loads the original language into cache 133 * @param string $code language code 134 * @return string|bool 135 */ 136 public function getFallbackFor($code) 137 { 138 $this->loadLanguage($code); 139 return $this->cache[$code]['fallback']; 140 } 141 142 /** 143 * Loads language into the cache, handles message file and fallbacks 144 * @param string $code language code 145 */ 146 public function loadLanguage($code) 147 { 148 static $languages_seen = array(); // recursion guard 149 150 // abort if we've already loaded it 151 if (isset($this->cache[$code])) { 152 return; 153 } 154 155 // generate filename 156 $filename = $this->dir . '/Language/messages/' . $code . '.php'; 157 158 // default fallback : may be overwritten by the ensuing include 159 $fallback = ($code != 'en') ? 'en' : false; 160 161 // load primary localisation 162 if (!file_exists($filename)) { 163 // skip the include: will rely solely on fallback 164 $filename = $this->dir . '/Language/messages/en.php'; 165 $cache = array(); 166 } else { 167 include $filename; 168 $cache = compact($this->keys); 169 } 170 171 // load fallback localisation 172 if (!empty($fallback)) { 173 174 // infinite recursion guard 175 if (isset($languages_seen[$code])) { 176 trigger_error( 177 'Circular fallback reference in language ' . 178 $code, 179 E_USER_ERROR 180 ); 181 $fallback = 'en'; 182 } 183 $language_seen[$code] = true; 184 185 // load the fallback recursively 186 $this->loadLanguage($fallback); 187 $fallback_cache = $this->cache[$fallback]; 188 189 // merge fallback with current language 190 foreach ($this->keys as $key) { 191 if (isset($cache[$key]) && isset($fallback_cache[$key])) { 192 if (isset($this->mergeable_keys_map[$key])) { 193 $cache[$key] = $cache[$key] + $fallback_cache[$key]; 194 } elseif (isset($this->mergeable_keys_list[$key])) { 195 $cache[$key] = array_merge($fallback_cache[$key], $cache[$key]); 196 } 197 } else { 198 $cache[$key] = $fallback_cache[$key]; 199 } 200 } 201 } 202 203 // save to cache for later retrieval 204 $this->cache[$code] = $cache; 205 return; 206 } 207} 208 209// vim: et sw=4 sts=4 210