PageRenderTime 52ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/libraries/joomla/installer/installer.php

https://github.com/joebushi/joomla
PHP | 1551 lines | 869 code | 157 blank | 525 comment | 179 complexity | a3186c1f641df5156ec473203c9fd83f MD5 | raw file
Possible License(s): LGPL-2.1, Apache-2.0
  1. <?php
  2. /**
  3. * @version $Id$
  4. * @copyright Copyright (C) 2005 - 2010 Open Source Matters, Inc. All rights reserved.
  5. * @license GNU General Public License version 2 or later; see LICENSE.txt
  6. */
  7. // No direct access
  8. defined('JPATH_BASE') or die;
  9. jimport('joomla.filesystem.file');
  10. jimport('joomla.filesystem.folder');
  11. jimport('joomla.filesystem.archive');
  12. jimport('joomla.filesystem.path');
  13. jimport('joomla.base.adapter');
  14. /**
  15. * Joomla base installer class
  16. *
  17. * @package Joomla.Framework
  18. * @subpackage Installer
  19. * @since 1.5
  20. */
  21. class JInstaller extends JAdapter
  22. {
  23. /**
  24. * Array of paths needed by the installer
  25. * @var array
  26. */
  27. protected $_paths = array();
  28. /**
  29. * True if packakge is an upgrade
  30. * @var boolean
  31. */
  32. protected $_upgrade = null;
  33. /**
  34. * The manifest trigger class
  35. * @var object
  36. */
  37. public $manifestClass = null;
  38. /**
  39. * True if existing files can be overwritten
  40. * @var boolean
  41. */
  42. protected $_overwrite = false;
  43. /**
  44. * Stack of installation steps
  45. * - Used for installation rollback
  46. * @var array
  47. */
  48. protected $_stepStack = array();
  49. /**
  50. * Extension Table Entry
  51. * @var JTableExtension
  52. */
  53. public $extension = null;
  54. /**
  55. * The output from the install/uninstall scripts
  56. * @var string
  57. */
  58. public $message = null;
  59. /**
  60. * The installation manifest XML object
  61. * @var object
  62. */
  63. public $manifest = null;
  64. protected $extension_message = null;
  65. /**
  66. * Constructor
  67. *
  68. * @access protected
  69. */
  70. public function __construct()
  71. {
  72. parent::__construct(dirname(__FILE__),'JInstaller');
  73. }
  74. /**
  75. * Returns the global Installer object, only creating it
  76. * if it doesn't already exist.
  77. *
  78. * @static
  79. * @return object An installer object
  80. * @since 1.5
  81. */
  82. public static function getInstance()
  83. {
  84. static $instance;
  85. if (!isset ($instance)) {
  86. $instance = new JInstaller();
  87. }
  88. return $instance;
  89. }
  90. /**
  91. * Get the allow overwrite switch
  92. *
  93. * @access public
  94. * @return boolean Allow overwrite switch
  95. * @since 1.5
  96. */
  97. public function getOverwrite()
  98. {
  99. return $this->_overwrite;
  100. }
  101. /**
  102. * Set the allow overwrite switch
  103. *
  104. * @access public
  105. * @param boolean $state Overwrite switch state
  106. * @return boolean Previous value
  107. * @since 1.5
  108. */
  109. public function setOverwrite($state=false)
  110. {
  111. $tmp = $this->_overwrite;
  112. if ($state) {
  113. $this->_overwrite = true;
  114. }
  115. else {
  116. $this->_overwrite = false;
  117. }
  118. return $tmp;
  119. }
  120. /**
  121. * Get the allow overwrite switch
  122. *
  123. * @access public
  124. * @return boolean Allow overwrite switch
  125. * @since 1.5
  126. */
  127. function getUpgrade()
  128. {
  129. return $this->_upgrade;
  130. }
  131. /**
  132. * Set the allow overwrite switch
  133. *
  134. * @access public
  135. * @param boolean $state Overwrite switch state
  136. * @return boolean Previous value
  137. * @since 1.5
  138. */
  139. function setUpgrade($state=false)
  140. {
  141. $tmp = $this->_upgrade;
  142. if ($state) {
  143. $this->_upgrade = true;
  144. }
  145. else {
  146. $this->_upgrade = false;
  147. }
  148. return $tmp;
  149. }
  150. /**
  151. * Get the installation manifest object
  152. *
  153. * @access public
  154. * @return object Manifest object
  155. * @since 1.5
  156. */
  157. public function getManifest()
  158. {
  159. if (!is_object($this->manifest)) {
  160. $this->findManifest();
  161. }
  162. return $this->manifest;
  163. }
  164. /**
  165. * Get an installer path by name
  166. *
  167. * @access public
  168. * @param string $name Path name
  169. * @param string $default Default value
  170. * @return string Path
  171. * @since 1.5
  172. */
  173. public function getPath($name, $default=null)
  174. {
  175. return (!empty($this->_paths[$name])) ? $this->_paths[$name] : $default;
  176. }
  177. /**
  178. * Sets an installer path by name
  179. *
  180. * @access public
  181. * @param string $name Path name
  182. * @param string $value Path
  183. * @return void
  184. * @since 1.5
  185. */
  186. public function setPath($name, $value)
  187. {
  188. $this->_paths[$name] = $value;
  189. }
  190. /**
  191. * Pushes a step onto the installer stack for rolling back steps
  192. *
  193. * @access public
  194. * @param array $step Installer step
  195. * @return void
  196. * @since 1.5
  197. */
  198. public function pushStep($step)
  199. {
  200. $this->_stepStack[] = $step;
  201. }
  202. /**
  203. * Installation abort method
  204. *
  205. * @access public
  206. * @param string $msg Abort message from the installer
  207. * @param string $type Package type if defined
  208. * @return boolean True if successful
  209. * @since 1.5
  210. */
  211. public function abort($msg=null, $type=null)
  212. {
  213. // Initialise variables.
  214. $retval = true;
  215. $step = array_pop($this->_stepStack);
  216. // Raise abort warning
  217. if ($msg) {
  218. JError::raiseWarning(100, $msg);
  219. }
  220. while ($step != null)
  221. {
  222. switch ($step['type'])
  223. {
  224. case 'file' :
  225. // remove the file
  226. $stepval = JFile::delete($step['path']);
  227. break;
  228. case 'folder' :
  229. // remove the folder
  230. $stepval = JFolder::delete($step['path']);
  231. break;
  232. case 'query' :
  233. // placeholder in case this is necessary in the future
  234. break;
  235. case 'extension' :
  236. // Get database connector object
  237. $db =& $this->getDBO();
  238. // Remove the entry from the #__extensions table
  239. $query = 'DELETE' .
  240. ' FROM `#__extensions`' .
  241. ' WHERE extension_id = '.(int)$step['id'];
  242. $db->setQuery($query);
  243. $stepval = $db->Query();
  244. break;
  245. default :
  246. if ($type && is_object($this->_adapters[$type])) {
  247. // Build the name of the custom rollback method for the type
  248. $method = '_rollback_'.$step['type'];
  249. // Custom rollback method handler
  250. if (method_exists($this->_adapters[$type], $method)) {
  251. $stepval = $this->_adapters[$type]->$method($step);
  252. }
  253. } else {
  254. $stepval = false; // set it to false
  255. }
  256. break;
  257. }
  258. // Only set the return value if it is false
  259. if ($stepval === false) {
  260. $retval = false;
  261. }
  262. // Get the next step and continue
  263. $step = array_pop($this->_stepStack);
  264. }
  265. return $retval;
  266. }
  267. // -----------------------
  268. // Adapter functions
  269. // -----------------------
  270. /**
  271. * Package installation method
  272. *
  273. * @access public
  274. * @param string $path Path to package source folder
  275. * @return boolean True if successful
  276. * @since 1.5
  277. */
  278. public function install($path=null)
  279. {
  280. if ($path && JFolder::exists($path)) {
  281. $this->setPath('source', $path);
  282. }
  283. else
  284. {
  285. $this->abort(JText::_('Install path does not exist'));
  286. return false;
  287. }
  288. if (!$this->setupInstall())
  289. {
  290. $this->abort(JText::_('Unable to detect manifest file'));
  291. return false;
  292. }
  293. $type = (string)$this->manifest->attributes()->type;
  294. if (is_object($this->_adapters[$type]))
  295. {
  296. // Add the languages from the package itself
  297. $lang =& JFactory::getLanguage();
  298. $lang->load('joomla',$path);
  299. // Fire the onBeforeExtensionInstall event.
  300. JPluginHelper::importPlugin('installer');
  301. $dispatcher =& JDispatcher::getInstance();
  302. $dispatcher->trigger('onBeforeExtensionInstall', array('method'=>'install', 'type'=>$type, 'manifest'=>$this->manifest, 'extension'=>0));
  303. // Run the install
  304. $result = $this->_adapters[$type]->install();
  305. // Fire the onAfterExtensionInstall
  306. $dispatcher->trigger('onAfterExtensionInstall', array('installer'=>clone $this, 'eid'=> $result));
  307. if ($result !== false) {
  308. return true;
  309. }
  310. else {
  311. return false;
  312. }
  313. }
  314. return false;
  315. }
  316. /*
  317. * Discovered package installation method
  318. *
  319. * @access public
  320. * @param string $path Path to package source folder
  321. * @return boolean True if successful
  322. * @since 1.5
  323. */
  324. function discover_install($eid=null)
  325. {
  326. if ($eid)
  327. {
  328. $this->extension =& JTable::getInstance('extension');
  329. if (!$this->extension->load($eid))
  330. {
  331. $this->abort(JText::_('Failed to load extension details'));
  332. return false;
  333. }
  334. if ($this->extension->state != -1)
  335. {
  336. $this->abort(JText::_('Extension is already installed'));
  337. return false;
  338. }
  339. // Lazy load the adapter
  340. if (!isset($this->_adapters[$this->extension->type]) || !is_object($this->_adapters[$this->extension->type]))
  341. {
  342. if (!$this->setAdapter($this->extension->type)) {
  343. return false;
  344. }
  345. }
  346. if (is_object($this->_adapters[$this->extension->type]))
  347. {
  348. if (method_exists($this->_adapters[$this->extension->type], 'discover_install'))
  349. {
  350. // Fire the onBeforeExtensionInstall event.
  351. JPluginHelper::importPlugin('installer');
  352. $dispatcher =& JDispatcher::getInstance();
  353. $dispatcher->trigger('onBeforeExtensionInstall', array('method'=>'discover_install', 'type'=>$this->extension->get('type'), 'manifest'=>null, 'extension'=>$this->extension->get('extension_id')));
  354. // Run the install
  355. $result = $this->_adapters[$this->extension->type]->discover_install();
  356. // Fire the onAfterExtensionInstall
  357. $dispatcher->trigger('onAfterExtensionInstall', array('installer'=>clone $this, 'eid'=> $result));
  358. if ($result !== false) return true; else return false;
  359. }
  360. else
  361. {
  362. $this->abort(JText::_('Method not supported for this extension type'));
  363. return false;
  364. }
  365. }
  366. return false;
  367. }
  368. else
  369. {
  370. $this->abort(JText::_('Extension is not a valid'));
  371. return false;
  372. }
  373. }
  374. /**
  375. * Extension discover method
  376. * Asks each adapter to find extensions
  377. *
  378. * @access public
  379. * @return Array JExtension
  380. */
  381. function discover()
  382. {
  383. $this->loadAllAdapters();
  384. $results = Array();
  385. foreach ($this->_adapters as $adapter)
  386. {
  387. // Joomla! 1.5 installation adapter legacy support
  388. if (method_exists($adapter,'discover'))
  389. {
  390. $tmp = $adapter->discover();
  391. // if its an array and has entries
  392. if (is_array($tmp) && count($tmp))
  393. {
  394. // merge it into the system
  395. $results = array_merge($results, $tmp);
  396. }
  397. }
  398. }
  399. return $results;
  400. }
  401. /**
  402. * Package update method
  403. *
  404. * @access public
  405. * @param string $path Path to package source folder
  406. * @return boolean True if successful
  407. * @since 1.5
  408. */
  409. public function update($path=null)
  410. {
  411. if ($path && JFolder::exists($path)) {
  412. $this->setPath('source', $path);
  413. }
  414. else {
  415. $this->abort(JText::_('Update path does not exist'));
  416. }
  417. if (!$this->setupInstall()) {
  418. return $this->abort(JText::_('Unable to detect manifest file'));
  419. }
  420. $type = (string)$this->manifest->attributes()->type;
  421. if (is_object($this->_adapters[$type]))
  422. {
  423. // Add the languages from the package itself
  424. $lang =& JFactory::getLanguage();
  425. $lang->load('joomla',$path);
  426. // Fire the onBeforeExtensionUpdate event.
  427. JPluginHelper::importPlugin('installer');
  428. $dispatcher =& JDispatcher::getInstance();
  429. $dispatcher->trigger('onBeforeExtensionUpdate', array('type'=>$type, 'manifest'=>$this->manifest));
  430. // Run the update
  431. $result = $this->_adapters[$type]->update();
  432. // Fire the onAfterExtensionUpdate
  433. $dispatcher->trigger('onAfterExtensionUpdate', array('installer'=>clone $this, 'eid'=> $result));
  434. if ($result !== false) {
  435. return true;
  436. }
  437. else {
  438. return false;
  439. }
  440. }
  441. return false;
  442. }
  443. /**
  444. * Package uninstallation method
  445. *
  446. * @access public
  447. * @param string $type Package type
  448. * @param mixed $identifier Package identifier for adapter
  449. * @param int $cid Application ID; deprecated in 1.6
  450. * @return boolean True if successful
  451. * @since 1.5
  452. */
  453. public function uninstall($type, $identifier, $cid=0)
  454. {
  455. if (!isset($this->_adapters[$type]) || !is_object($this->_adapters[$type]))
  456. {
  457. if (!$this->setAdapter($type)) {
  458. return false; // we failed to get the right adapter
  459. }
  460. }
  461. if (is_object($this->_adapters[$type]))
  462. {
  463. // We don't load languages here, we get the extension adapter to work it out
  464. // Fire the onBeforeExtensionUninstall event.
  465. JPluginHelper::importPlugin('installer');
  466. $dispatcher =& JDispatcher::getInstance();
  467. $dispatcher->trigger('onBeforeExtensionUninstall', array('eid' => $identifier));
  468. // Run the uninstall
  469. $result = $this->_adapters[$type]->uninstall($identifier);
  470. // Fire the onAfterExtensionInstall
  471. $dispatcher->trigger('onAfterExtensionUninstall', array('installer'=>clone $this, 'eid'=> $identifier, 'result' => $result));
  472. return $result;
  473. }
  474. return false;
  475. }
  476. function refreshManifestCache($eid)
  477. {
  478. if ($eid)
  479. {
  480. $this->extension =& JTable::getInstance('extension');
  481. if (!$this->extension->load($eid))
  482. {
  483. $this->abort(JText::_('Failed to load extension details'));
  484. return false;
  485. }
  486. if ($this->extension->state == -1)
  487. {
  488. $this->abort(JText::_('Refresh Manifest Cache failed').': '.JText::_('Extension is not currently installed'));
  489. return false;
  490. }
  491. // Lazy load the adapter
  492. if (!isset($this->_adapters[$this->extension->type]) || !is_object($this->_adapters[$this->extension->type]))
  493. {
  494. if (!$this->setAdapter($this->extension->type)) {
  495. return false;
  496. }
  497. }
  498. if (is_object($this->_adapters[$this->extension->type]))
  499. {
  500. if (method_exists($this->_adapters[$this->extension->type], 'refreshManifestCache'))
  501. {
  502. $result = $this->_adapters[$this->extension->type]->refreshManifestCache();
  503. if ($result !== false) return true; else return false;
  504. }
  505. else
  506. {
  507. $this->abort(JText::_('Method not supported for this extension type').': '. $this->extension->type);
  508. return false;
  509. }
  510. }
  511. return false;
  512. }
  513. else
  514. {
  515. $this->abort(JText::_('Refresh Manifest Cache failed').': '.JText::_('Extension not valid'));
  516. return false;
  517. }
  518. }
  519. // -----------------------
  520. // Utility functions
  521. // -----------------------
  522. /**
  523. * Prepare for installation: this method sets the installation directory, finds
  524. * and checks the installation file and verifies the installation type
  525. *
  526. * @access public
  527. * @return boolean True on success
  528. * @since 1.0
  529. */
  530. public function setupInstall()
  531. {
  532. // We need to find the installation manifest file
  533. if (!$this->findManifest()) {
  534. return false;
  535. }
  536. // Load the adapter(s) for the install manifest
  537. $type = (string)$this->manifest->attributes()->type;
  538. // Lazy load the adapter
  539. if (!isset($this->_adapters[$type]) || !is_object($this->_adapters[$type]))
  540. {
  541. if (!$this->setAdapter($type)) {
  542. return false;
  543. }
  544. }
  545. return true;
  546. }
  547. /**
  548. * Backward compatible Method to parse through a queries element of the
  549. * installation manifest file and take appropriate action.
  550. *
  551. * @access public
  552. * @param object $element The xml node to process
  553. * @return mixed Number of queries processed or False on error
  554. * @since 1.5
  555. */
  556. public function parseQueries($element)
  557. {
  558. // Get the database connector object
  559. $db = & $this->_db;
  560. if (!($element instanceof JXMLElement) || ! count($element->children()))
  561. {
  562. // Either the tag does not exist or has no children therefore we return zero files processed.
  563. return 0;
  564. }
  565. // Get the array of query nodes to process
  566. $queries = $element->children();
  567. if (count($queries) == 0)
  568. {
  569. // No queries to process
  570. return 0;
  571. }
  572. // Process each query in the $queries array (children of $tagName).
  573. foreach ($queries as $query)
  574. {
  575. $db->setQuery($query->data());
  576. if (!$db->query())
  577. {
  578. JError::raiseWarning(1, 'JInstaller::install: '.JText::_('SQL_ERROR')." ".$db->stderr(true));
  579. return false;
  580. }
  581. }
  582. return (int) count($queries);
  583. }
  584. /**
  585. * Method to extract the name of a discreet installation sql file from the installation manifest file.
  586. *
  587. * @access public
  588. * @param object $element The xml node to process
  589. * @param string $version The database connector to use
  590. * @return mixed Number of queries processed or False on error
  591. * @since 1.5
  592. */
  593. public function parseSQLFiles($element)
  594. {
  595. if ( ! $element instanceof JXMLElement || ! count($element->children())) {
  596. // The tag does not exist.
  597. return 0;
  598. }
  599. // Initialise variables.
  600. $queries = array();
  601. $db = & $this->_db;
  602. $dbDriver = strtolower($db->get('name'));
  603. if ($dbDriver == 'mysqli') {
  604. $dbDriver = 'mysql';
  605. }
  606. $dbCharset = ($db->hasUTF()) ? 'utf8' : '';
  607. // Get the name of the sql file to process
  608. $sqlfile = '';
  609. foreach ($element->children() as $file)
  610. {
  611. $fCharset = (strtolower($file->attributes()->charset) == 'utf8') ? 'utf8' : '';
  612. $fDriver = strtolower($file->attributes()->driver);
  613. if ($fDriver == 'mysqli') {
  614. $fDriver = 'mysql';
  615. }
  616. if ($fCharset == $dbCharset && $fDriver == $dbDriver)
  617. {
  618. $sqlfile = $this->getPath('extension_root').DS.$file;
  619. // Check that sql files exists before reading. Otherwise raise error for rollback
  620. if (!file_exists($sqlfile))
  621. {
  622. JError::raiseWarning(1,'JInstaller::installer: '. JText::_('SQL File not found').' '. $sqlfile);
  623. return false;
  624. }
  625. $buffer = file_get_contents($sqlfile);
  626. // Graceful exit and rollback if read not successful
  627. if ($buffer === false)
  628. {
  629. JError::raiseWarning(1, 'JInstaller::installer: '. JText::_('SQL File Buffer Read Error'));
  630. return false;
  631. }
  632. // Create an array of queries from the sql file
  633. jimport('joomla.installer.helper');
  634. $queries = JInstallerHelper::splitSql($buffer);
  635. if (count($queries) == 0) {
  636. // No queries to process
  637. return 0;
  638. }
  639. // Process each query in the $queries array (split out of sql file).
  640. foreach ($queries as $query)
  641. {
  642. $query = trim($query);
  643. if ($query != '' && $query{0} != '#')
  644. {
  645. $db->setQuery($query);
  646. if (!$db->query())
  647. {
  648. JError::raiseWarning(1, 'JInstaller::install: '.JText::_('SQL Error')." ".$db->stderr(true));
  649. return false;
  650. }
  651. }
  652. }
  653. }
  654. }
  655. return (int) count($queries);
  656. }
  657. /**
  658. * Method to parse through a files element of the installation manifest and take appropriate
  659. * action.
  660. *
  661. * @access public
  662. * @param object $element The xml node to process
  663. * @param int $cid Application ID of application to install to
  664. * @param Array List of old files (JXMLElement's)
  665. * @param Array List of old MD5 sums (indexed by filename with value as MD5)
  666. * @return boolean True on success
  667. * @since 1.5
  668. */
  669. public function parseFiles($element, $cid=0, $oldFiles=null, $oldMD5=null)
  670. {
  671. // Get the array of file nodes to process; we checked this had children above
  672. if ( ! is_a($element, 'JXMLElement') || ! count($element->children()))
  673. {
  674. // Either the tag does not exist or has no children (hence no files to process) therefore we return zero files processed.
  675. return 0;
  676. }
  677. // Initialise variables.
  678. $copyfiles = array ();
  679. // Get the client info
  680. jimport('joomla.application.helper');
  681. $client = &JApplicationHelper::getClientInfo($cid);
  682. /*
  683. * Here we set the folder we are going to remove the files from.
  684. */
  685. if ($client)
  686. {
  687. $pathname = 'extension_'.$client->name;
  688. $destination = $this->getPath($pathname);
  689. }
  690. else
  691. {
  692. $pathname = 'extension_root';
  693. $destination = $this->getPath($pathname);
  694. }
  695. /*
  696. * Here we set the folder we are going to copy the files from.
  697. *
  698. * Does the element have a folder attribute?
  699. *
  700. * If so this indicates that the files are in a subdirectory of the source
  701. * folder and we should append the folder attribute to the source path when
  702. * copying files.
  703. */
  704. $folder = (string)$element->attributes()->folder;
  705. if ($folder && file_exists($this->getPath('source').DS.$folder))
  706. {
  707. $source = $this->getPath('source').DS.$folder;
  708. }
  709. else {
  710. $source = $this->getPath('source');
  711. }
  712. // Work out what files have been deleted
  713. if ($oldFiles && is_a($oldFiles, 'JXMLElement'))
  714. {
  715. $oldEntries = $oldFiles->children();
  716. if (count($oldEntries))
  717. {
  718. $deletions = $this->findDeletedFiles($oldEntries, $files);
  719. foreach ($deletions['folders'] as $deleted_folder) {
  720. JFolder::delete($destination.DS.$deleted_folder);
  721. }
  722. foreach ($deletions['files'] as $deleted_file) {
  723. JFile::delete($destination.DS.$deleted_file);
  724. }
  725. }
  726. }
  727. // Copy the MD5SUMS file if it exists
  728. if (file_exists($source.DS.'MD5SUMS'))
  729. {
  730. $path['src'] = $source.DS.'MD5SUMS';
  731. $path['dest'] = $destination.DS.'MD5SUMS';
  732. $path['type'] = 'file';
  733. $copyfiles[] = $path;
  734. }
  735. // Process each file in the $files array (children of $tagName).
  736. foreach ($element->children() as $file)
  737. {
  738. $path['src'] = $source.DS.$file;
  739. $path['dest'] = $destination.DS.$file;
  740. // Is this path a file or folder?
  741. $path['type'] = ($file->getName() == 'folder') ? 'folder' : 'file';
  742. /*
  743. * Before we can add a file to the copyfiles array we need to ensure
  744. * that the folder we are copying our file to exits and if it doesn't,
  745. * we need to create it.
  746. */
  747. if (basename($path['dest']) != $path['dest'])
  748. {
  749. $newdir = dirname($path['dest']);
  750. if (!JFolder::create($newdir))
  751. {
  752. JError::raiseWarning(1, 'JInstaller::install: '.JText::_('FAILED_TO_CREATE_DIRECTORY').' "'.$newdir.'"');
  753. return false;
  754. }
  755. }
  756. // Add the file to the copyfiles array
  757. $copyfiles[] = $path;
  758. }
  759. return $this->copyFiles($copyfiles);
  760. }
  761. /**
  762. * Method to parse through a languages element of the installation manifest and take appropriate
  763. * action.
  764. *
  765. * @access public
  766. * @param object $element The xml node to process
  767. * @param int $cid Application ID of application to install to
  768. * @return boolean True on success
  769. * @since 1.5
  770. */
  771. public function parseLanguages($element, $cid=0)
  772. {
  773. if ( ! $element instanceof JXMLElement || ! count($element->children())) {
  774. // Either the tag does not exist or has no children therefore we return zero files processed.
  775. return 0;
  776. }
  777. // Initialise variables.
  778. $copyfiles = array ();
  779. // Get the client info
  780. jimport('joomla.application.helper');
  781. $client = &JApplicationHelper::getClientInfo($cid);
  782. /*
  783. * Here we set the folder we are going to copy the files to.
  784. *
  785. * 'languages' Files are copied to JPATH_BASE/language/ folder
  786. */
  787. $destination = $client->path.DS.'language';
  788. /*
  789. * Here we set the folder we are going to copy the files from.
  790. *
  791. * Does the element have a folder attribute?
  792. *
  793. * If so this indicates that the files are in a subdirectory of the source
  794. * folder and we should append the folder attribute to the source path when
  795. * copying files.
  796. */
  797. $folder = (string)$element->attributes()->folder;
  798. if ($folder && file_exists($this->getPath('source').DS.$folder)) {
  799. $source = $this->getPath('source').DS.$folder;
  800. }
  801. else {
  802. $source = $this->getPath('source');
  803. }
  804. // Process each file in the $files array (children of $tagName).
  805. foreach ($element->children() as $file)
  806. {
  807. /*
  808. * Language files go in a subfolder based on the language code, ie.
  809. *
  810. * <language tag="en-US">en-US.mycomponent.ini</language>
  811. *
  812. * would go in the en-US subdirectory of the language folder.
  813. *
  814. * We will only install language files where a core language pack
  815. * already exists.
  816. */
  817. if ((string)$file->attributes()->tag != '')
  818. {
  819. $path['src'] = $source.DS.$file;
  820. if ((string)$file->attributes()->client != '')
  821. {
  822. // override the client
  823. $langclient =& JApplicationHelper::getClientInfo((string)$file->attributes()->client, true);
  824. $path['dest'] = $langclient->path.DS.'language'.DS.$file->attributes()->tag.DS.basename((string)$file);
  825. }
  826. else
  827. {
  828. // use the default client
  829. $path['dest'] = $destination.DS.$file->attributes()->tag.DS.basename((string)$file);
  830. }
  831. // If the language folder is not present, then the core pack hasn't been installed... ignore
  832. if (!JFolder::exists(dirname($path['dest']))) {
  833. continue;
  834. }
  835. }
  836. else
  837. {
  838. $path['src'] = $source.DS.$file;
  839. $path['dest'] = $destination.DS.$file;
  840. }
  841. /*
  842. * Before we can add a file to the copyfiles array we need to ensure
  843. * that the folder we are copying our file to exits and if it doesn't,
  844. * we need to create it.
  845. */
  846. if (basename($path['dest']) != $path['dest'])
  847. {
  848. $newdir = dirname($path['dest']);
  849. if (!JFolder::create($newdir))
  850. {
  851. JError::raiseWarning(1, 'JInstaller::install: '.JText::_('FAILED_TO_CREATE_DIRECTORY').' "'.$newdir.'"');
  852. return false;
  853. }
  854. }
  855. // Add the file to the copyfiles array
  856. $copyfiles[] = $path;
  857. }
  858. return $this->copyFiles($copyfiles);
  859. }
  860. /**
  861. * Method to parse through a media element of the installation manifest and take appropriate
  862. * action.
  863. *
  864. * @access public
  865. * @param object $element The xml node to process
  866. * @param int $cid Application ID of application to install to
  867. * @return boolean True on success
  868. * @since 1.5
  869. */
  870. public function parseMedia($element, $cid=0)
  871. {
  872. if (!($element instanceof JXMLElement) || !count($element->children()))
  873. {
  874. // Either the tag does not exist or has no children therefore we return zero files processed.
  875. return 0;
  876. }
  877. // Initialise variables.
  878. $copyfiles = array ();
  879. // Get the client info
  880. jimport('joomla.application.helper');
  881. $client = &JApplicationHelper::getClientInfo($cid);
  882. /*
  883. * Here we set the folder we are going to copy the files to.
  884. * Default 'media' Files are copied to the JPATH_BASE/media folder
  885. */
  886. $folder = ((string)$element->attributes()->destination) ? DS.$element->attributes()->destination : null;
  887. $destination = JPath::clean(JPATH_ROOT.DS.'media'.$folder);
  888. /*
  889. * Here we set the folder we are going to copy the files from.
  890. *
  891. * Does the element have a folder attribute?
  892. *
  893. * If so this indicates that the files are in a subdirectory of the source
  894. * folder and we should append the folder attribute to the source path when
  895. * copying files.
  896. */
  897. $folder = (string)$element->attributes()->folder;
  898. if ($folder && file_exists($this->getPath('source').DS.$folder)) {
  899. $source = $this->getPath('source').DS.$folder;
  900. }
  901. else {
  902. $source = $this->getPath('source');
  903. }
  904. // Process each file in the $files array (children of $tagName).
  905. foreach ($element->children() as $file)
  906. {
  907. $path['src'] = $source.DS.$file;
  908. $path['dest'] = $destination.DS.$file;
  909. // Is this path a file or folder?
  910. $path['type'] = ($file->getName() == 'folder') ? 'folder' : 'file';
  911. /*
  912. * Before we can add a file to the copyfiles array we need to ensure
  913. * that the folder we are copying our file to exits and if it doesn't,
  914. * we need to create it.
  915. */
  916. if (basename($path['dest']) != $path['dest'])
  917. {
  918. $newdir = dirname($path['dest']);
  919. if (!JFolder::create($newdir))
  920. {
  921. JError::raiseWarning(1, 'JInstaller::install: '.JText::_('FAILED_TO_CREATE_DIRECTORY').' "'.$newdir.'"');
  922. return false;
  923. }
  924. }
  925. // Add the file to the copyfiles array
  926. $copyfiles[] = $path;
  927. }
  928. return $this->copyFiles($copyfiles);
  929. }
  930. /**
  931. * Method to parse the parameters of an extension, build the INI
  932. * string for it's default parameters, and return the INI string.
  933. *
  934. * @access public
  935. * @return string INI string of parameter values
  936. * @since 1.5
  937. */
  938. public function getParams()
  939. {
  940. // Get the manifest document root element
  941. $root = & $this->manifest->document;
  942. // Get the element of the tag names
  943. $params = $this->manifest->params;
  944. if( ! count($params->children())) {
  945. // Either the tag does not exist or has no children therefore we return zero files processed.
  946. return null;
  947. }
  948. // Process each parameter in the $params array.
  949. $ini = null;
  950. foreach ($params as $param)
  951. {
  952. if (!$name = $param->attributes()->name) {
  953. continue;
  954. }
  955. if (!$value = $param->attributes()->default) {
  956. continue;
  957. }
  958. $ini .= $name."=".$value."\n";
  959. }
  960. return $ini;
  961. }
  962. /**
  963. * Copy files from source directory to the target directory
  964. *
  965. * @access public
  966. * @param array $files array with filenames
  967. * @param boolean $overwrite True if existing files can be replaced
  968. * @return boolean True on success
  969. * @since 1.5
  970. */
  971. public function copyFiles($files, $overwrite=null)
  972. {
  973. /*
  974. * To allow for manual override on the overwriting flag, we check to see if
  975. * the $overwrite flag was set and is a boolean value. If not, use the object
  976. * allowOverwrite flag.
  977. */
  978. if (is_null($overwrite) || !is_bool($overwrite)) {
  979. $overwrite = $this->_overwrite;
  980. }
  981. /*
  982. * $files must be an array of filenames. Verify that it is an array with
  983. * at least one file to copy.
  984. */
  985. if (is_array($files) && count($files) > 0)
  986. {
  987. foreach ($files as $file)
  988. {
  989. // Get the source and destination paths
  990. $filesource = JPath::clean($file['src']);
  991. $filedest = JPath::clean($file['dest']);
  992. $filetype = array_key_exists('type', $file) ? $file['type'] : 'file';
  993. if (!file_exists($filesource))
  994. {
  995. /*
  996. * The source file does not exist. Nothing to copy so set an error
  997. * and return false.
  998. */
  999. JError::raiseWarning(1, 'JInstaller::install: '.JText::sprintf('File does not exist', $filesource));
  1000. return false;
  1001. }
  1002. elseif (file_exists($filedest) && !$overwrite)
  1003. {
  1004. /*
  1005. * It's okay if the manifest already exists
  1006. */
  1007. if ($this->getPath('manifest') == $filesource) {
  1008. continue;
  1009. }
  1010. /*
  1011. * The destination file already exists and the overwrite flag is false.
  1012. * Set an error and return false.
  1013. */
  1014. JError::raiseWarning(1, 'JInstaller::install: '.JText::sprintf('WARNSAME', $filedest));
  1015. return false;
  1016. }
  1017. else
  1018. {
  1019. // Copy the folder or file to the new location.
  1020. if ($filetype == 'folder')
  1021. {
  1022. if (!(JFolder::copy($filesource, $filedest, null, $overwrite,1)))
  1023. {
  1024. JError::raiseWarning(1, 'JInstaller::install: '.JText::sprintf('Failed to copy folder to', $filesource, $filedest));
  1025. return false;
  1026. }
  1027. $step = array ('type' => 'folder', 'path' => $filedest);
  1028. }
  1029. else
  1030. {
  1031. if (!(JFile::copy($filesource, $filedest,null,1)))
  1032. {
  1033. JError::raiseWarning(1, 'JInstaller::install: '.JText::sprintf('Failed to copy file to', $filesource, $filedest));
  1034. return false;
  1035. }
  1036. $step = array ('type' => 'file', 'path' => $filedest);
  1037. }
  1038. /*
  1039. * Since we copied a file/folder, we want to add it to the installation step stack so that
  1040. * in case we have to roll back the installation we can remove the files copied.
  1041. */
  1042. $this->_stepStack[] = $step;
  1043. }
  1044. }
  1045. }
  1046. else
  1047. {
  1048. /*
  1049. * The $files variable was either not an array or an empty array
  1050. */
  1051. return false;
  1052. }
  1053. return count($files);
  1054. }
  1055. /**
  1056. * Method to parse through a files element of the installation manifest and remove
  1057. * the files that were installed
  1058. *
  1059. * @access public
  1060. * @param object $element The xml node to process
  1061. * @param int $cid Application ID of application to remove from
  1062. * @return boolean True on success
  1063. * @since 1.5
  1064. */
  1065. public function removeFiles($element, $cid=0)
  1066. {
  1067. // Initialise variables.
  1068. $removefiles = array ();
  1069. $retval = true;
  1070. // Get the client info if we're using a specific client
  1071. jimport('joomla.application.helper');
  1072. if ($cid > -1) {
  1073. $client = &JApplicationHelper::getClientInfo($cid);
  1074. }
  1075. else {
  1076. $client = null;
  1077. }
  1078. if (!($element instanceof JXMLElement) || !count($element->children()))
  1079. {
  1080. // Either the tag does not exist or has no children therefore we return zero files processed.
  1081. return true;
  1082. }
  1083. // Get the array of file nodes to process
  1084. $files = $element->children();
  1085. if (count($files) == 0) {
  1086. // No files to process
  1087. return true;
  1088. }
  1089. $folder = '';
  1090. /*
  1091. * Here we set the folder we are going to remove the files from. There are a few
  1092. * special cases that need to be considered for certain reserved tags.
  1093. */
  1094. switch ($element->getName())
  1095. {
  1096. case 'media':
  1097. if ((string)$element->attributes()->destination) {
  1098. $folder = (string)$element->attributes()->destination;
  1099. }
  1100. else {
  1101. $folder = '';
  1102. }
  1103. $source = $client->path.DS.'media'.DS.$folder;
  1104. break;
  1105. case 'languages':
  1106. if ($client) {
  1107. $source = $client->path.DS.'language';
  1108. }
  1109. else {
  1110. $source = '';
  1111. }
  1112. break;
  1113. default:
  1114. if ($client)
  1115. {
  1116. $pathname = 'extension_'.$client->name;
  1117. $source = $this->getPath($pathname);
  1118. }
  1119. else
  1120. {
  1121. $pathname = 'extension_root';
  1122. $source = $this->getPath($pathname);
  1123. }
  1124. break;
  1125. }
  1126. // Process each file in the $files array (children of $tagName).
  1127. foreach ($files as $file)
  1128. {
  1129. /*
  1130. * If the file is a language, we must handle it differently. Language files
  1131. * go in a subdirectory based on the language code, ie.
  1132. *
  1133. * <language tag="en_US">en_US.mycomponent.ini</language>
  1134. *
  1135. * would go in the en_US subdirectory of the languages directory.
  1136. */
  1137. if ($file->getName() == 'language' && (string)$file->attributes()->tag != '')
  1138. {
  1139. if ($source) {
  1140. $path = $source.DS.$file->attributes()->tag.DS.basename((string)$file);
  1141. }
  1142. else
  1143. {
  1144. $target_client = JApplicationHelper::getClientInfo((string)$file->attributes()->client, true);
  1145. $path = $target_client->path.DS.'language'.DS.$file->attributes()->tag.DS.basename((string)$file);
  1146. }
  1147. // If the language folder is not present, then the core pack hasn't been installed... ignore
  1148. if (!JFolder::exists(dirname($path)))
  1149. {
  1150. continue;
  1151. }
  1152. }
  1153. else {
  1154. $path = $source.DS.$file;
  1155. }
  1156. /*
  1157. * Actually delete the files/folders
  1158. */
  1159. if (is_dir($path)) {
  1160. $val = JFolder::delete($path);
  1161. }
  1162. else {
  1163. $val = JFile::delete($path);
  1164. }
  1165. if ($val === false)
  1166. {
  1167. JError::raiseWarning(43, 'Failed to delete '. $path);
  1168. $retval = false;
  1169. }
  1170. }
  1171. if (!empty($folder)) {
  1172. $val = JFolder::delete($source);
  1173. }
  1174. return $retval;
  1175. }
  1176. /**
  1177. * Copies the installation manifest file to the extension folder in the given client
  1178. *
  1179. * @access public
  1180. * @param int $cid Where to copy the installfile [optional: defaults to 1 (admin)]
  1181. * @return boolean True on success, False on error
  1182. * @since 1.5
  1183. */
  1184. public function copyManifest($cid=1)
  1185. {
  1186. // Get the client info
  1187. jimport('joomla.application.helper');
  1188. $client = &JApplicationHelper::getClientInfo($cid);
  1189. $path['src'] = $this->getPath('manifest');
  1190. if ($client)
  1191. {
  1192. $pathname = 'extension_'.$client->name;
  1193. $path['dest'] = $this->getPath($pathname).DS.basename($this->getPath('manifest'));
  1194. }
  1195. else
  1196. {
  1197. $pathname = 'extension_root';
  1198. $path['dest'] = $this->getPath($pathname).DS.basename($this->getPath('manifest'));
  1199. }
  1200. return $this->copyFiles(array ($path), true);
  1201. }
  1202. /**
  1203. * Tries to find the package manifest file
  1204. *
  1205. * @access private
  1206. * @return boolean True on success, False on error
  1207. * @since 1.0
  1208. */
  1209. public function findManifest()
  1210. {
  1211. // Get an array of all the xml files from the installation directory
  1212. $xmlfiles = JFolder::files($this->getPath('source'), '.xml$', 1, true);
  1213. // If at least one xml file exists
  1214. if (!empty($xmlfiles))
  1215. {
  1216. foreach ($xmlfiles as $file)
  1217. {
  1218. // Is it a valid joomla installation manifest file?
  1219. $manifest = $this->isManifest($file);
  1220. if (!is_null($manifest))
  1221. {
  1222. // If the root method attribute is set to upgrade, allow file overwrite
  1223. if ((string)$manifest->attributes()->method == 'upgrade')
  1224. {
  1225. $this->_upgrade = true;
  1226. $this->_overwrite = true;
  1227. }
  1228. // If the overwrite option is set, allow file overwriting
  1229. if ((string)$manifest->attributes()->overwrite == 'true') {
  1230. $this->_overwrite = true;
  1231. }
  1232. // Set the manifest object and path
  1233. $this->manifest = $manifest;
  1234. $this->setPath('manifest', $file);
  1235. // Set the installation source path to that of the manifest file
  1236. $this->setPath('source', dirname($file));
  1237. return true;
  1238. }
  1239. }
  1240. // None of the xml files found were valid install files
  1241. JError::raiseWarning(1, 'JInstaller::install: '.JText::_('ERRORNOTFINDJOOMLAXMLSETUPFILE'));
  1242. return false;
  1243. }
  1244. else
  1245. {
  1246. // No xml files were found in the install folder
  1247. JError::raiseWarning(1, 'JInstaller::install: '.JText::_('ERRORXMLSETUP'));
  1248. return false;
  1249. }
  1250. }
  1251. /**
  1252. * Is the xml file a valid Joomla installation manifest file
  1253. *
  1254. * @access private
  1255. * @param string $file An xmlfile path to check
  1256. * @return mixed A JXMLElement, or null if the file failed to parse
  1257. * @since 1.5
  1258. */
  1259. public function isManifest($file)
  1260. {
  1261. // Initialise variables.
  1262. $xml = JFactory::getXML($file);
  1263. // If we cannot load the xml file return null
  1264. if( ! $xml)
  1265. {
  1266. return null;
  1267. }
  1268. /*
  1269. * Check for a valid XML root tag.
  1270. * @todo: Remove backwards compatability in a future version
  1271. * Should be 'extension', but for backward compatability we will accept 'extension' or 'install'.
  1272. */
  1273. // 1.5 uses 'install'
  1274. // 1.6 uses 'extension'
  1275. if($xml->getName() != 'install' && $xml->getName() != 'extension')
  1276. {
  1277. return null;
  1278. }
  1279. // Valid manifest file return the object
  1280. return $xml;
  1281. }
  1282. /**
  1283. * Generates a manifest cache
  1284. * @return string serialised manifest data
  1285. */
  1286. public function generateManifestCache()
  1287. {
  1288. return serialize(JApplicationHelper::parseXMLInstallFile($this->getPath('manifest')));
  1289. }
  1290. /**
  1291. * Cleans up discovered extensions if they're being installed somehow else
  1292. */
  1293. public function cleanDiscoveredExtension($type, $element, $folder='', $client=0)
  1294. {
  1295. $dbo =& JFactory::getDBO();
  1296. $dbo->setQuery('DELETE FROM #__extensions WHERE type = '. $dbo->Quote($type).' AND element = '. $dbo->Quote($element) .' AND folder = '. $dbo->Quote($folder). ' AND client_id = '. intval($client).' AND state = -1');
  1297. return $dbo->Query();
  1298. }
  1299. /**
  1300. * Compares two "files" entries to find deleted files/folders
  1301. * @param array An array of JXMLElement objects that are the old files
  1302. * @param array An array of JXMLElement objects that are the new files
  1303. * @return array An array with the delete files and folders in findDeletedFiles[files] and findDeletedFiles[folders] resepctively
  1304. */
  1305. public function findDeletedFiles($old_files, $new_files)
  1306. {
  1307. // The magic find deleted files function!
  1308. $files = Array(); // the files that are new
  1309. $folders = Array(); // the folders that are new
  1310. $containers = Array(); // the folders of the files that are new
  1311. $files_deleted = Array(); // a list of files to delete
  1312. $folders_deleted = Array(); // a list of folders to delete
  1313. foreach ($new_files as $file)
  1314. {
  1315. switch($file->getName())
  1316. {
  1317. case 'folder':
  1318. $folders[] = (string)$file; // add any folders to the list
  1319. break;
  1320. case 'file':
  1321. default:
  1322. $files[] = (string)$file; // add any files to the list
  1323. // now handle the folder part of the file to ensure we get any containers
  1324. $container_parts = explode('/',dirname((string)$file)); // break up the parts of the directory
  1325. $container = ''; // make sure this is clean and empty
  1326. foreach ($container_parts as $part)
  1327. {
  1328. // iterate through each part
  1329. if (!empty($container)) $container .= '/'; // add a slash if its not empty
  1330. $container .= $part; // append the folder part
  1331. if (!in_array($container, $containers)) $containers[] = $container; // add the container if it doesn't already exist
  1332. }
  1333. break;
  1334. }
  1335. }
  1336. foreach ($old_files as $file)
  1337. {
  1338. switch($file->getName())
  1339. {
  1340. case 'folder':
  1341. if (!in_array((string)$file, $folders))
  1342. {
  1343. // look if the folder exists in the new list
  1344. if (!in_array((string)$file, $containers)) { // check if the folder exists as a container in the new list
  1345. $folders_deleted[] = (string)$file; // if its not in the new list or a container then delete it
  1346. }
  1347. }
  1348. break;
  1349. case 'file':
  1350. default:
  1351. if (!in_array((string)$file, $files))
  1352. {
  1353. // look if the file exists in the new list
  1354. if (!in_array(dirname((string)$file), $folders)) {
  1355. // look if the file is now potentially in a folder
  1356. $files_deleted[] = (string)$file; // not in a folder, doesn't exist, wipe it out!
  1357. }
  1358. }
  1359. break;
  1360. }
  1361. }
  1362. return Array('files'=>$files_deleted, 'folders'=>$folders_deleted);
  1363. }
  1364. /**
  1365. * Loads an MD5SUMS file into an associative array
  1366. * @param string Filename to load
  1367. * @return Array Associative array with filenames as the index and the MD5 as the value
  1368. */
  1369. function loadMD5Sum($filename)
  1370. {
  1371. if (!file_exists($filename)) return false; // bail if the file doesn't exist
  1372. $data = file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
  1373. $retval = Array();
  1374. foreach ($data as $row)
  1375. {
  1376. $results = explode(' ', $row); // split up the data
  1377. $results[1] = str_replace('./','', $results[1]); // cull any potential prefix
  1378. $retval[$results[1]] = $results[0]; // throw into the array
  1379. }
  1380. return $retval;
  1381. }
  1382. /**
  1383. * Get a group ID from a given name
  1384. * @param string Name of group to find
  1385. * @return int the group id of the user, false on error
  1386. * @todo Find the right place to put this function
  1387. */
  1388. function getGroupIDFromName($groupname)
  1389. {
  1390. $dbo = $this->getDBO();
  1391. $dbo->setQuery('SELECT id FROM #__usergroups WHERE title = "'. $dbo->getEscaped($groupname) .'"');
  1392. return $dbo->loadResult();
  1393. }
  1394. }