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: * @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: