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