1<?php
2
3namespace Facebook\WebDriver\Firefox;
4
5use Facebook\WebDriver\Exception\WebDriverException;
6use FilesystemIterator;
7use RecursiveDirectoryIterator;
8use RecursiveIteratorIterator;
9use ZipArchive;
10
11class FirefoxProfile
12{
13    /**
14     * @var array
15     */
16    private $preferences = [];
17    /**
18     * @var array
19     */
20    private $extensions = [];
21    /**
22     * @var array
23     */
24    private $extensions_datas = [];
25    /**
26     * @var string
27     */
28    private $rdf_file;
29
30    /**
31     * @param string $extension The path to the xpi extension.
32     * @return FirefoxProfile
33     */
34    public function addExtension($extension)
35    {
36        $this->extensions[] = $extension;
37
38        return $this;
39    }
40
41    /**
42     * @param string $extension_datas The path to the folder containing the datas to add to the extension
43     * @return FirefoxProfile
44     */
45    public function addExtensionDatas($extension_datas)
46    {
47        if (!is_dir($extension_datas)) {
48            return null;
49        }
50
51        $this->extensions_datas[basename($extension_datas)] = $extension_datas;
52
53        return $this;
54    }
55
56    /**
57     * @param string $rdf_file The path to the rdf file
58     * @return FirefoxProfile
59     */
60    public function setRdfFile($rdf_file)
61    {
62        if (!is_file($rdf_file)) {
63            return null;
64        }
65
66        $this->rdf_file = $rdf_file;
67
68        return $this;
69    }
70
71    /**
72     * @param string $key
73     * @param string|bool|int $value
74     * @throws WebDriverException
75     * @return FirefoxProfile
76     */
77    public function setPreference($key, $value)
78    {
79        if (is_string($value)) {
80            $value = sprintf('"%s"', $value);
81        } else {
82            if (is_int($value)) {
83                $value = sprintf('%d', $value);
84            } else {
85                if (is_bool($value)) {
86                    $value = $value ? 'true' : 'false';
87                } else {
88                    throw new WebDriverException(
89                        'The value of the preference should be either a string, int or bool.'
90                    );
91                }
92            }
93        }
94        $this->preferences[$key] = $value;
95
96        return $this;
97    }
98
99    /**
100     * @param mixed $key
101     * @return mixed
102     */
103    public function getPreference($key)
104    {
105        if (array_key_exists($key, $this->preferences)) {
106            return $this->preferences[$key];
107        }
108
109        return null;
110    }
111
112    /**
113     * @return string
114     */
115    public function encode()
116    {
117        $temp_dir = $this->createTempDirectory('WebDriverFirefoxProfile');
118
119        if (isset($this->rdf_file)) {
120            copy($this->rdf_file, $temp_dir . DIRECTORY_SEPARATOR . 'mimeTypes.rdf');
121        }
122
123        foreach ($this->extensions as $extension) {
124            $this->installExtension($extension, $temp_dir);
125        }
126
127        foreach ($this->extensions_datas as $dirname => $extension_datas) {
128            mkdir($temp_dir . DIRECTORY_SEPARATOR . $dirname);
129            $iterator = new RecursiveIteratorIterator(
130                new RecursiveDirectoryIterator($extension_datas, RecursiveDirectoryIterator::SKIP_DOTS),
131                RecursiveIteratorIterator::SELF_FIRST
132            );
133            foreach ($iterator as $item) {
134                $target_dir = $temp_dir . DIRECTORY_SEPARATOR . $dirname . DIRECTORY_SEPARATOR
135                    . $iterator->getSubPathName();
136
137                if ($item->isDir()) {
138                    mkdir($target_dir);
139                } else {
140                    copy($item, $target_dir);
141                }
142            }
143        }
144
145        $content = '';
146        foreach ($this->preferences as $key => $value) {
147            $content .= sprintf("user_pref(\"%s\", %s);\n", $key, $value);
148        }
149        file_put_contents($temp_dir . '/user.js', $content);
150
151        // Intentionally do not use `tempnam()`, as it creates empty file which zip extension may not handle.
152        $temp_zip = sys_get_temp_dir() . '/' . uniqid('WebDriverFirefoxProfileZip', false);
153
154        $zip = new ZipArchive();
155        $zip->open($temp_zip, ZipArchive::CREATE);
156
157        $dir = new RecursiveDirectoryIterator($temp_dir);
158        $files = new RecursiveIteratorIterator($dir);
159
160        $dir_prefix = preg_replace(
161            '#\\\\#',
162            '\\\\\\\\',
163            $temp_dir . DIRECTORY_SEPARATOR
164        );
165
166        foreach ($files as $name => $object) {
167            if (is_dir($name)) {
168                continue;
169            }
170
171            $path = preg_replace("#^{$dir_prefix}#", '', $name);
172            $zip->addFile($name, $path);
173        }
174        $zip->close();
175
176        $profile = base64_encode(file_get_contents($temp_zip));
177
178        // clean up
179        $this->deleteDirectory($temp_dir);
180        unlink($temp_zip);
181
182        return $profile;
183    }
184
185    /**
186     * @param string $extension The path to the extension.
187     * @param string $profile_dir The path to the profile directory.
188     * @return string The path to the directory of this extension.
189     */
190    private function installExtension($extension, $profile_dir)
191    {
192        $temp_dir = $this->createTempDirectory('WebDriverFirefoxProfileExtension');
193        $this->extractTo($extension, $temp_dir);
194
195        // This is a hacky way to parse the id since there is no offical RDF parser library.
196        // Find the correct namespace for the id element.
197        $install_rdf_path = $temp_dir . '/install.rdf';
198        $xml = simplexml_load_file($install_rdf_path);
199        $ns = $xml->getDocNamespaces();
200        $prefix = '';
201        if (!empty($ns)) {
202            foreach ($ns as $key => $value) {
203                if (mb_strpos($value, '//www.mozilla.org/2004/em-rdf') > 0) {
204                    if ($key != '') {
205                        $prefix = $key . ':'; // Separate the namespace from the name.
206                    }
207                    break;
208                }
209            }
210        }
211        // Get the extension id from the install manifest.
212        $matches = [];
213        preg_match('#<' . $prefix . 'id>([^<]+)</' . $prefix . 'id>#', $xml->asXML(), $matches);
214        if (isset($matches[1])) {
215            $ext_dir = $profile_dir . '/extensions/' . $matches[1];
216            mkdir($ext_dir, 0777, true);
217            $this->extractTo($extension, $ext_dir);
218        } else {
219            $this->deleteDirectory($temp_dir);
220
221            throw new WebDriverException('Cannot get the extension id from the install manifest.');
222        }
223
224        $this->deleteDirectory($temp_dir);
225
226        return $ext_dir;
227    }
228
229    /**
230     * @param string $prefix Prefix of the temp directory.
231     *
232     * @throws WebDriverException
233     * @return string The path to the temp directory created.
234     */
235    private function createTempDirectory($prefix = '')
236    {
237        $temp_dir = tempnam(sys_get_temp_dir(), $prefix);
238        if (file_exists($temp_dir)) {
239            unlink($temp_dir);
240            mkdir($temp_dir);
241            if (!is_dir($temp_dir)) {
242                throw new WebDriverException('Cannot create firefox profile.');
243            }
244        }
245
246        return $temp_dir;
247    }
248
249    /**
250     * @param string $directory The path to the directory.
251     */
252    private function deleteDirectory($directory)
253    {
254        $dir = new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS);
255        $paths = new RecursiveIteratorIterator($dir, RecursiveIteratorIterator::CHILD_FIRST);
256
257        foreach ($paths as $path) {
258            if ($path->isDir() && !$path->isLink()) {
259                rmdir($path->getPathname());
260            } else {
261                unlink($path->getPathname());
262            }
263        }
264
265        rmdir($directory);
266    }
267
268    /**
269     * @param string $xpi The path to the .xpi extension.
270     * @param string $target_dir The path to the unzip directory.
271     *
272     * @throws \Exception
273     * @return FirefoxProfile
274     */
275    private function extractTo($xpi, $target_dir)
276    {
277        $zip = new ZipArchive();
278        if (file_exists($xpi)) {
279            if ($zip->open($xpi)) {
280                $zip->extractTo($target_dir);
281                $zip->close();
282            } else {
283                throw new \Exception("Failed to open the firefox extension. '$xpi'");
284            }
285        } else {
286            throw new \Exception("Firefox extension doesn't exist. '$xpi'");
287        }
288
289        return $this;
290    }
291}
292