1<?php
2/**
3 * Joomla! Content Management System
4 *
5 * @copyright  Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved.
6 * @license    GNU General Public License version 2 or later; see LICENSE.txt
7 */
8
9namespace Joomla\CMS\Installer;
10
11defined('JPATH_PLATFORM') or die;
12
13use Joomla\CMS\Application\ApplicationHelper;
14use Joomla\CMS\Plugin\PluginHelper;
15use Joomla\CMS\Table\Extension;
16use Joomla\CMS\Table\Table;
17
18\JLoader::import('joomla.filesystem.file');
19\JLoader::import('joomla.filesystem.folder');
20\JLoader::import('joomla.filesystem.path');
21\JLoader::import('joomla.base.adapter');
22
23/**
24 * Joomla base installer class
25 *
26 * @since  3.1
27 */
28class Installer extends \JAdapter
29{
30	/**
31	 * Array of paths needed by the installer
32	 *
33	 * @var    array
34	 * @since  3.1
35	 */
36	protected $paths = array();
37
38	/**
39	 * True if package is an upgrade
40	 *
41	 * @var    boolean
42	 * @since  3.1
43	 */
44	protected $upgrade = null;
45
46	/**
47	 * The manifest trigger class
48	 *
49	 * @var    object
50	 * @since  3.1
51	 */
52	public $manifestClass = null;
53
54	/**
55	 * True if existing files can be overwritten
56	 *
57	 * @var    boolean
58	 * @since  3.0.0
59	 */
60	protected $overwrite = false;
61
62	/**
63	 * Stack of installation steps
64	 * - Used for installation rollback
65	 *
66	 * @var    array
67	 * @since  3.1
68	 */
69	protected $stepStack = array();
70
71	/**
72	 * Extension Table Entry
73	 *
74	 * @var    Extension
75	 * @since  3.1
76	 */
77	public $extension = null;
78
79	/**
80	 * The output from the install/uninstall scripts
81	 *
82	 * @var    string
83	 * @since  3.1
84	 * */
85	public $message = null;
86
87	/**
88	 * The installation manifest XML object
89	 *
90	 * @var    object
91	 * @since  3.1
92	 */
93	public $manifest = null;
94
95	/**
96	 * The extension message that appears
97	 *
98	 * @var    string
99	 * @since  3.1
100	 */
101	protected $extension_message = null;
102
103	/**
104	 * The redirect URL if this extension (can be null if no redirect)
105	 *
106	 * @var    string
107	 * @since  3.1
108	 */
109	protected $redirect_url = null;
110
111	/**
112	 * Flag if the uninstall process was triggered by uninstalling a package
113	 *
114	 * @var    boolean
115	 * @since  3.7.0
116	 */
117	protected $packageUninstall = false;
118
119	/**
120	 * Installer instance container.
121	 *
122	 * @var    Installer
123	 * @since  3.1
124	 * @deprecated  4.0
125	 */
126	protected static $instance;
127
128	/**
129	 * Installer instances container.
130	 *
131	 * @var    Installer[]
132	 * @since  3.4
133	 */
134	protected static $instances;
135
136	/**
137	 * Constructor
138	 *
139	 * @param   string  $basepath       Base Path of the adapters
140	 * @param   string  $classprefix    Class prefix of adapters
141	 * @param   string  $adapterfolder  Name of folder to append to base path
142	 *
143	 * @since   3.1
144	 */
145	public function __construct($basepath = __DIR__, $classprefix = '\\Joomla\\CMS\\Installer\\Adapter', $adapterfolder = 'Adapter')
146	{
147		parent::__construct($basepath, $classprefix, $adapterfolder);
148
149		$this->extension = Table::getInstance('extension');
150	}
151
152	/**
153	 * Returns the global Installer object, only creating it if it doesn't already exist.
154	 *
155	 * @param   string  $basepath       Base Path of the adapters
156	 * @param   string  $classprefix    Class prefix of adapters
157	 * @param   string  $adapterfolder  Name of folder to append to base path
158	 *
159	 * @return  Installer  An installer object
160	 *
161	 * @since   3.1
162	 */
163	public static function getInstance($basepath = __DIR__, $classprefix = '\\Joomla\\CMS\\Installer\\Adapter', $adapterfolder = 'Adapter')
164	{
165		if (!isset(self::$instances[$basepath]))
166		{
167			self::$instances[$basepath] = new Installer($basepath, $classprefix, $adapterfolder);
168
169			// For B/C, we load the first instance into the static $instance container, remove at 4.0
170			if (!isset(self::$instance))
171			{
172				self::$instance = self::$instances[$basepath];
173			}
174		}
175
176		return self::$instances[$basepath];
177	}
178
179	/**
180	 * Get the allow overwrite switch
181	 *
182	 * @return  boolean  Allow overwrite switch
183	 *
184	 * @since   3.1
185	 */
186	public function isOverwrite()
187	{
188		return $this->overwrite;
189	}
190
191	/**
192	 * Set the allow overwrite switch
193	 *
194	 * @param   boolean  $state  Overwrite switch state
195	 *
196	 * @return  boolean  True it state is set, false if it is not
197	 *
198	 * @since   3.1
199	 */
200	public function setOverwrite($state = false)
201	{
202		$tmp = $this->overwrite;
203
204		if ($state)
205		{
206			$this->overwrite = true;
207		}
208		else
209		{
210			$this->overwrite = false;
211		}
212
213		return $tmp;
214	}
215
216	/**
217	 * Get the redirect location
218	 *
219	 * @return  string  Redirect location (or null)
220	 *
221	 * @since   3.1
222	 */
223	public function getRedirectUrl()
224	{
225		return $this->redirect_url;
226	}
227
228	/**
229	 * Set the redirect location
230	 *
231	 * @param   string  $newurl  New redirect location
232	 *
233	 * @return  void
234	 *
235	 * @since   3.1
236	 */
237	public function setRedirectUrl($newurl)
238	{
239		$this->redirect_url = $newurl;
240	}
241
242	/**
243	 * Get whether this installer is uninstalling extensions which are part of a package
244	 *
245	 * @return  boolean
246	 *
247	 * @since   3.7.0
248	 */
249	public function isPackageUninstall()
250	{
251		return $this->packageUninstall;
252	}
253
254	/**
255	 * Set whether this installer is uninstalling extensions which are part of a package
256	 *
257	 * @param   boolean  $uninstall  True if a package triggered the uninstall, false otherwise
258	 *
259	 * @return  void
260	 *
261	 * @since   3.7.0
262	 */
263	public function setPackageUninstall($uninstall)
264	{
265		$this->packageUninstall = $uninstall;
266	}
267
268	/**
269	 * Get the upgrade switch
270	 *
271	 * @return  boolean
272	 *
273	 * @since   3.1
274	 */
275	public function isUpgrade()
276	{
277		return $this->upgrade;
278	}
279
280	/**
281	 * Set the upgrade switch
282	 *
283	 * @param   boolean  $state  Upgrade switch state
284	 *
285	 * @return  boolean  True if upgrade, false otherwise
286	 *
287	 * @since   3.1
288	 */
289	public function setUpgrade($state = false)
290	{
291		$tmp = $this->upgrade;
292
293		if ($state)
294		{
295			$this->upgrade = true;
296		}
297		else
298		{
299			$this->upgrade = false;
300		}
301
302		return $tmp;
303	}
304
305	/**
306	 * Get the installation manifest object
307	 *
308	 * @return  \SimpleXMLElement  Manifest object
309	 *
310	 * @since   3.1
311	 */
312	public function getManifest()
313	{
314		if (!is_object($this->manifest))
315		{
316			$this->findManifest();
317		}
318
319		return $this->manifest;
320	}
321
322	/**
323	 * Get an installer path by name
324	 *
325	 * @param   string  $name     Path name
326	 * @param   string  $default  Default value
327	 *
328	 * @return  string  Path
329	 *
330	 * @since   3.1
331	 */
332	public function getPath($name, $default = null)
333	{
334		return (!empty($this->paths[$name])) ? $this->paths[$name] : $default;
335	}
336
337	/**
338	 * Sets an installer path by name
339	 *
340	 * @param   string  $name   Path name
341	 * @param   string  $value  Path
342	 *
343	 * @return  void
344	 *
345	 * @since   3.1
346	 */
347	public function setPath($name, $value)
348	{
349		$this->paths[$name] = $value;
350	}
351
352	/**
353	 * Pushes a step onto the installer stack for rolling back steps
354	 *
355	 * @param   array  $step  Installer step
356	 *
357	 * @return  void
358	 *
359	 * @since   3.1
360	 */
361	public function pushStep($step)
362	{
363		$this->stepStack[] = $step;
364	}
365
366	/**
367	 * Installation abort method
368	 *
369	 * @param   string  $msg   Abort message from the installer
370	 * @param   string  $type  Package type if defined
371	 *
372	 * @return  boolean  True if successful
373	 *
374	 * @since   3.1
375	 */
376	public function abort($msg = null, $type = null)
377	{
378		$retval = true;
379		$step = array_pop($this->stepStack);
380
381		// Raise abort warning
382		if ($msg)
383		{
384			\JLog::add($msg, \JLog::WARNING, 'jerror');
385		}
386
387		while ($step != null)
388		{
389			switch ($step['type'])
390			{
391				case 'file':
392					// Remove the file
393					$stepval = \JFile::delete($step['path']);
394					break;
395
396				case 'folder':
397					// Remove the folder
398					$stepval = \JFolder::delete($step['path']);
399					break;
400
401				case 'query':
402					// Execute the query.
403					$stepval = $this->parseSQLFiles($step['script']);
404					break;
405
406				case 'extension':
407					// Get database connector object
408					$db = $this->getDbo();
409					$query = $db->getQuery(true);
410
411					// Remove the entry from the #__extensions table
412					$query->delete($db->quoteName('#__extensions'))
413						->where($db->quoteName('extension_id') . ' = ' . (int) $step['id']);
414					$db->setQuery($query);
415
416					try
417					{
418						$db->execute();
419
420						$stepval = true;
421					}
422					catch (\JDatabaseExceptionExecuting $e)
423					{
424						// The database API will have already logged the error it caught, we just need to alert the user to the issue
425						\JLog::add(\JText::_('JLIB_INSTALLER_ABORT_ERROR_DELETING_EXTENSIONS_RECORD'), \JLog::WARNING, 'jerror');
426
427						$stepval = false;
428					}
429
430					break;
431
432				default:
433					if ($type && is_object($this->_adapters[$type]))
434					{
435						// Build the name of the custom rollback method for the type
436						$method = '_rollback_' . $step['type'];
437
438						// Custom rollback method handler
439						if (method_exists($this->_adapters[$type], $method))
440						{
441							$stepval = $this->_adapters[$type]->$method($step);
442						}
443					}
444					else
445					{
446						// Set it to false
447						$stepval = false;
448					}
449					break;
450			}
451
452			// Only set the return value if it is false
453			if ($stepval === false)
454			{
455				$retval = false;
456			}
457
458			// Get the next step and continue
459			$step = array_pop($this->stepStack);
460		}
461
462		return $retval;
463	}
464
465	// Adapter functions
466
467	/**
468	 * Package installation method
469	 *
470	 * @param   string  $path  Path to package source folder
471	 *
472	 * @return  boolean  True if successful
473	 *
474	 * @since   3.1
475	 */
476	public function install($path = null)
477	{
478		if ($path && \JFolder::exists($path))
479		{
480			$this->setPath('source', $path);
481		}
482		else
483		{
484			$this->abort(\JText::_('JLIB_INSTALLER_ABORT_NOINSTALLPATH'));
485
486			return false;
487		}
488
489		if (!$adapter = $this->setupInstall('install', true))
490		{
491			$this->abort(\JText::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST'));
492
493			return false;
494		}
495
496		if (!is_object($adapter))
497		{
498			return false;
499		}
500
501		// Add the languages from the package itself
502		if (method_exists($adapter, 'loadLanguage'))
503		{
504			$adapter->loadLanguage($path);
505		}
506
507		// Fire the onExtensionBeforeInstall event.
508		PluginHelper::importPlugin('extension');
509		$dispatcher = \JEventDispatcher::getInstance();
510		$dispatcher->trigger(
511			'onExtensionBeforeInstall',
512			array(
513				'method' => 'install',
514				'type' => $this->manifest->attributes()->type,
515				'manifest' => $this->manifest,
516				'extension' => 0,
517			)
518		);
519
520		// Run the install
521		$result = $adapter->install();
522
523		// Fire the onExtensionAfterInstall
524		$dispatcher->trigger(
525			'onExtensionAfterInstall',
526			array('installer' => clone $this, 'eid' => $result)
527		);
528
529		if ($result !== false)
530		{
531			// Refresh versionable assets cache
532			\JFactory::getApplication()->flushAssets();
533
534			return true;
535		}
536
537		return false;
538	}
539
540	/**
541	 * Discovered package installation method
542	 *
543	 * @param   integer  $eid  Extension ID
544	 *
545	 * @return  boolean  True if successful
546	 *
547	 * @since   3.1
548	 */
549	public function discover_install($eid = null)
550	{
551		if (!$eid)
552		{
553			$this->abort(\JText::_('JLIB_INSTALLER_ABORT_EXTENSIONNOTVALID'));
554
555			return false;
556		}
557
558		if (!$this->extension->load($eid))
559		{
560			$this->abort(\JText::_('JLIB_INSTALLER_ABORT_LOAD_DETAILS'));
561
562			return false;
563		}
564
565		if ($this->extension->state != -1)
566		{
567			$this->abort(\JText::_('JLIB_INSTALLER_ABORT_ALREADYINSTALLED'));
568
569			return false;
570		}
571
572		// Load the adapter(s) for the install manifest
573		$type   = $this->extension->type;
574		$params = array('extension' => $this->extension, 'route' => 'discover_install');
575
576		$adapter = $this->getAdapter($type, $params);
577
578		if (!is_object($adapter))
579		{
580			return false;
581		}
582
583		if (!method_exists($adapter, 'discover_install') || !$adapter->getDiscoverInstallSupported())
584		{
585			$this->abort(\JText::sprintf('JLIB_INSTALLER_ERROR_DISCOVER_INSTALL_UNSUPPORTED', $type));
586
587			return false;
588		}
589
590		// The adapter needs to prepare itself
591		if (method_exists($adapter, 'prepareDiscoverInstall'))
592		{
593			try
594			{
595				$adapter->prepareDiscoverInstall();
596			}
597			catch (\RuntimeException $e)
598			{
599				$this->abort($e->getMessage());
600
601				return false;
602			}
603		}
604
605		// Add the languages from the package itself
606		if (method_exists($adapter, 'loadLanguage'))
607		{
608			$adapter->loadLanguage();
609		}
610
611		// Fire the onExtensionBeforeInstall event.
612		PluginHelper::importPlugin('extension');
613		$dispatcher = \JEventDispatcher::getInstance();
614		$dispatcher->trigger(
615			'onExtensionBeforeInstall',
616			array(
617				'method' => 'discover_install',
618				'type' => $this->extension->get('type'),
619				'manifest' => null,
620				'extension' => $this->extension->get('extension_id'),
621			)
622		);
623
624		// Run the install
625		$result = $adapter->discover_install();
626
627		// Fire the onExtensionAfterInstall
628		$dispatcher->trigger(
629			'onExtensionAfterInstall',
630			array('installer' => clone $this, 'eid' => $result)
631		);
632
633		if ($result !== false)
634		{
635			// Refresh versionable assets cache
636			\JFactory::getApplication()->flushAssets();
637
638			return true;
639		}
640
641		return false;
642	}
643
644	/**
645	 * Extension discover method
646	 *
647	 * Asks each adapter to find extensions
648	 *
649	 * @return  InstallerExtension[]
650	 *
651	 * @since   3.1
652	 */
653	public function discover()
654	{
655		$this->loadAllAdapters();
656		$results = array();
657
658		foreach ($this->_adapters as $adapter)
659		{
660			// Joomla! 1.5 installation adapter legacy support
661			if (method_exists($adapter, 'discover'))
662			{
663				$tmp = $adapter->discover();
664
665				// If its an array and has entries
666				if (is_array($tmp) && count($tmp))
667				{
668					// Merge it into the system
669					$results = array_merge($results, $tmp);
670				}
671			}
672		}
673
674		return $results;
675	}
676
677	/**
678	 * Package update method
679	 *
680	 * @param   string  $path  Path to package source folder
681	 *
682	 * @return  boolean  True if successful
683	 *
684	 * @since   3.1
685	 */
686	public function update($path = null)
687	{
688		if ($path && \JFolder::exists($path))
689		{
690			$this->setPath('source', $path);
691		}
692		else
693		{
694			$this->abort(\JText::_('JLIB_INSTALLER_ABORT_NOUPDATEPATH'));
695
696			return false;
697		}
698
699		if (!$adapter = $this->setupInstall('update', true))
700		{
701			$this->abort(\JText::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST'));
702
703			return false;
704		}
705
706		if (!is_object($adapter))
707		{
708			return false;
709		}
710
711		// Add the languages from the package itself
712		if (method_exists($adapter, 'loadLanguage'))
713		{
714			$adapter->loadLanguage($path);
715		}
716
717		// Fire the onExtensionBeforeUpdate event.
718		PluginHelper::importPlugin('extension');
719		$dispatcher = \JEventDispatcher::getInstance();
720		$dispatcher->trigger('onExtensionBeforeUpdate', array('type' => $this->manifest->attributes()->type, 'manifest' => $this->manifest));
721
722		// Run the update
723		$result = $adapter->update();
724
725		// Fire the onExtensionAfterUpdate
726		$dispatcher->trigger(
727			'onExtensionAfterUpdate',
728			array('installer' => clone $this, 'eid' => $result)
729		);
730
731		if ($result !== false)
732		{
733			return true;
734		}
735
736		return false;
737	}
738
739	/**
740	 * Package uninstallation method
741	 *
742	 * @param   string   $type        Package type
743	 * @param   mixed    $identifier  Package identifier for adapter
744	 * @param   integer  $cid         Application ID; deprecated in 1.6
745	 *
746	 * @return  boolean  True if successful
747	 *
748	 * @since   3.1
749	 */
750	public function uninstall($type, $identifier, $cid = 0)
751	{
752		$params = array('extension' => $this->extension, 'route' => 'uninstall');
753
754		$adapter = $this->getAdapter($type, $params);
755
756		if (!is_object($adapter))
757		{
758			return false;
759		}
760
761		// We don't load languages here, we get the extension adapter to work it out
762		// Fire the onExtensionBeforeUninstall event.
763		PluginHelper::importPlugin('extension');
764		$dispatcher = \JEventDispatcher::getInstance();
765		$dispatcher->trigger('onExtensionBeforeUninstall', array('eid' => $identifier));
766
767		// Run the uninstall
768		$result = $adapter->uninstall($identifier);
769
770		// Fire the onExtensionAfterInstall
771		$dispatcher->trigger(
772			'onExtensionAfterUninstall',
773			array('installer' => clone $this, 'eid' => $identifier, 'result' => $result)
774		);
775
776		// Refresh versionable assets cache
777		\JFactory::getApplication()->flushAssets();
778
779		return $result;
780	}
781
782	/**
783	 * Refreshes the manifest cache stored in #__extensions
784	 *
785	 * @param   integer  $eid  Extension ID
786	 *
787	 * @return  boolean
788	 *
789	 * @since   3.1
790	 */
791	public function refreshManifestCache($eid)
792	{
793		if ($eid)
794		{
795			if (!$this->extension->load($eid))
796			{
797				$this->abort(\JText::_('JLIB_INSTALLER_ABORT_LOAD_DETAILS'));
798
799				return false;
800			}
801
802			if ($this->extension->state == -1)
803			{
804				$this->abort(\JText::sprintf('JLIB_INSTALLER_ABORT_REFRESH_MANIFEST_CACHE', $this->extension->name));
805
806				return false;
807			}
808
809			// Fetch the adapter
810			$adapter = $this->getAdapter($this->extension->type);
811
812			if (!is_object($adapter))
813			{
814				return false;
815			}
816
817			if (!method_exists($adapter, 'refreshManifestCache'))
818			{
819				$this->abort(\JText::sprintf('JLIB_INSTALLER_ABORT_METHODNOTSUPPORTED_TYPE', $this->extension->type));
820
821				return false;
822			}
823
824			$result = $adapter->refreshManifestCache();
825
826			if ($result !== false)
827			{
828				return true;
829			}
830			else
831			{
832				return false;
833			}
834		}
835
836		$this->abort(\JText::_('JLIB_INSTALLER_ABORT_REFRESH_MANIFEST_CACHE_VALID'));
837
838		return false;
839	}
840
841	// Utility functions
842
843	/**
844	 * Prepare for installation: this method sets the installation directory, finds
845	 * and checks the installation file and verifies the installation type.
846	 *
847	 * @param   string   $route          The install route being followed
848	 * @param   boolean  $returnAdapter  Flag to return the instantiated adapter
849	 *
850	 * @return  boolean|InstallerAdapter  InstallerAdapter object if explicitly requested otherwise boolean
851	 *
852	 * @since   3.1
853	 */
854	public function setupInstall($route = 'install', $returnAdapter = false)
855	{
856		// We need to find the installation manifest file
857		if (!$this->findManifest())
858		{
859			return false;
860		}
861
862		// Load the adapter(s) for the install manifest
863		$type   = (string) $this->manifest->attributes()->type;
864		$params = array('route' => $route, 'manifest' => $this->getManifest());
865
866		// Load the adapter
867		$adapter = $this->getAdapter($type, $params);
868
869		if ($returnAdapter)
870		{
871			return $adapter;
872		}
873
874		return true;
875	}
876
877	/**
878	 * Backward compatible method to parse through a queries element of the
879	 * installation manifest file and take appropriate action.
880	 *
881	 * @param   \SimpleXMLElement  $element  The XML node to process
882	 *
883	 * @return  mixed  Number of queries processed or False on error
884	 *
885	 * @since   3.1
886	 */
887	public function parseQueries(\SimpleXMLElement $element)
888	{
889		// Get the database connector object
890		$db = & $this->_db;
891
892		if (!$element || !count($element->children()))
893		{
894			// Either the tag does not exist or has no children therefore we return zero files processed.
895			return 0;
896		}
897
898		// Get the array of query nodes to process
899		$queries = $element->children();
900
901		if (count($queries) === 0)
902		{
903			// No queries to process
904			return 0;
905		}
906
907		$update_count = 0;
908
909		// Process each query in the $queries array (children of $tagName).
910		foreach ($queries as $query)
911		{
912			$db->setQuery($db->convertUtf8mb4QueryToUtf8($query));
913
914			try
915			{
916				$db->execute();
917			}
918			catch (\JDatabaseExceptionExecuting $e)
919			{
920				\JLog::add(\JText::sprintf('JLIB_INSTALLER_ERROR_SQL_ERROR', $e->getMessage()), \JLog::WARNING, 'jerror');
921
922				return false;
923			}
924
925			$update_count++;
926		}
927
928		return $update_count;
929	}
930
931	/**
932	 * Method to extract the name of a discreet installation sql file from the installation manifest file.
933	 *
934	 * @param   object  $element  The XML node to process
935	 *
936	 * @return  mixed  Number of queries processed or False on error
937	 *
938	 * @since   3.1
939	 */
940	public function parseSQLFiles($element)
941	{
942		if (!$element || !count($element->children()))
943		{
944			// The tag does not exist.
945			return 0;
946		}
947
948		$db = & $this->_db;
949
950		// TODO - At 4.0 we can change this to use `getServerType()` since SQL Server will not be supported
951		$dbDriver = strtolower($db->name);
952
953		if ($db->getServerType() === 'mysql')
954		{
955			$dbDriver = 'mysql';
956		}
957		elseif ($db->getServerType() === 'postgresql')
958		{
959			$dbDriver = 'postgresql';
960		}
961
962		$update_count = 0;
963
964		// Get the name of the sql file to process
965		foreach ($element->children() as $file)
966		{
967			$fCharset = strtolower($file->attributes()->charset) === 'utf8' ? 'utf8' : '';
968			$fDriver  = strtolower($file->attributes()->driver);
969
970			if ($fDriver === 'mysqli' || $fDriver === 'pdomysql')
971			{
972				$fDriver = 'mysql';
973			}
974			elseif ($fDriver === 'pgsql')
975			{
976				$fDriver = 'postgresql';
977			}
978
979			if ($fCharset === 'utf8' && $fDriver == $dbDriver)
980			{
981				$sqlfile = $this->getPath('extension_root') . '/' . trim($file);
982
983				// Check that sql files exists before reading. Otherwise raise error for rollback
984				if (!file_exists($sqlfile))
985				{
986					\JLog::add(\JText::sprintf('JLIB_INSTALLER_ERROR_SQL_FILENOTFOUND', $sqlfile), \JLog::WARNING, 'jerror');
987
988					return false;
989				}
990
991				$buffer = file_get_contents($sqlfile);
992
993				// Graceful exit and rollback if read not successful
994				if ($buffer === false)
995				{
996					\JLog::add(\JText::_('JLIB_INSTALLER_ERROR_SQL_READBUFFER'), \JLog::WARNING, 'jerror');
997
998					return false;
999				}
1000
1001				// Create an array of queries from the sql file
1002				$queries = \JDatabaseDriver::splitSql($buffer);
1003
1004				if (count($queries) === 0)
1005				{
1006					// No queries to process
1007					continue;
1008				}
1009
1010				// Process each query in the $queries array (split out of sql file).
1011				foreach ($queries as $query)
1012				{
1013					$db->setQuery($db->convertUtf8mb4QueryToUtf8($query));
1014
1015					try
1016					{
1017						$db->execute();
1018					}
1019					catch (\JDatabaseExceptionExecuting $e)
1020					{
1021						\JLog::add(\JText::sprintf('JLIB_INSTALLER_ERROR_SQL_ERROR', $e->getMessage()), \JLog::WARNING, 'jerror');
1022
1023						return false;
1024					}
1025
1026					$update_count++;
1027				}
1028			}
1029		}
1030
1031		return $update_count;
1032	}
1033
1034	/**
1035	 * Set the schema version for an extension by looking at its latest update
1036	 *
1037	 * @param   \SimpleXMLElement  $schema  Schema Tag
1038	 * @param   integer            $eid     Extension ID
1039	 *
1040	 * @return  void
1041	 *
1042	 * @since   3.1
1043	 */
1044	public function setSchemaVersion(\SimpleXMLElement $schema, $eid)
1045	{
1046		if ($eid && $schema)
1047		{
1048			$db = \JFactory::getDbo();
1049			$schemapaths = $schema->children();
1050
1051			if (!$schemapaths)
1052			{
1053				return;
1054			}
1055
1056			if (count($schemapaths))
1057			{
1058				$dbDriver = strtolower($db->name);
1059
1060				if ($db->getServerType() === 'mysql')
1061				{
1062					$dbDriver = 'mysql';
1063				}
1064				elseif ($db->getServerType() === 'postgresql')
1065				{
1066					$dbDriver = 'postgresql';
1067				}
1068
1069				$schemapath = '';
1070
1071				foreach ($schemapaths as $entry)
1072				{
1073					$attrs = $entry->attributes();
1074
1075					if ($attrs['type'] == $dbDriver)
1076					{
1077						$schemapath = $entry;
1078						break;
1079					}
1080				}
1081
1082				if ($schemapath !== '')
1083				{
1084					$files = str_replace('.sql', '', \JFolder::files($this->getPath('extension_root') . '/' . $schemapath, '\.sql$'));
1085					usort($files, 'version_compare');
1086
1087					// Update the database
1088					$query = $db->getQuery(true)
1089						->delete('#__schemas')
1090						->where('extension_id = ' . $eid);
1091					$db->setQuery($query);
1092
1093					if ($db->execute())
1094					{
1095						$query->clear()
1096							->insert($db->quoteName('#__schemas'))
1097							->columns(array($db->quoteName('extension_id'), $db->quoteName('version_id')))
1098							->values($eid . ', ' . $db->quote(end($files)));
1099						$db->setQuery($query);
1100						$db->execute();
1101					}
1102				}
1103			}
1104		}
1105	}
1106
1107	/**
1108	 * Method to process the updates for an item
1109	 *
1110	 * @param   \SimpleXMLElement  $schema  The XML node to process
1111	 * @param   integer            $eid     Extension Identifier
1112	 *
1113	 * @return  boolean           Result of the operations
1114	 *
1115	 * @since   3.1
1116	 */
1117	public function parseSchemaUpdates(\SimpleXMLElement $schema, $eid)
1118	{
1119		$update_count = 0;
1120
1121		// Ensure we have an XML element and a valid extension id
1122		if ($eid && $schema)
1123		{
1124			$db = \JFactory::getDbo();
1125			$schemapaths = $schema->children();
1126
1127			if (count($schemapaths))
1128			{
1129				// TODO - At 4.0 we can change this to use `getServerType()` since SQL Server will not be supported
1130				$dbDriver = strtolower($db->name);
1131
1132				if ($db->getServerType() === 'mysql')
1133				{
1134					$dbDriver = 'mysql';
1135				}
1136				elseif ($db->getServerType() === 'postgresql')
1137				{
1138					$dbDriver = 'postgresql';
1139				}
1140
1141				$schemapath = '';
1142
1143				foreach ($schemapaths as $entry)
1144				{
1145					$attrs = $entry->attributes();
1146
1147					// Assuming that the type is a mandatory attribute but if it is not mandatory then there should be a discussion for it.
1148					$uDriver = strtolower($attrs['type']);
1149
1150					if ($uDriver === 'mysqli' || $uDriver === 'pdomysql')
1151					{
1152						$uDriver = 'mysql';
1153					}
1154					elseif ($uDriver === 'pgsql')
1155					{
1156						$uDriver = 'postgresql';
1157					}
1158
1159					if ($uDriver == $dbDriver)
1160					{
1161						$schemapath = $entry;
1162						break;
1163					}
1164				}
1165
1166				if ($schemapath !== '')
1167				{
1168					$files = \JFolder::files($this->getPath('extension_root') . '/' . $schemapath, '\.sql$');
1169
1170					if (empty($files))
1171					{
1172						return $update_count;
1173					}
1174
1175					$files = str_replace('.sql', '', $files);
1176					usort($files, 'version_compare');
1177
1178					$query = $db->getQuery(true)
1179						->select('version_id')
1180						->from('#__schemas')
1181						->where('extension_id = ' . $eid);
1182					$db->setQuery($query);
1183					$version = $db->loadResult();
1184
1185					// No version - use initial version.
1186					if (!$version)
1187					{
1188						$version = '0.0.0';
1189					}
1190
1191					foreach ($files as $file)
1192					{
1193						if (version_compare($file, $version) > 0)
1194						{
1195							$buffer = file_get_contents($this->getPath('extension_root') . '/' . $schemapath . '/' . $file . '.sql');
1196
1197							// Graceful exit and rollback if read not successful
1198							if ($buffer === false)
1199							{
1200								\JLog::add(\JText::sprintf('JLIB_INSTALLER_ERROR_SQL_READBUFFER'), \JLog::WARNING, 'jerror');
1201
1202								return false;
1203							}
1204
1205							// Create an array of queries from the sql file
1206							$queries = \JDatabaseDriver::splitSql($buffer);
1207
1208							if (count($queries) === 0)
1209							{
1210								// No queries to process
1211								continue;
1212							}
1213
1214							// Process each query in the $queries array (split out of sql file).
1215							foreach ($queries as $query)
1216							{
1217								$db->setQuery($db->convertUtf8mb4QueryToUtf8($query));
1218
1219								try
1220								{
1221									$db->execute();
1222								}
1223								catch (\JDatabaseExceptionExecuting $e)
1224								{
1225									\JLog::add(\JText::sprintf('JLIB_INSTALLER_ERROR_SQL_ERROR', $e->getMessage()), \JLog::WARNING, 'jerror');
1226
1227									return false;
1228								}
1229
1230								$queryString = (string) $query;
1231								$queryString = str_replace(array("\r", "\n"), array('', ' '), substr($queryString, 0, 80));
1232								\JLog::add(\JText::sprintf('JLIB_INSTALLER_UPDATE_LOG_QUERY', $file, $queryString), \JLog::INFO, 'Update');
1233
1234								$update_count++;
1235							}
1236						}
1237					}
1238
1239					// Update the database
1240					$query = $db->getQuery(true)
1241						->delete('#__schemas')
1242						->where('extension_id = ' . $eid);
1243					$db->setQuery($query);
1244
1245					if ($db->execute())
1246					{
1247						$query->clear()
1248							->insert($db->quoteName('#__schemas'))
1249							->columns(array($db->quoteName('extension_id'), $db->quoteName('version_id')))
1250							->values($eid . ', ' . $db->quote(end($files)));
1251						$db->setQuery($query);
1252						$db->execute();
1253					}
1254				}
1255			}
1256		}
1257
1258		return $update_count;
1259	}
1260
1261	/**
1262	 * Method to parse through a files element of the installation manifest and take appropriate
1263	 * action.
1264	 *
1265	 * @param   \SimpleXMLElement  $element   The XML node to process
1266	 * @param   integer            $cid       Application ID of application to install to
1267	 * @param   array              $oldFiles  List of old files (SimpleXMLElement's)
1268	 * @param   array              $oldMD5    List of old MD5 sums (indexed by filename with value as MD5)
1269	 *
1270	 * @return  boolean      True on success
1271	 *
1272	 * @since   3.1
1273	 */
1274	public function parseFiles(\SimpleXMLElement $element, $cid = 0, $oldFiles = null, $oldMD5 = null)
1275	{
1276		// Get the array of file nodes to process; we checked whether this had children above.
1277		if (!$element || !count($element->children()))
1278		{
1279			// Either the tag does not exist or has no children (hence no files to process) therefore we return zero files processed.
1280			return 0;
1281		}
1282
1283		$copyfiles = array();
1284
1285		// Get the client info
1286		$client = ApplicationHelper::getClientInfo($cid);
1287
1288		/*
1289		 * Here we set the folder we are going to remove the files from.
1290		 */
1291		if ($client)
1292		{
1293			$pathname = 'extension_' . $client->name;
1294			$destination = $this->getPath($pathname);
1295		}
1296		else
1297		{
1298			$pathname = 'extension_root';
1299			$destination = $this->getPath($pathname);
1300		}
1301
1302		/*
1303		 * Here we set the folder we are going to copy the files from.
1304		 *
1305		 * Does the element have a folder attribute?
1306		 *
1307		 * If so this indicates that the files are in a subdirectory of the source
1308		 * folder and we should append the folder attribute to the source path when
1309		 * copying files.
1310		 */
1311
1312		$folder = (string) $element->attributes()->folder;
1313
1314		if ($folder && file_exists($this->getPath('source') . '/' . $folder))
1315		{
1316			$source = $this->getPath('source') . '/' . $folder;
1317		}
1318		else
1319		{
1320			$source = $this->getPath('source');
1321		}
1322
1323		// Work out what files have been deleted
1324		if ($oldFiles && ($oldFiles instanceof \SimpleXMLElement))
1325		{
1326			$oldEntries = $oldFiles->children();
1327
1328			if (count($oldEntries))
1329			{
1330				$deletions = $this->findDeletedFiles($oldEntries, $element->children());
1331
1332				foreach ($deletions['folders'] as $deleted_folder)
1333				{
1334					\JFolder::delete($destination . '/' . $deleted_folder);
1335				}
1336
1337				foreach ($deletions['files'] as $deleted_file)
1338				{
1339					\JFile::delete($destination . '/' . $deleted_file);
1340				}
1341			}
1342		}
1343
1344		$path = array();
1345
1346		// Copy the MD5SUMS file if it exists
1347		if (file_exists($source . '/MD5SUMS'))
1348		{
1349			$path['src'] = $source . '/MD5SUMS';
1350			$path['dest'] = $destination . '/MD5SUMS';
1351			$path['type'] = 'file';
1352			$copyfiles[] = $path;
1353		}
1354
1355		// Process each file in the $files array (children of $tagName).
1356		foreach ($element->children() as $file)
1357		{
1358			$path['src'] = $source . '/' . $file;
1359			$path['dest'] = $destination . '/' . $file;
1360
1361			// Is this path a file or folder?
1362			$path['type'] = $file->getName() === 'folder' ? 'folder' : 'file';
1363
1364			/*
1365			 * Before we can add a file to the copyfiles array we need to ensure
1366			 * that the folder we are copying our file to exits and if it doesn't,
1367			 * we need to create it.
1368			 */
1369
1370			if (basename($path['dest']) !== $path['dest'])
1371			{
1372				$newdir = dirname($path['dest']);
1373
1374				if (!\JFolder::create($newdir))
1375				{
1376					\JLog::add(\JText::sprintf('JLIB_INSTALLER_ERROR_CREATE_DIRECTORY', $newdir), \JLog::WARNING, 'jerror');
1377
1378					return false;
1379				}
1380			}
1381
1382			// Add the file to the copyfiles array
1383			$copyfiles[] = $path;
1384		}
1385
1386		return $this->copyFiles($copyfiles);
1387	}
1388
1389	/**
1390	 * Method to parse through a languages element of the installation manifest and take appropriate
1391	 * action.
1392	 *
1393	 * @param   \SimpleXMLElement  $element  The XML node to process
1394	 * @param   integer            $cid      Application ID of application to install to
1395	 *
1396	 * @return  boolean  True on success
1397	 *
1398	 * @since   3.1
1399	 */
1400	public function parseLanguages(\SimpleXMLElement $element, $cid = 0)
1401	{
1402		// TODO: work out why the below line triggers 'node no longer exists' errors with files
1403		if (!$element || !count($element->children()))
1404		{
1405			// Either the tag does not exist or has no children therefore we return zero files processed.
1406			return 0;
1407		}
1408
1409		$copyfiles = array();
1410
1411		// Get the client info
1412		$client = ApplicationHelper::getClientInfo($cid);
1413
1414		// Here we set the folder we are going to copy the files to.
1415		// 'languages' Files are copied to JPATH_BASE/language/ folder
1416
1417		$destination = $client->path . '/language';
1418
1419		/*
1420		 * Here we set the folder we are going to copy the files from.
1421		 *
1422		 * Does the element have a folder attribute?
1423		 *
1424		 * If so this indicates that the files are in a subdirectory of the source
1425		 * folder and we should append the folder attribute to the source path when
1426		 * copying files.
1427		 */
1428
1429		$folder = (string) $element->attributes()->folder;
1430
1431		if ($folder && file_exists($this->getPath('source') . '/' . $folder))
1432		{
1433			$source = $this->getPath('source') . '/' . $folder;
1434		}
1435		else
1436		{
1437			$source = $this->getPath('source');
1438		}
1439
1440		// Process each file in the $files array (children of $tagName).
1441		foreach ($element->children() as $file)
1442		{
1443			/*
1444			 * Language files go in a subfolder based on the language code, ie.
1445			 * <language tag="en-US">en-US.mycomponent.ini</language>
1446			 * would go in the en-US subdirectory of the language folder.
1447			 */
1448
1449			// We will only install language files where a core language pack
1450			// already exists.
1451
1452			if ((string) $file->attributes()->tag !== '')
1453			{
1454				$path['src'] = $source . '/' . $file;
1455
1456				if ((string) $file->attributes()->client !== '')
1457				{
1458					// Override the client
1459					$langclient = ApplicationHelper::getClientInfo((string) $file->attributes()->client, true);
1460					$path['dest'] = $langclient->path . '/language/' . $file->attributes()->tag . '/' . basename((string) $file);
1461				}
1462				else
1463				{
1464					// Use the default client
1465					$path['dest'] = $destination . '/' . $file->attributes()->tag . '/' . basename((string) $file);
1466				}
1467
1468				// If the language folder is not present, then the core pack hasn't been installed... ignore
1469				if (!\JFolder::exists(dirname($path['dest'])))
1470				{
1471					continue;
1472				}
1473			}
1474			else
1475			{
1476				$path['src'] = $source . '/' . $file;
1477				$path['dest'] = $destination . '/' . $file;
1478			}
1479
1480			/*
1481			 * Before we can add a file to the copyfiles array we need to ensure
1482			 * that the folder we are copying our file to exits and if it doesn't,
1483			 * we need to create it.
1484			 */
1485
1486			if (basename($path['dest']) !== $path['dest'])
1487			{
1488				$newdir = dirname($path['dest']);
1489
1490				if (!\JFolder::create($newdir))
1491				{
1492					\JLog::add(\JText::sprintf('JLIB_INSTALLER_ERROR_CREATE_DIRECTORY', $newdir), \JLog::WARNING, 'jerror');
1493
1494					return false;
1495				}
1496			}
1497
1498			// Add the file to the copyfiles array
1499			$copyfiles[] = $path;
1500		}
1501
1502		return $this->copyFiles($copyfiles);
1503	}
1504
1505	/**
1506	 * Method to parse through a media element of the installation manifest and take appropriate
1507	 * action.
1508	 *
1509	 * @param   \SimpleXMLElement  $element  The XML node to process
1510	 * @param   integer            $cid      Application ID of application to install to
1511	 *
1512	 * @return  boolean     True on success
1513	 *
1514	 * @since   3.1
1515	 */
1516	public function parseMedia(\SimpleXMLElement $element, $cid = 0)
1517	{
1518		if (!$element || !count($element->children()))
1519		{
1520			// Either the tag does not exist or has no children therefore we return zero files processed.
1521			return 0;
1522		}
1523
1524		$copyfiles = array();
1525
1526		// Here we set the folder we are going to copy the files to.
1527		// Default 'media' Files are copied to the JPATH_BASE/media folder
1528
1529		$folder = ((string) $element->attributes()->destination) ? '/' . $element->attributes()->destination : null;
1530		$destination = \JPath::clean(JPATH_ROOT . '/media' . $folder);
1531
1532		// Here we set the folder we are going to copy the files from.
1533
1534		/*
1535		 * Does the element have a folder attribute?
1536		 * If so this indicates that the files are in a subdirectory of the source
1537		 * folder and we should append the folder attribute to the source path when
1538		 * copying files.
1539		 */
1540
1541		$folder = (string) $element->attributes()->folder;
1542
1543		if ($folder && file_exists($this->getPath('source') . '/' . $folder))
1544		{
1545			$source = $this->getPath('source') . '/' . $folder;
1546		}
1547		else
1548		{
1549			$source = $this->getPath('source');
1550		}
1551
1552		// Process each file in the $files array (children of $tagName).
1553		foreach ($element->children() as $file)
1554		{
1555			$path['src'] = $source . '/' . $file;
1556			$path['dest'] = $destination . '/' . $file;
1557
1558			// Is this path a file or folder?
1559			$path['type'] = $file->getName() === 'folder' ? 'folder' : 'file';
1560
1561			/*
1562			 * Before we can add a file to the copyfiles array we need to ensure
1563			 * that the folder we are copying our file to exits and if it doesn't,
1564			 * we need to create it.
1565			 */
1566
1567			if (basename($path['dest']) !== $path['dest'])
1568			{
1569				$newdir = dirname($path['dest']);
1570
1571				if (!\JFolder::create($newdir))
1572				{
1573					\JLog::add(\JText::sprintf('JLIB_INSTALLER_ERROR_CREATE_DIRECTORY', $newdir), \JLog::WARNING, 'jerror');
1574
1575					return false;
1576				}
1577			}
1578
1579			// Add the file to the copyfiles array
1580			$copyfiles[] = $path;
1581		}
1582
1583		return $this->copyFiles($copyfiles);
1584	}
1585
1586	/**
1587	 * Method to parse the parameters of an extension, build the JSON string for its default parameters, and return the JSON string.
1588	 *
1589	 * @return  string  JSON string of parameter values
1590	 *
1591	 * @since   3.1
1592	 * @note    This method must always return a JSON compliant string
1593	 */
1594	public function getParams()
1595	{
1596		// Validate that we have a fieldset to use
1597		if (!isset($this->manifest->config->fields->fieldset))
1598		{
1599			return '{}';
1600		}
1601
1602		// Getting the fieldset tags
1603		$fieldsets = $this->manifest->config->fields->fieldset;
1604
1605		// Creating the data collection variable:
1606		$ini = array();
1607
1608		// Iterating through the fieldsets:
1609		foreach ($fieldsets as $fieldset)
1610		{
1611			if (!count($fieldset->children()))
1612			{
1613				// Either the tag does not exist or has no children therefore we return zero files processed.
1614				return '{}';
1615			}
1616
1617			// Iterating through the fields and collecting the name/default values:
1618			foreach ($fieldset as $field)
1619			{
1620				// Check against the null value since otherwise default values like "0"
1621				// cause entire parameters to be skipped.
1622
1623				if (($name = $field->attributes()->name) === null)
1624				{
1625					continue;
1626				}
1627
1628				if (($value = $field->attributes()->default) === null)
1629				{
1630					continue;
1631				}
1632
1633				$ini[(string) $name] = (string) $value;
1634			}
1635		}
1636
1637		return json_encode($ini);
1638	}
1639
1640	/**
1641	 * Copyfiles
1642	 *
1643	 * Copy files from source directory to the target directory
1644	 *
1645	 * @param   array    $files      Array with filenames
1646	 * @param   boolean  $overwrite  True if existing files can be replaced
1647	 *
1648	 * @return  boolean  True on success
1649	 *
1650	 * @since   3.1
1651	 */
1652	public function copyFiles($files, $overwrite = null)
1653	{
1654		/*
1655		 * To allow for manual override on the overwriting flag, we check to see if
1656		 * the $overwrite flag was set and is a boolean value.  If not, use the object
1657		 * allowOverwrite flag.
1658		 */
1659
1660		if ($overwrite === null || !is_bool($overwrite))
1661		{
1662			$overwrite = $this->overwrite;
1663		}
1664
1665		/*
1666		 * $files must be an array of filenames.  Verify that it is an array with
1667		 * at least one file to copy.
1668		 */
1669		if (is_array($files) && count($files) > 0)
1670		{
1671			foreach ($files as $file)
1672			{
1673				// Get the source and destination paths
1674				$filesource = \JPath::clean($file['src']);
1675				$filedest = \JPath::clean($file['dest']);
1676				$filetype = array_key_exists('type', $file) ? $file['type'] : 'file';
1677
1678				if (!file_exists($filesource))
1679				{
1680					/*
1681					 * The source file does not exist.  Nothing to copy so set an error
1682					 * and return false.
1683					 */
1684					\JLog::add(\JText::sprintf('JLIB_INSTALLER_ERROR_NO_FILE', $filesource), \JLog::WARNING, 'jerror');
1685
1686					return false;
1687				}
1688				elseif (($exists = file_exists($filedest)) && !$overwrite)
1689				{
1690					// It's okay if the manifest already exists
1691					if ($this->getPath('manifest') === $filesource)
1692					{
1693						continue;
1694					}
1695
1696					// The destination file already exists and the overwrite flag is false.
1697					// Set an error and return false.
1698					\JLog::add(\JText::sprintf('JLIB_INSTALLER_ERROR_FILE_EXISTS', $filedest), \JLog::WARNING, 'jerror');
1699
1700					return false;
1701				}
1702				else
1703				{
1704					// Copy the folder or file to the new location.
1705					if ($filetype === 'folder')
1706					{
1707						if (!\JFolder::copy($filesource, $filedest, null, $overwrite))
1708						{
1709							\JLog::add(\JText::sprintf('JLIB_INSTALLER_ERROR_FAIL_COPY_FOLDER', $filesource, $filedest), \JLog::WARNING, 'jerror');
1710
1711							return false;
1712						}
1713
1714						$step = array('type' => 'folder', 'path' => $filedest);
1715					}
1716					else
1717					{
1718						if (!\JFile::copy($filesource, $filedest, null))
1719						{
1720							\JLog::add(\JText::sprintf('JLIB_INSTALLER_ERROR_FAIL_COPY_FILE', $filesource, $filedest), \JLog::WARNING, 'jerror');
1721
1722							// In 3.2, TinyMCE language handling changed.  Display a special notice in case an older language pack is installed.
1723							if (strpos($filedest, 'media/editors/tinymce/jscripts/tiny_mce/langs'))
1724							{
1725								\JLog::add(\JText::_('JLIB_INSTALLER_NOT_ERROR'), \JLog::WARNING, 'jerror');
1726							}
1727
1728							return false;
1729						}
1730
1731						$step = array('type' => 'file', 'path' => $filedest);
1732					}
1733
1734					/*
1735					 * Since we copied a file/folder, we want to add it to the installation step stack so that
1736					 * in case we have to roll back the installation we can remove the files copied.
1737					 */
1738					if (!$exists)
1739					{
1740						$this->stepStack[] = $step;
1741					}
1742				}
1743			}
1744		}
1745		else
1746		{
1747			// The $files variable was either not an array or an empty array
1748			return false;
1749		}
1750
1751		return count($files);
1752	}
1753
1754	/**
1755	 * Method to parse through a files element of the installation manifest and remove
1756	 * the files that were installed
1757	 *
1758	 * @param   object   $element  The XML node to process
1759	 * @param   integer  $cid      Application ID of application to remove from
1760	 *
1761	 * @return  boolean  True on success
1762	 *
1763	 * @since   3.1
1764	 */
1765	public function removeFiles($element, $cid = 0)
1766	{
1767		if (!$element || !count($element->children()))
1768		{
1769			// Either the tag does not exist or has no children therefore we return zero files processed.
1770			return true;
1771		}
1772
1773		$retval = true;
1774
1775		// Get the client info if we're using a specific client
1776		if ($cid > -1)
1777		{
1778			$client = ApplicationHelper::getClientInfo($cid);
1779		}
1780		else
1781		{
1782			$client = null;
1783		}
1784
1785		// Get the array of file nodes to process
1786		$files = $element->children();
1787
1788		if (count($files) === 0)
1789		{
1790			// No files to process
1791			return true;
1792		}
1793
1794		$folder = '';
1795
1796		/*
1797		 * Here we set the folder we are going to remove the files from.  There are a few
1798		 * special cases that need to be considered for certain reserved tags.
1799		 */
1800		switch ($element->getName())
1801		{
1802			case 'media':
1803				if ((string) $element->attributes()->destination)
1804				{
1805					$folder = (string) $element->attributes()->destination;
1806				}
1807				else
1808				{
1809					$folder = '';
1810				}
1811
1812				$source = $client->path . '/media/' . $folder;
1813
1814				break;
1815
1816			case 'languages':
1817				$lang_client = (string) $element->attributes()->client;
1818
1819				if ($lang_client)
1820				{
1821					$client = ApplicationHelper::getClientInfo($lang_client, true);
1822					$source = $client->path . '/language';
1823				}
1824				else
1825				{
1826					if ($client)
1827					{
1828						$source = $client->path . '/language';
1829					}
1830					else
1831					{
1832						$source = '';
1833					}
1834				}
1835
1836				break;
1837
1838			default:
1839				if ($client)
1840				{
1841					$pathname = 'extension_' . $client->name;
1842					$source = $this->getPath($pathname);
1843				}
1844				else
1845				{
1846					$pathname = 'extension_root';
1847					$source = $this->getPath($pathname);
1848				}
1849
1850				break;
1851		}
1852
1853		// Process each file in the $files array (children of $tagName).
1854		foreach ($files as $file)
1855		{
1856			/*
1857			 * If the file is a language, we must handle it differently.  Language files
1858			 * go in a subdirectory based on the language code, ie.
1859			 * <language tag="en_US">en_US.mycomponent.ini</language>
1860			 * would go in the en_US subdirectory of the languages directory.
1861			 */
1862
1863			if ($file->getName() === 'language' && (string) $file->attributes()->tag !== '')
1864			{
1865				if ($source)
1866				{
1867					$path = $source . '/' . $file->attributes()->tag . '/' . basename((string) $file);
1868				}
1869				else
1870				{
1871					$target_client = ApplicationHelper::getClientInfo((string) $file->attributes()->client, true);
1872					$path = $target_client->path . '/language/' . $file->attributes()->tag . '/' . basename((string) $file);
1873				}
1874
1875				// If the language folder is not present, then the core pack hasn't been installed... ignore
1876				if (!\JFolder::exists(dirname($path)))
1877				{
1878					continue;
1879				}
1880			}
1881			else
1882			{
1883				$path = $source . '/' . $file;
1884			}
1885
1886			// Actually delete the files/folders
1887
1888			if (is_dir($path))
1889			{
1890				$val = \JFolder::delete($path);
1891			}
1892			else
1893			{
1894				$val = \JFile::delete($path);
1895			}
1896
1897			if ($val === false)
1898			{
1899				\JLog::add('Failed to delete ' . $path, \JLog::WARNING, 'jerror');
1900				$retval = false;
1901			}
1902		}
1903
1904		if (!empty($folder))
1905		{
1906			\JFolder::delete($source);
1907		}
1908
1909		return $retval;
1910	}
1911
1912	/**
1913	 * Copies the installation manifest file to the extension folder in the given client
1914	 *
1915	 * @param   integer  $cid  Where to copy the installfile [optional: defaults to 1 (admin)]
1916	 *
1917	 * @return  boolean  True on success, False on error
1918	 *
1919	 * @since   3.1
1920	 */
1921	public function copyManifest($cid = 1)
1922	{
1923		// Get the client info
1924		$client = ApplicationHelper::getClientInfo($cid);
1925
1926		$path['src'] = $this->getPath('manifest');
1927
1928		if ($client)
1929		{
1930			$pathname = 'extension_' . $client->name;
1931			$path['dest'] = $this->getPath($pathname) . '/' . basename($this->getPath('manifest'));
1932		}
1933		else
1934		{
1935			$pathname = 'extension_root';
1936			$path['dest'] = $this->getPath($pathname) . '/' . basename($this->getPath('manifest'));
1937		}
1938
1939		return $this->copyFiles(array($path), true);
1940	}
1941
1942	/**
1943	 * Tries to find the package manifest file
1944	 *
1945	 * @return  boolean  True on success, False on error
1946	 *
1947	 * @since   3.1
1948	 */
1949	public function findManifest()
1950	{
1951		// Do nothing if folder does not exist for some reason
1952		if (!\JFolder::exists($this->getPath('source')))
1953		{
1954			return false;
1955		}
1956
1957		// Main folder manifests (higher priority)
1958		$parentXmlfiles = \JFolder::files($this->getPath('source'), '.xml$', false, true);
1959
1960		// Search for children manifests (lower priority)
1961		$allXmlFiles    = \JFolder::files($this->getPath('source'), '.xml$', 1, true);
1962
1963		// Create an unique array of files ordered by priority
1964		$xmlfiles = array_unique(array_merge($parentXmlfiles, $allXmlFiles));
1965
1966		// If at least one XML file exists
1967		if (!empty($xmlfiles))
1968		{
1969			foreach ($xmlfiles as $file)
1970			{
1971				// Is it a valid Joomla installation manifest file?
1972				$manifest = $this->isManifest($file);
1973
1974				if ($manifest !== null)
1975				{
1976					// If the root method attribute is set to upgrade, allow file overwrite
1977					if ((string) $manifest->attributes()->method === 'upgrade')
1978					{
1979						$this->upgrade = true;
1980						$this->overwrite = true;
1981					}
1982
1983					// If the overwrite option is set, allow file overwriting
1984					if ((string) $manifest->attributes()->overwrite === 'true')
1985					{
1986						$this->overwrite = true;
1987					}
1988
1989					// Set the manifest object and path
1990					$this->manifest = $manifest;
1991					$this->setPath('manifest', $file);
1992
1993					// Set the installation source path to that of the manifest file
1994					$this->setPath('source', dirname($file));
1995
1996					return true;
1997				}
1998			}
1999
2000			// None of the XML files found were valid install files
2001			\JLog::add(\JText::_('JLIB_INSTALLER_ERROR_NOTFINDJOOMLAXMLSETUPFILE'), \JLog::WARNING, 'jerror');
2002
2003			return false;
2004		}
2005		else
2006		{
2007			// No XML files were found in the install folder
2008			\JLog::add(\JText::_('JLIB_INSTALLER_ERROR_NOTFINDXMLSETUPFILE'), \JLog::WARNING, 'jerror');
2009
2010			return false;
2011		}
2012	}
2013
2014	/**
2015	 * Is the XML file a valid Joomla installation manifest file.
2016	 *
2017	 * @param   string  $file  An xmlfile path to check
2018	 *
2019	 * @return  \SimpleXMLElement|null  A \SimpleXMLElement, or null if the file failed to parse
2020	 *
2021	 * @since   3.1
2022	 */
2023	public function isManifest($file)
2024	{
2025		$xml = simplexml_load_file($file);
2026
2027		// If we cannot load the XML file return null
2028		if (!$xml)
2029		{
2030			return;
2031		}
2032
2033		// Check for a valid XML root tag.
2034		if ($xml->getName() !== 'extension')
2035		{
2036			return;
2037		}
2038
2039		// Valid manifest file return the object
2040		return $xml;
2041	}
2042
2043	/**
2044	 * Generates a manifest cache
2045	 *
2046	 * @return string serialised manifest data
2047	 *
2048	 * @since   3.1
2049	 */
2050	public function generateManifestCache()
2051	{
2052		return json_encode(self::parseXMLInstallFile($this->getPath('manifest')));
2053	}
2054
2055	/**
2056	 * Cleans up discovered extensions if they're being installed some other way
2057	 *
2058	 * @param   string   $type     The type of extension (component, etc)
2059	 * @param   string   $element  Unique element identifier (e.g. com_content)
2060	 * @param   string   $folder   The folder of the extension (plugins; e.g. system)
2061	 * @param   integer  $client   The client application (administrator or site)
2062	 *
2063	 * @return  object    Result of query
2064	 *
2065	 * @since   3.1
2066	 */
2067	public function cleanDiscoveredExtension($type, $element, $folder = '', $client = 0)
2068	{
2069		$db = \JFactory::getDbo();
2070		$query = $db->getQuery(true)
2071			->delete($db->quoteName('#__extensions'))
2072			->where('type = ' . $db->quote($type))
2073			->where('element = ' . $db->quote($element))
2074			->where('folder = ' . $db->quote($folder))
2075			->where('client_id = ' . (int) $client)
2076			->where('state = -1');
2077		$db->setQuery($query);
2078
2079		return $db->execute();
2080	}
2081
2082	/**
2083	 * Compares two "files" entries to find deleted files/folders
2084	 *
2085	 * @param   array  $oldFiles  An array of \SimpleXMLElement objects that are the old files
2086	 * @param   array  $newFiles  An array of \SimpleXMLElement objects that are the new files
2087	 *
2088	 * @return  array  An array with the delete files and folders in findDeletedFiles[files] and findDeletedFiles[folders] respectively
2089	 *
2090	 * @since   3.1
2091	 */
2092	public function findDeletedFiles($oldFiles, $newFiles)
2093	{
2094		// The magic find deleted files function!
2095		// The files that are new
2096		$files = array();
2097
2098		// The folders that are new
2099		$folders = array();
2100
2101		// The folders of the files that are new
2102		$containers = array();
2103
2104		// A list of files to delete
2105		$files_deleted = array();
2106
2107		// A list of folders to delete
2108		$folders_deleted = array();
2109
2110		foreach ($newFiles as $file)
2111		{
2112			switch ($file->getName())
2113			{
2114				case 'folder':
2115					// Add any folders to the list
2116					$folders[] = (string) $file; // add any folders to the list
2117					break;
2118
2119				case 'file':
2120				default:
2121					// Add any files to the list
2122					$files[] = (string) $file;
2123
2124					// Now handle the folder part of the file to ensure we get any containers
2125					// Break up the parts of the directory
2126					$container_parts = explode('/', dirname((string) $file));
2127
2128					// Make sure this is clean and empty
2129					$container = '';
2130
2131					foreach ($container_parts as $part)
2132					{
2133						// Iterate through each part
2134						// Add a slash if its not empty
2135						if (!empty($container))
2136						{
2137							$container .= '/';
2138						}
2139
2140						// Aappend the folder part
2141						$container .= $part;
2142
2143						if (!in_array($container, $containers))
2144						{
2145							// Add the container if it doesn't already exist
2146							$containers[] = $container;
2147						}
2148					}
2149					break;
2150			}
2151		}
2152
2153		foreach ($oldFiles as $file)
2154		{
2155			switch ($file->getName())
2156			{
2157				case 'folder':
2158					if (!in_array((string) $file, $folders))
2159					{
2160						// See whether the folder exists in the new list
2161						if (!in_array((string) $file, $containers))
2162						{
2163							// Check if the folder exists as a container in the new list
2164							// If it's not in the new list or a container then delete it
2165							$folders_deleted[] = (string) $file;
2166						}
2167					}
2168					break;
2169
2170				case 'file':
2171				default:
2172					if (!in_array((string) $file, $files))
2173					{
2174						// Look if the file exists in the new list
2175						if (!in_array(dirname((string) $file), $folders))
2176						{
2177							// Look if the file is now potentially in a folder
2178							$files_deleted[] = (string) $file; // not in a folder, doesn't exist, wipe it out!
2179						}
2180					}
2181					break;
2182			}
2183		}
2184
2185		return array('files' => $files_deleted, 'folders' => $folders_deleted);
2186	}
2187
2188	/**
2189	 * Loads an MD5SUMS file into an associative array
2190	 *
2191	 * @param   string  $filename  Filename to load
2192	 *
2193	 * @return  array  Associative array with filenames as the index and the MD5 as the value
2194	 *
2195	 * @since   3.1
2196	 */
2197	public function loadMD5Sum($filename)
2198	{
2199		if (!file_exists($filename))
2200		{
2201			// Bail if the file doesn't exist
2202			return false;
2203		}
2204
2205		$data = file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2206		$retval = array();
2207
2208		foreach ($data as $row)
2209		{
2210			// Split up the data
2211			$results = explode('  ', $row);
2212
2213			// Cull any potential prefix
2214			$results[1] = str_replace('./', '', $results[1]);
2215
2216			// Throw into the array
2217			$retval[$results[1]] = $results[0];
2218		}
2219
2220		return $retval;
2221	}
2222
2223	/**
2224	 * Parse a XML install manifest file.
2225	 *
2226	 * XML Root tag should be 'install' except for languages which use meta file.
2227	 *
2228	 * @param   string  $path  Full path to XML file.
2229	 *
2230	 * @return  array  XML metadata.
2231	 *
2232	 * @since   3.0.0
2233	 */
2234	public static function parseXMLInstallFile($path)
2235	{
2236		// Check if xml file exists.
2237		if (!file_exists($path))
2238		{
2239			return false;
2240		}
2241
2242		// Read the file to see if it's a valid component XML file
2243		$xml = simplexml_load_file($path);
2244
2245		if (!$xml)
2246		{
2247			return false;
2248		}
2249
2250		// Check for a valid XML root tag.
2251
2252		// Extensions use 'extension' as the root tag.  Languages use 'metafile' instead
2253
2254		$name = $xml->getName();
2255
2256		if ($name !== 'extension' && $name !== 'metafile')
2257		{
2258			unset($xml);
2259
2260			return false;
2261		}
2262
2263		$data = array();
2264
2265		$data['name'] = (string) $xml->name;
2266
2267		// Check if we're a language. If so use metafile.
2268		$data['type'] = $xml->getName() === 'metafile' ? 'language' : (string) $xml->attributes()->type;
2269
2270		$data['creationDate'] = ((string) $xml->creationDate) ?: \JText::_('JLIB_UNKNOWN');
2271		$data['author'] = ((string) $xml->author) ?: \JText::_('JLIB_UNKNOWN');
2272
2273		$data['copyright'] = (string) $xml->copyright;
2274		$data['authorEmail'] = (string) $xml->authorEmail;
2275		$data['authorUrl'] = (string) $xml->authorUrl;
2276		$data['version'] = (string) $xml->version;
2277		$data['description'] = (string) $xml->description;
2278		$data['group'] = (string) $xml->group;
2279
2280		if ($xml->files && count($xml->files->children()))
2281		{
2282			$filename = \JFile::getName($path);
2283			$data['filename'] = \JFile::stripExt($filename);
2284
2285			foreach ($xml->files->children() as $oneFile)
2286			{
2287				if ((string) $oneFile->attributes()->plugin)
2288				{
2289					$data['filename'] = (string) $oneFile->attributes()->plugin;
2290					break;
2291				}
2292			}
2293		}
2294
2295		return $data;
2296	}
2297
2298	/**
2299	 * Fetches an adapter and adds it to the internal storage if an instance is not set
2300	 * while also ensuring its a valid adapter name
2301	 *
2302	 * @param   string  $name     Name of adapter to return
2303	 * @param   array   $options  Adapter options
2304	 *
2305	 * @return  InstallerAdapter
2306	 *
2307	 * @since       3.4
2308	 * @deprecated  4.0  The internal adapter cache will no longer be supported,
2309	 *                   use loadAdapter() to fetch an adapter instance
2310	 */
2311	public function getAdapter($name, $options = array())
2312	{
2313		$this->getAdapters($options);
2314
2315		if (!$this->setAdapter($name, $this->_adapters[$name]))
2316		{
2317			return false;
2318		}
2319
2320		return $this->_adapters[$name];
2321	}
2322
2323	/**
2324	 * Gets a list of available install adapters.
2325	 *
2326	 * @param   array  $options  An array of options to inject into the adapter
2327	 * @param   array  $custom   Array of custom install adapters
2328	 *
2329	 * @return  array  An array of available install adapters.
2330	 *
2331	 * @since   3.4
2332	 * @note    As of 4.0, this method will only return the names of available adapters and will not
2333	 *          instantiate them and store to the $_adapters class var.
2334	 */
2335	public function getAdapters($options = array(), array $custom = array())
2336	{
2337		$files = new \DirectoryIterator($this->_basepath . '/' . $this->_adapterfolder);
2338
2339		// Process the core adapters
2340		foreach ($files as $file)
2341		{
2342			$fileName = $file->getFilename();
2343
2344			// Only load for php files.
2345			if (!$file->isFile() || $file->getExtension() !== 'php')
2346			{
2347				continue;
2348			}
2349
2350			// Derive the class name from the filename.
2351			$name  = str_ireplace('.php', '', trim($fileName));
2352			$name  = str_ireplace('adapter', '', trim($name));
2353			$class = rtrim($this->_classprefix, '\\') . '\\' . ucfirst($name) . 'Adapter';
2354
2355			if (!class_exists($class))
2356			{
2357				// Not namespaced
2358				$class = $this->_classprefix . ucfirst($name);
2359			}
2360
2361			// Core adapters should autoload based on classname, keep this fallback just in case
2362			if (!class_exists($class))
2363			{
2364				// Try to load the adapter object
2365				\JLoader::register($class, $this->_basepath . '/' . $this->_adapterfolder . '/' . $fileName);
2366
2367				if (!class_exists($class))
2368				{
2369					// Skip to next one
2370					continue;
2371				}
2372			}
2373
2374			$this->_adapters[strtolower($name)] = $this->loadAdapter($name, $options);
2375		}
2376
2377		// Add any custom adapters if specified
2378		if (count($custom) >= 1)
2379		{
2380			foreach ($custom as $adapter)
2381			{
2382				// Setup the class name
2383				// TODO - Can we abstract this to not depend on the Joomla class namespace without PHP namespaces?
2384				$class = $this->_classprefix . ucfirst(trim($adapter));
2385
2386				// If the class doesn't exist we have nothing left to do but look at the next type. We did our best.
2387				if (!class_exists($class))
2388				{
2389					continue;
2390				}
2391
2392				$this->_adapters[$name] = $this->loadAdapter($name, $options);
2393			}
2394		}
2395
2396		return $this->_adapters;
2397	}
2398
2399	/**
2400	 * Method to load an adapter instance
2401	 *
2402	 * @param   string  $adapter  Adapter name
2403	 * @param   array   $options  Adapter options
2404	 *
2405	 * @return  InstallerAdapter
2406	 *
2407	 * @since   3.4
2408	 * @throws  \InvalidArgumentException
2409	 */
2410	public function loadAdapter($adapter, $options = array())
2411	{
2412		$class = rtrim($this->_classprefix, '\\') . '\\' . ucfirst($adapter) . 'Adapter';
2413
2414		if (!class_exists($class))
2415		{
2416			// Not namespaced
2417			$class = $this->_classprefix . ucfirst($adapter);
2418		}
2419
2420		if (!class_exists($class))
2421		{
2422			// @deprecated 4.0 - The adapter should be autoloaded or manually included by the caller
2423			$path = $this->_basepath . '/' . $this->_adapterfolder . '/' . $adapter . '.php';
2424
2425			// Try to load the adapter object
2426			if (!file_exists($path))
2427			{
2428				throw new \InvalidArgumentException(sprintf('The %s install adapter does not exist.', $adapter));
2429			}
2430
2431			// Try once more to find the class
2432			\JLoader::register($class, $path);
2433
2434			if (!class_exists($class))
2435			{
2436				throw new \InvalidArgumentException(sprintf('The %s install adapter does not exist.', $adapter));
2437			}
2438		}
2439
2440		// Ensure the adapter type is part of the options array
2441		$options['type'] = $adapter;
2442
2443		return new $class($this, $this->getDbo(), $options);
2444	}
2445
2446	/**
2447	 * Loads all adapters.
2448	 *
2449	 * @param   array  $options  Adapter options
2450	 *
2451	 * @return  void
2452	 *
2453	 * @since       3.4
2454	 * @deprecated  4.0  Individual adapters should be instantiated as needed
2455	 * @note        This method is serving as a proxy of the legacy \JAdapter API into the preferred API
2456	 */
2457	public function loadAllAdapters($options = array())
2458	{
2459		$this->getAdapters($options);
2460	}
2461}
2462