PageRenderTime 54ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/libraries/joomla/installer/installer.php

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