Merge remote-tracking branch 'upstream/4.3' into 4.3-4.4-2013-11-26-11-43-18
[civicrm-core.git] / CRM / Extension / Downloader.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.4 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2013 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28 /**
29 * This class handles downloads of remotely-provided extensions
30 *
31 * @package CRM
32 * @copyright CiviCRM LLC (c) 2004-2013
33 * $Id$
34 *
35 */
36 class CRM_Extension_Downloader {
37 /**
38 * @var CRM_Extension_Container_Basic the place where downloaded extensions are ultimately stored
39 */
40 public $container;
41
42 /**
43 * @var string local path to a temporary data directory
44 */
45 public $tmpDir;
46
47 /**
48 * @param string $containerDir the place to store downloaded & extracted extensions
49 * @param string $tmpDir
50 */
51 public function __construct(CRM_Extension_Manager $manager, $containerDir, $tmpDir) {
52 $this->manager = $manager;
53 $this->containerDir = $containerDir;
54 $this->tmpDir = $tmpDir;
55 }
56
57 /**
58 * Determine whether downloading is supported
59 *
60 * @return array list of error messages; empty if OK
61 */
62 public function checkRequirements() {
63 $errors = array();
64
65 if (!$this->containerDir || !is_dir($this->containerDir) || !is_writeable($this->containerDir)) {
66 $civicrmDestination = urlencode(CRM_Utils_System::url('civicrm/admin/extensions', 'reset=1'));
67 $url = CRM_Utils_System::url('civicrm/admin/setting/path', "reset=1&civicrmDestination=${civicrmDestination}");
68 $errors[] = array(
69 'title' => ts('Directory Unwritable'),
70 //'message' => ts('Your extensions directory: %1 is not web server writable. Please go to the <a href="%2">path setting page</a> and correct it.<br/>',
71 'message' => ts("Your extensions directory is not set or is not writable. Click <a href='%1'>here</a> to set the extensions directory.",
72 array(
73 //1 => $this->containerDir,
74 1 => $url,
75 )
76 )
77 );
78 }
79
80 if (!class_exists('ZipArchive')) {
81 $errors[] = array(
82 'title' => ts('ZIP Support Required'),
83 'message' => ts('You will not be able to install extensions at this time because your installation of PHP does not support ZIP archives. Please ask your system administrator to install the standard PHP-ZIP extension.'),
84 );
85 }
86
87 return $errors;
88 }
89
90 /**
91 * Install or upgrade an extension from a remote URL
92 *
93 * @param string $key the name of the extension being installed
94 * @param string $downloadUrl URL of a .zip file
95 * @return bool TRUE for success
96 * @throws CRM_Extension_Exception
97 */
98 public function download($key, $downloadUrl) {
99 $filename = $this->tmpDir . DIRECTORY_SEPARATOR . $key . '.zip';
100 $destDir = $this->containerDir . DIRECTORY_SEPARATOR . $key;
101
102 if (!$downloadUrl) {
103 CRM_Core_Error::fatal('Cannot install this extension - downloadUrl is not set!');
104 }
105
106 if (! $this->fetch($downloadUrl, $filename)) {
107 return FALSE;
108 }
109
110 $extractedZipPath = $this->extractFiles($key, $filename);
111 if (! $extractedZipPath) {
112 return FALSE;
113 }
114
115 if (! $this->validateFiles($key, $extractedZipPath)) {
116 return FALSE;
117 }
118
119 $this->manager->replace($extractedZipPath);
120
121 return TRUE;
122 }
123
124 /**
125 * Download the remote zipfile.
126 *
127 * @param string $remoteFile URL of a .zip file
128 * @param string $localFile path at which to store the .zip file
129 * @return boolean Whether the download was successful.
130 */
131 public function fetch($remoteFile, $localFile) {
132 $result = CRM_Utils_HttpClient::singleton()->fetch($remoteFile, $localFile);
133 switch ($result) {
134 case CRM_Utils_HttpClient::STATUS_OK:
135 return TRUE;
136 default:
137 return FALSE;
138 }
139 }
140
141 /**
142 * Extract an extension from a zip file
143 *
144 * @param string $key the name of the extension being installed; this usually matches the basedir in the .zip
145 * @param string $zipFile the local path to a .zip file
146 * @return string|FALSE zip file path
147 */
148 public function extractFiles($key, $zipFile) {
149 $config = CRM_Core_Config::singleton();
150
151 $zip = new ZipArchive();
152 $res = $zip->open($zipFile);
153 if ($res === TRUE) {
154 $zipSubDir = CRM_Utils_Zip::guessBasedir($zip, $key);
155 if ($zipSubDir === FALSE) {
156 CRM_Core_Session::setStatus(ts('Unable to extract the extension: bad directory structure'), '', 'error');
157 return FALSE;
158 }
159 $extractedZipPath = $this->tmpDir . DIRECTORY_SEPARATOR . $zipSubDir;
160 if (is_dir($extractedZipPath)) {
161 if (!CRM_Utils_File::cleanDir($extractedZipPath, TRUE, FALSE)) {
162 CRM_Core_Session::setStatus(ts('Unable to extract the extension: %1 cannot be cleared', array(1 => $extractedZipPath)), ts('Installation Error'), 'error');
163 return FALSE;
164 }
165 }
166 if (!$zip->extractTo($this->tmpDir)) {
167 CRM_Core_Session::setStatus(ts('Unable to extract the extension to %1.', array(1 => $this->tmpDir)), ts('Installation Error'), 'error');
168 return FALSE;
169 }
170 $zip->close();
171 }
172 else {
173 CRM_Core_Session::setStatus(ts('Unable to extract the extension.'), '', 'error');
174 return FALSE;
175 }
176
177 return $extractedZipPath;
178 }
179
180 /**
181 * Validate that $extractedZipPath contains valid for extension $key
182 *
183 * @return bool
184 */
185 function validateFiles($key, $extractedZipPath) {
186 $filename = $extractedZipPath . DIRECTORY_SEPARATOR . CRM_Extension_Info::FILENAME;
187 if (!is_readable($filename)) {
188 CRM_Core_Session::setStatus(ts('Failed reading data from %1 during installation', array(1 => $filename)), ts('Installation Error'), 'error');
189 return FALSE;
190 }
191
192 try {
193 $newInfo = CRM_Extension_Info::loadFromFile($filename);
194 } catch (Exception $e) {
195 CRM_Core_Session::setStatus(ts('Failed reading data from %1 during installation', array(1 => $filename)), ts('Installation Error'), 'error');
196 return FALSE;
197 }
198
199 if ($newInfo->key != $key) {
200 CRM_Core_Error::fatal('Cannot install - there are differences between extdir XML file and archive XML file!');
201 }
202
203 return TRUE;
204 }
205
206 }