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