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