Merge pull request #4983 from colemanw/CRM-15842
[civicrm-core.git] / CRM / Extension / Downloader.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
39de6fd5 4 | CiviCRM version 4.6 |
6a488035 5 +--------------------------------------------------------------------+
06b69b18 6 | Copyright CiviCRM LLC (c) 2004-2014 |
6a488035
TO
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 +--------------------------------------------------------------------+
d25dd0ee 26 */
6a488035
TO
27
28/**
29 * This class handles downloads of remotely-provided extensions
30 *
31 * @package CRM
06b69b18 32 * @copyright CiviCRM LLC (c) 2004-2014
6a488035
TO
33 * $Id$
34 *
35 */
36class 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 /**
fd31fa4c 48 * @param CRM_Extension_Manager $manager
f41911fd
TO
49 * @param string $containerDir
50 * The place to store downloaded & extracted extensions.
6a488035
TO
51 * @param string $tmpDir
52 */
53 public function __construct(CRM_Extension_Manager $manager, $containerDir, $tmpDir) {
54 $this->manager = $manager;
55 $this->containerDir = $containerDir;
56 $this->tmpDir = $tmpDir;
57 }
58
59 /**
60 * Determine whether downloading is supported
61 *
a6c01b45
CW
62 * @return array
63 * list of error messages; empty if OK
6a488035
TO
64 */
65 public function checkRequirements() {
66 $errors = array();
67
22e263ad 68 if (!$this->containerDir || !is_dir($this->containerDir) || !is_writable($this->containerDir)) {
6a488035
TO
69 $civicrmDestination = urlencode(CRM_Utils_System::url('civicrm/admin/extensions', 'reset=1'));
70 $url = CRM_Utils_System::url('civicrm/admin/setting/path', "reset=1&civicrmDestination=${civicrmDestination}");
71 $errors[] = array(
72 'title' => ts('Directory Unwritable'),
73 //'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/>',
74 'message' => ts("Your extensions directory is not set or is not writable. Click <a href='%1'>here</a> to set the extensions directory.",
75 array(
76 //1 => $this->containerDir,
77 1 => $url,
78 )
21dfd5f5 79 ),
6a488035
TO
80 );
81 }
82
83 if (!class_exists('ZipArchive')) {
84 $errors[] = array(
85 'title' => ts('ZIP Support Required'),
86 '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.'),
87 );
88 }
89
353ffa53 90 if (empty($errors) && !CRM_Utils_HttpClient::singleton()->isRedirectSupported()) {
eb066397
TO
91 CRM_Core_Session::setStatus(ts('WARNING: The downloader may be unable to download files which require HTTP redirection. This may be a configuration issue with PHP\'s open_basedir or safe_mode.'));
92 CRM_Core_Error::debug_log_message('WARNING: The downloader may be unable to download files which require HTTP redirection. This may be a configuration issue with PHP\'s open_basedir or safe_mode.');
93 }
94
6a488035
TO
95 return $errors;
96 }
97
98 /**
99 * Install or upgrade an extension from a remote URL
100 *
f41911fd
TO
101 * @param string $key
102 * The name of the extension being installed.
103 * @param string $downloadUrl
104 * URL of a .zip file.
a6c01b45
CW
105 * @return bool
106 * TRUE for success
6a488035
TO
107 * @throws CRM_Extension_Exception
108 */
109 public function download($key, $downloadUrl) {
110 $filename = $this->tmpDir . DIRECTORY_SEPARATOR . $key . '.zip';
111 $destDir = $this->containerDir . DIRECTORY_SEPARATOR . $key;
112
113 if (!$downloadUrl) {
114 CRM_Core_Error::fatal('Cannot install this extension - downloadUrl is not set!');
115 }
116
353ffa53 117 if (!$this->fetch($downloadUrl, $filename)) {
6a488035
TO
118 return FALSE;
119 }
120
121 $extractedZipPath = $this->extractFiles($key, $filename);
353ffa53 122 if (!$extractedZipPath) {
6a488035
TO
123 return FALSE;
124 }
125
353ffa53 126 if (!$this->validateFiles($key, $extractedZipPath)) {
6a488035
TO
127 return FALSE;
128 }
129
130 $this->manager->replace($extractedZipPath);
131
132 return TRUE;
133 }
134
135 /**
136 * Download the remote zipfile.
137 *
f41911fd
TO
138 * @param string $remoteFile
139 * URL of a .zip file.
140 * @param string $localFile
141 * Path at which to store the .zip file.
ae5ffbb7 142 * @return bool
a6c01b45 143 * Whether the download was successful.
6a488035
TO
144 */
145 public function fetch($remoteFile, $localFile) {
3b6f287b 146 $result = CRM_Utils_HttpClient::singleton()->fetch($remoteFile, $localFile);
6a488035
TO
147 switch ($result) {
148 case CRM_Utils_HttpClient::STATUS_OK:
149 return TRUE;
b3a4b879 150
6a488035
TO
151 default:
152 return FALSE;
153 }
154 }
155
156 /**
157 * Extract an extension from a zip file
158 *
f41911fd
TO
159 * @param string $key
160 * The name of the extension being installed; this usually matches the basedir in the .zip.
161 * @param string $zipFile
162 * The local path to a .zip file.
72b3a70c
CW
163 * @return string|FALSE
164 * zip file path
6a488035
TO
165 */
166 public function extractFiles($key, $zipFile) {
167 $config = CRM_Core_Config::singleton();
168
169 $zip = new ZipArchive();
170 $res = $zip->open($zipFile);
171 if ($res === TRUE) {
172 $zipSubDir = CRM_Utils_Zip::guessBasedir($zip, $key);
173 if ($zipSubDir === FALSE) {
174 CRM_Core_Session::setStatus(ts('Unable to extract the extension: bad directory structure'), '', 'error');
175 return FALSE;
176 }
177 $extractedZipPath = $this->tmpDir . DIRECTORY_SEPARATOR . $zipSubDir;
178 if (is_dir($extractedZipPath)) {
179 if (!CRM_Utils_File::cleanDir($extractedZipPath, TRUE, FALSE)) {
180 CRM_Core_Session::setStatus(ts('Unable to extract the extension: %1 cannot be cleared', array(1 => $extractedZipPath)), ts('Installation Error'), 'error');
181 return FALSE;
182 }
183 }
184 if (!$zip->extractTo($this->tmpDir)) {
185 CRM_Core_Session::setStatus(ts('Unable to extract the extension to %1.', array(1 => $this->tmpDir)), ts('Installation Error'), 'error');
186 return FALSE;
187 }
188 $zip->close();
189 }
190 else {
191 CRM_Core_Session::setStatus(ts('Unable to extract the extension.'), '', 'error');
192 return FALSE;
193 }
194
195 return $extractedZipPath;
196 }
197
198 /**
199 * Validate that $extractedZipPath contains valid for extension $key
200 *
da6b46f4
EM
201 * @param $key
202 * @param $extractedZipPath
203 *
6a488035
TO
204 * @return bool
205 */
00be9182 206 public function validateFiles($key, $extractedZipPath) {
6a488035
TO
207 $filename = $extractedZipPath . DIRECTORY_SEPARATOR . CRM_Extension_Info::FILENAME;
208 if (!is_readable($filename)) {
209 CRM_Core_Session::setStatus(ts('Failed reading data from %1 during installation', array(1 => $filename)), ts('Installation Error'), 'error');
210 return FALSE;
211 }
212
213 try {
214 $newInfo = CRM_Extension_Info::loadFromFile($filename);
0db6c3e1
TO
215 }
216 catch (Exception $e) {
6a488035
TO
217 CRM_Core_Session::setStatus(ts('Failed reading data from %1 during installation', array(1 => $filename)), ts('Installation Error'), 'error');
218 return FALSE;
219 }
220
221 if ($newInfo->key != $key) {
222 CRM_Core_Error::fatal('Cannot install - there are differences between extdir XML file and archive XML file!');
223 }
224
225 return TRUE;
226 }
227
228}