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