1 <?php
2 
3 namespace Facebook\WebDriver\Firefox;
4 
5 use Facebook\WebDriver\Exception\WebDriverException;
6 use FilesystemIterator;
7 use RecursiveDirectoryIterator;
8 use RecursiveIteratorIterator;
9 use ZipArchive;
10 
11 class 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