1<?php
2
3class HTMLPurifier_ConfigSchema_InterchangeBuilder
4{
5
6    /**
7     * Used for processing DEFAULT, nothing else.
8     * @type HTMLPurifier_VarParser
9     */
10    protected $varParser;
11
12    /**
13     * @param HTMLPurifier_VarParser $varParser
14     */
15    public function __construct($varParser = null)
16    {
17        $this->varParser = $varParser ? $varParser : new HTMLPurifier_VarParser_Native();
18    }
19
20    /**
21     * @param string $dir
22     * @return HTMLPurifier_ConfigSchema_Interchange
23     */
24    public static function buildFromDirectory($dir = null)
25    {
26        $builder = new HTMLPurifier_ConfigSchema_InterchangeBuilder();
27        $interchange = new HTMLPurifier_ConfigSchema_Interchange();
28        return $builder->buildDir($interchange, $dir);
29    }
30
31    /**
32     * @param HTMLPurifier_ConfigSchema_Interchange $interchange
33     * @param string $dir
34     * @return HTMLPurifier_ConfigSchema_Interchange
35     */
36    public function buildDir($interchange, $dir = null)
37    {
38        if (!$dir) {
39            $dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema';
40        }
41        if (file_exists($dir . '/info.ini')) {
42            $info = parse_ini_file($dir . '/info.ini');
43            $interchange->name = $info['name'];
44        }
45
46        $files = array();
47        $dh = opendir($dir);
48        while (false !== ($file = readdir($dh))) {
49            if (!$file || $file[0] == '.' || strrchr($file, '.') !== '.txt') {
50                continue;
51            }
52            $files[] = $file;
53        }
54        closedir($dh);
55
56        sort($files);
57        foreach ($files as $file) {
58            $this->buildFile($interchange, $dir . '/' . $file);
59        }
60        return $interchange;
61    }
62
63    /**
64     * @param HTMLPurifier_ConfigSchema_Interchange $interchange
65     * @param string $file
66     */
67    public function buildFile($interchange, $file)
68    {
69        $parser = new HTMLPurifier_StringHashParser();
70        $this->build(
71            $interchange,
72            new HTMLPurifier_StringHash($parser->parseFile($file))
73        );
74    }
75
76    /**
77     * Builds an interchange object based on a hash.
78     * @param HTMLPurifier_ConfigSchema_Interchange $interchange HTMLPurifier_ConfigSchema_Interchange object to build
79     * @param HTMLPurifier_StringHash $hash source data
80     * @throws HTMLPurifier_ConfigSchema_Exception
81     */
82    public function build($interchange, $hash)
83    {
84        if (!$hash instanceof HTMLPurifier_StringHash) {
85            $hash = new HTMLPurifier_StringHash($hash);
86        }
87        if (!isset($hash['ID'])) {
88            throw new HTMLPurifier_ConfigSchema_Exception('Hash does not have any ID');
89        }
90        if (strpos($hash['ID'], '.') === false) {
91            if (count($hash) == 2 && isset($hash['DESCRIPTION'])) {
92                $hash->offsetGet('DESCRIPTION'); // prevent complaining
93            } else {
94                throw new HTMLPurifier_ConfigSchema_Exception('All directives must have a namespace');
95            }
96        } else {
97            $this->buildDirective($interchange, $hash);
98        }
99        $this->_findUnused($hash);
100    }
101
102    /**
103     * @param HTMLPurifier_ConfigSchema_Interchange $interchange
104     * @param HTMLPurifier_StringHash $hash
105     * @throws HTMLPurifier_ConfigSchema_Exception
106     */
107    public function buildDirective($interchange, $hash)
108    {
109        $directive = new HTMLPurifier_ConfigSchema_Interchange_Directive();
110
111        // These are required elements:
112        $directive->id = $this->id($hash->offsetGet('ID'));
113        $id = $directive->id->toString(); // convenience
114
115        if (isset($hash['TYPE'])) {
116            $type = explode('/', $hash->offsetGet('TYPE'));
117            if (isset($type[1])) {
118                $directive->typeAllowsNull = true;
119            }
120            $directive->type = $type[0];
121        } else {
122            throw new HTMLPurifier_ConfigSchema_Exception("TYPE in directive hash '$id' not defined");
123        }
124
125        if (isset($hash['DEFAULT'])) {
126            try {
127                $directive->default = $this->varParser->parse(
128                    $hash->offsetGet('DEFAULT'),
129                    $directive->type,
130                    $directive->typeAllowsNull
131                );
132            } catch (HTMLPurifier_VarParserException $e) {
133                throw new HTMLPurifier_ConfigSchema_Exception($e->getMessage() . " in DEFAULT in directive hash '$id'");
134            }
135        }
136
137        if (isset($hash['DESCRIPTION'])) {
138            $directive->description = $hash->offsetGet('DESCRIPTION');
139        }
140
141        if (isset($hash['ALLOWED'])) {
142            $directive->allowed = $this->lookup($this->evalArray($hash->offsetGet('ALLOWED')));
143        }
144
145        if (isset($hash['VALUE-ALIASES'])) {
146            $directive->valueAliases = $this->evalArray($hash->offsetGet('VALUE-ALIASES'));
147        }
148
149        if (isset($hash['ALIASES'])) {
150            $raw_aliases = trim($hash->offsetGet('ALIASES'));
151            $aliases = preg_split('/\s*,\s*/', $raw_aliases);
152            foreach ($aliases as $alias) {
153                $directive->aliases[] = $this->id($alias);
154            }
155        }
156
157        if (isset($hash['VERSION'])) {
158            $directive->version = $hash->offsetGet('VERSION');
159        }
160
161        if (isset($hash['DEPRECATED-USE'])) {
162            $directive->deprecatedUse = $this->id($hash->offsetGet('DEPRECATED-USE'));
163        }
164
165        if (isset($hash['DEPRECATED-VERSION'])) {
166            $directive->deprecatedVersion = $hash->offsetGet('DEPRECATED-VERSION');
167        }
168
169        if (isset($hash['EXTERNAL'])) {
170            $directive->external = preg_split('/\s*,\s*/', trim($hash->offsetGet('EXTERNAL')));
171        }
172
173        $interchange->addDirective($directive);
174    }
175
176    /**
177     * Evaluates an array PHP code string without array() wrapper
178     * @param string $contents
179     */
180    protected function evalArray($contents)
181    {
182        return eval('return array(' . $contents . ');');
183    }
184
185    /**
186     * Converts an array list into a lookup array.
187     * @param array $array
188     * @return array
189     */
190    protected function lookup($array)
191    {
192        $ret = array();
193        foreach ($array as $val) {
194            $ret[$val] = true;
195        }
196        return $ret;
197    }
198
199    /**
200     * Convenience function that creates an HTMLPurifier_ConfigSchema_Interchange_Id
201     * object based on a string Id.
202     * @param string $id
203     * @return HTMLPurifier_ConfigSchema_Interchange_Id
204     */
205    protected function id($id)
206    {
207        return HTMLPurifier_ConfigSchema_Interchange_Id::make($id);
208    }
209
210    /**
211     * Triggers errors for any unused keys passed in the hash; such keys
212     * may indicate typos, missing values, etc.
213     * @param HTMLPurifier_StringHash $hash Hash to check.
214     */
215    protected function _findUnused($hash)
216    {
217        $accessed = $hash->getAccessed();
218        foreach ($hash as $k => $v) {
219            if (!isset($accessed[$k])) {
220                trigger_error("String hash key '$k' not used by builder", E_USER_NOTICE);
221            }
222        }
223    }
224}
225
226// vim: et sw=4 sts=4
227