1<?php 2/** 3 * @package FrameworkOnFramework 4 * @subpackage dispatcher 5 * @copyright Copyright (C) 2010-2016 Nicholas K. Dionysopoulos / Akeeba Ltd. All rights reserved. 6 * @license GNU General Public License version 2 or later; see LICENSE.txt 7 */ 8// Protect from unauthorized access 9defined('FOF_INCLUDED') or die; 10 11class FOFDownload 12{ 13 /** 14 * Parameters passed from the GUI when importing from URL 15 * 16 * @var array 17 */ 18 private $params = array(); 19 20 /** 21 * The download adapter which will be used by this class 22 * 23 * @var FOFDownloadInterface 24 */ 25 private $adapter = null; 26 27 /** 28 * Additional params that will be passed to the adapter while performing the download 29 * 30 * @var array 31 */ 32 private $adapterOptions = array(); 33 34 /** 35 * Creates a new download object and assigns it the most fitting download adapter 36 */ 37 public function __construct() 38 { 39 // Find the best fitting adapter 40 $allAdapters = self::getFiles(__DIR__ . '/adapter', array(), array('abstract.php')); 41 $priority = 0; 42 43 foreach ($allAdapters as $adapterInfo) 44 { 45 if (!class_exists($adapterInfo['classname'], true)) 46 { 47 continue; 48 } 49 50 /** @var FOFDownloadAdapterAbstract $adapter */ 51 $adapter = new $adapterInfo['classname']; 52 53 if ( !$adapter->isSupported()) 54 { 55 continue; 56 } 57 58 if ($adapter->priority > $priority) 59 { 60 $this->adapter = $adapter; 61 $priority = $adapter->priority; 62 } 63 } 64 65 // Load the language strings 66 FOFPlatform::getInstance()->loadTranslations('lib_f0f'); 67 } 68 69 /** 70 * Forces the use of a specific adapter 71 * 72 * @param string $className The name of the class or the name of the adapter, e.g. 'FOFDownloadAdapterCurl' or 73 * 'curl' 74 */ 75 public function setAdapter($className) 76 { 77 $adapter = null; 78 79 if (class_exists($className, true)) 80 { 81 $adapter = new $className; 82 } 83 elseif (class_exists('FOFDownloadAdapter' . ucfirst($className))) 84 { 85 $className = 'FOFDownloadAdapter' . ucfirst($className); 86 $adapter = new $className; 87 } 88 89 if (is_object($adapter) && ($adapter instanceof FOFDownloadInterface)) 90 { 91 $this->adapter = $adapter; 92 } 93 } 94 95 /** 96 * Returns the name of the current adapter 97 * 98 * @return string 99 */ 100 public function getAdapterName() 101 { 102 if(is_object($this->adapter)) 103 { 104 $class = get_class($this->adapter); 105 106 return strtolower(str_ireplace('FOFDownloadAdapter', '', $class)); 107 } 108 109 return ''; 110 } 111 112 /** 113 * Sets the additional options for the adapter 114 * 115 * @param array $options 116 */ 117 public function setAdapterOptions(array $options) 118 { 119 $this->adapterOptions = $options; 120 } 121 122 /** 123 * Returns the additional options for the adapter 124 * 125 * @return array 126 */ 127 public function getAdapterOptions() 128 { 129 return $this->adapterOptions; 130 } 131 132 /** 133 * Used to decode the $params array 134 * 135 * @param string $key The parameter key you want to retrieve the value for 136 * @param mixed $default The default value, if none is specified 137 * 138 * @return mixed The value for this parameter key 139 */ 140 private function getParam($key, $default = null) 141 { 142 if (array_key_exists($key, $this->params)) 143 { 144 return $this->params[$key]; 145 } 146 else 147 { 148 return $default; 149 } 150 } 151 152 /** 153 * Download data from a URL and return it 154 * 155 * @param string $url The URL to download from 156 * 157 * @return bool|string The downloaded data or false on failure 158 */ 159 public function getFromURL($url) 160 { 161 try 162 { 163 return $this->adapter->downloadAndReturn($url, null, null, $this->adapterOptions); 164 } 165 catch (Exception $e) 166 { 167 return false; 168 } 169 } 170 171 /** 172 * Performs the staggered download of file. The downloaded file will be stored in Joomla!'s temp-path using the 173 * basename of the URL as a filename 174 * 175 * The $params array can have any of the following keys 176 * url The file being downloaded 177 * frag Rolling counter of the file fragment being downloaded 178 * totalSize The total size of the file being downloaded, in bytes 179 * doneSize How many bytes we have already downloaded 180 * maxExecTime Maximum execution time downloading file fragments, in seconds 181 * length How many bytes to download at once 182 * 183 * The array returned is in the following format: 184 * 185 * status True if there are no errors, false if there are errors 186 * error A string with the error message if there are errors 187 * frag The next file fragment to download 188 * totalSize The total size of the downloaded file in bytes, if the server supports HEAD requests 189 * doneSize How many bytes have already been downloaded 190 * percent % of the file already downloaded (if totalSize could be determined) 191 * localfile The name of the local file, without the path 192 * 193 * @param array $params A parameters array, as sent by the user interface 194 * 195 * @return array A return status array 196 */ 197 public function importFromURL($params) 198 { 199 $this->params = $params; 200 201 // Fetch data 202 $url = $this->getParam('url'); 203 $localFilename = $this->getParam('localFilename'); 204 $frag = $this->getParam('frag', -1); 205 $totalSize = $this->getParam('totalSize', -1); 206 $doneSize = $this->getParam('doneSize', -1); 207 $maxExecTime = $this->getParam('maxExecTime', 5); 208 $runTimeBias = $this->getParam('runTimeBias', 75); 209 $length = $this->getParam('length', 1048576); 210 211 if (empty($localFilename)) 212 { 213 $localFilename = basename($url); 214 215 if (strpos($localFilename, '?') !== false) 216 { 217 $paramsPos = strpos($localFilename, '?'); 218 $localFilename = substr($localFilename, 0, $paramsPos - 1); 219 } 220 } 221 222 $tmpDir = JFactory::getConfig()->get('tmp_path', JPATH_ROOT . '/tmp'); 223 $tmpDir = rtrim($tmpDir, '/\\'); 224 225 // Init retArray 226 $retArray = array( 227 "status" => true, 228 "error" => '', 229 "frag" => $frag, 230 "totalSize" => $totalSize, 231 "doneSize" => $doneSize, 232 "percent" => 0, 233 "localfile" => $localFilename 234 ); 235 236 try 237 { 238 $timer = new FOFUtilsTimer($maxExecTime, $runTimeBias); 239 $start = $timer->getRunningTime(); // Mark the start of this download 240 $break = false; // Don't break the step 241 242 // Figure out where on Earth to put that file 243 $local_file = $tmpDir . '/' . $localFilename; 244 245 while (($timer->getTimeLeft() > 0) && !$break) 246 { 247 // Do we have to initialize the file? 248 if ($frag == -1) 249 { 250 // Currently downloaded size 251 $doneSize = 0; 252 253 if (@file_exists($local_file)) 254 { 255 @unlink($local_file); 256 } 257 258 // Delete and touch the output file 259 $fp = @fopen($local_file, 'wb'); 260 261 if ($fp !== false) 262 { 263 @fclose($fp); 264 } 265 266 // Init 267 $frag = 0; 268 269 //debugMsg("-- First frag, getting the file size"); 270 $retArray['totalSize'] = $this->adapter->getFileSize($url); 271 $totalSize = $retArray['totalSize']; 272 } 273 274 // Calculate from and length 275 $from = $frag * $length; 276 $to = $length + $from - 1; 277 278 // Try to download the first frag 279 $required_time = 1.0; 280 281 try 282 { 283 $result = $this->adapter->downloadAndReturn($url, $from, $to, $this->adapterOptions); 284 285 if ($result === false) 286 { 287 throw new Exception(JText::sprintf('LIB_FOF_DOWNLOAD_ERR_COULDNOTDOWNLOADFROMURL', $url), 500); 288 } 289 } 290 catch (Exception $e) 291 { 292 $result = false; 293 $error = $e->getMessage(); 294 } 295 296 if ($result === false) 297 { 298 // Failed download 299 if ($frag == 0) 300 { 301 // Failure to download first frag = failure to download. Period. 302 $retArray['status'] = false; 303 $retArray['error'] = $error; 304 305 //debugMsg("-- Download FAILED"); 306 307 return $retArray; 308 } 309 else 310 { 311 // Since this is a staggered download, consider this normal and finish 312 $frag = -1; 313 //debugMsg("-- Import complete"); 314 $totalSize = $doneSize; 315 $break = true; 316 } 317 } 318 319 // Add the currently downloaded frag to the total size of downloaded files 320 if ($result) 321 { 322 $filesize = strlen($result); 323 //debugMsg("-- Successful download of $filesize bytes"); 324 $doneSize += $filesize; 325 326 // Append the file 327 $fp = @fopen($local_file, 'ab'); 328 329 if ($fp === false) 330 { 331 //debugMsg("-- Can't open local file $local_file for writing"); 332 // Can't open the file for writing 333 $retArray['status'] = false; 334 $retArray['error'] = JText::sprintf('LIB_FOF_DOWNLOAD_ERR_COULDNOTWRITELOCALFILE', $local_file); 335 336 return $retArray; 337 } 338 339 fwrite($fp, $result); 340 fclose($fp); 341 342 //debugMsg("-- Appended data to local file $local_file"); 343 344 $frag++; 345 346 //debugMsg("-- Proceeding to next fragment, frag $frag"); 347 348 if (($filesize < $length) || ($filesize > $length)) 349 { 350 // A partial download or a download larger than the frag size means we are done 351 $frag = -1; 352 //debugMsg("-- Import complete (partial download of last frag)"); 353 $totalSize = $doneSize; 354 $break = true; 355 } 356 } 357 358 // Advance the frag pointer and mark the end 359 $end = $timer->getRunningTime(); 360 361 // Do we predict that we have enough time? 362 $required_time = max(1.1 * ($end - $start), $required_time); 363 364 if ($required_time > (10 - $end + $start)) 365 { 366 $break = true; 367 } 368 369 $start = $end; 370 } 371 372 if ($frag == -1) 373 { 374 $percent = 100; 375 } 376 elseif ($doneSize <= 0) 377 { 378 $percent = 0; 379 } 380 else 381 { 382 if ($totalSize > 0) 383 { 384 $percent = 100 * ($doneSize / $totalSize); 385 } 386 else 387 { 388 $percent = 0; 389 } 390 } 391 392 // Update $retArray 393 $retArray = array( 394 "status" => true, 395 "error" => '', 396 "frag" => $frag, 397 "totalSize" => $totalSize, 398 "doneSize" => $doneSize, 399 "percent" => $percent, 400 ); 401 } 402 catch (Exception $e) 403 { 404 //debugMsg("EXCEPTION RAISED:"); 405 //debugMsg($e->getMessage()); 406 $retArray['status'] = false; 407 $retArray['error'] = $e->getMessage(); 408 } 409 410 return $retArray; 411 } 412 413 /** 414 * This method will crawl a starting directory and get all the valid files 415 * that will be analyzed by __construct. Then it organizes them into an 416 * associative array. 417 * 418 * @param string $path Folder where we should start looking 419 * @param array $ignoreFolders Folder ignore list 420 * @param array $ignoreFiles File ignore list 421 * 422 * @return array Associative array, where the `fullpath` key contains the path to the file, 423 * and the `classname` key contains the name of the class 424 */ 425 protected static function getFiles($path, array $ignoreFolders = array(), array $ignoreFiles = array()) 426 { 427 $return = array(); 428 429 $files = self::scanDirectory($path, $ignoreFolders, $ignoreFiles); 430 431 // Ok, I got the files, now I have to organize them 432 foreach ($files as $file) 433 { 434 $clean = str_replace($path, '', $file); 435 $clean = trim(str_replace('\\', '/', $clean), '/'); 436 437 $parts = explode('/', $clean); 438 439 $return[] = array( 440 'fullpath' => $file, 441 'classname' => 'FOFDownloadAdapter' . ucfirst(basename($parts[0], '.php')) 442 ); 443 } 444 445 return $return; 446 } 447 448 /** 449 * Recursive function that will scan every directory unless it's in the 450 * ignore list. Files that aren't in the ignore list are returned. 451 * 452 * @param string $path Folder where we should start looking 453 * @param array $ignoreFolders Folder ignore list 454 * @param array $ignoreFiles File ignore list 455 * 456 * @return array List of all the files 457 */ 458 protected static function scanDirectory($path, array $ignoreFolders = array(), array $ignoreFiles = array()) 459 { 460 $return = array(); 461 462 $handle = @opendir($path); 463 464 if ( !$handle) 465 { 466 return $return; 467 } 468 469 while (($file = readdir($handle)) !== false) 470 { 471 if ($file == '.' || $file == '..') 472 { 473 continue; 474 } 475 476 $fullpath = $path . '/' . $file; 477 478 if ((is_dir($fullpath) && in_array($file, $ignoreFolders)) || (is_file($fullpath) && in_array($file, $ignoreFiles))) 479 { 480 continue; 481 } 482 483 if (is_dir($fullpath)) 484 { 485 $return = array_merge(self::scanDirectory($fullpath, $ignoreFolders, $ignoreFiles), $return); 486 } 487 else 488 { 489 $return[] = $path . '/' . $file; 490 } 491 } 492 493 return $return; 494 } 495}