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