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
9 defined('FOF_INCLUDED') or die;
10
11 class 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 }