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}