Merge pull request #7024 from colemanw/CRM-13823
[civicrm-core.git] / CRM / Utils / HttpClient.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2015 |
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 HTTP downloads
30 *
31 * FIXME: fetch() and get() report errors differently -- e.g.
32 * fetch() returns fatal and get() returns an error code. Should
33 * refactor both (or get a third-party HTTP library) but don't
34 * want to deal with that so late in the 4.3 dev cycle.
35 *
36 * @package CRM
37 * @copyright CiviCRM LLC (c) 2004-2015
38 */
39 class CRM_Utils_HttpClient {
40
41 const STATUS_OK = 'ok';
42 const STATUS_WRITE_ERROR = 'write-error';
43 const STATUS_DL_ERROR = 'dl-error';
44
45 /**
46 * @var CRM_Utils_HttpClient
47 */
48 protected static $singleton;
49
50 /**
51 * @var int|NULL
52 * seconds; or NULL to use system default
53 */
54 protected $connectionTimeout;
55
56 /**
57 * @return CRM_Utils_HttpClient
58 */
59 public static function singleton() {
60 if (!self::$singleton) {
61 self::$singleton = new CRM_Utils_HttpClient();
62 }
63 return self::$singleton;
64 }
65
66 /**
67 * @param null $connectionTimeout
68 */
69 public function __construct($connectionTimeout = NULL) {
70 $this->connectionTimeout = $connectionTimeout;
71 }
72
73 /**
74 * Download the remote zipfile.
75 *
76 * @param string $remoteFile
77 * URL of a .zip file.
78 * @param string $localFile
79 * Path at which to store the .zip file.
80 * @return STATUS_OK|STATUS_WRITE_ERROR|STATUS_DL_ERROR
81 */
82 public function fetch($remoteFile, $localFile) {
83 // Download extension zip file ...
84 if (!function_exists('curl_init')) {
85 CRM_Core_Error::fatal('Cannot install this extension - curl is not installed!');
86 }
87
88 list($ch, $caConfig) = $this->createCurl($remoteFile);
89 if (preg_match('/^https:/', $remoteFile) && !$caConfig->isEnableSSL()) {
90 CRM_Core_Error::fatal('Cannot install this extension - does not support SSL');
91 }
92
93 $fp = @fopen($localFile, "w");
94 if (!$fp) {
95 // Fixme: throw error instead of setting message
96 CRM_Core_Session::setStatus(ts('Unable to write to %1.<br />Is the location writable?', array(1 => $localFile)), ts('Write Error'), 'error');
97 return self::STATUS_WRITE_ERROR;
98 }
99 curl_setopt($ch, CURLOPT_FILE, $fp);
100
101 curl_exec($ch);
102 if (curl_errno($ch)) {
103 // Fixme: throw error instead of setting message
104 CRM_Core_Session::setStatus(ts('Unable to download extension from %1. Error Message: %2',
105 array(1 => $remoteFile, 2 => curl_error($ch))), ts('Extension download error'), 'error');
106 return self::STATUS_DL_ERROR;
107 }
108 else {
109 curl_close($ch);
110 }
111
112 fclose($fp);
113
114 return self::STATUS_OK;
115 }
116
117 /**
118 * Send an HTTP GET for a remote resource.
119 *
120 * @param string $remoteFile
121 * URL of remote file.
122 * @return array
123 * array(0 => STATUS_OK|STATUS_DL_ERROR, 1 => string)
124 */
125 public function get($remoteFile) {
126 // Download extension zip file ...
127 if (!function_exists('curl_init')) {
128 // CRM-13805
129 CRM_Core_Session::setStatus(
130 ts('As a result, actions like retrieving the CiviCRM news feed will fail. Talk to your server administrator or hosting company to rectify this.'),
131 ts('Curl is not installed')
132 );
133 return array(self::STATUS_DL_ERROR, NULL);
134 }
135
136 list($ch, $caConfig) = $this->createCurl($remoteFile);
137
138 if (preg_match('/^https:/', $remoteFile) && !$caConfig->isEnableSSL()) {
139 // CRM_Core_Error::fatal('Cannot install this extension - does not support SSL');
140 return array(self::STATUS_DL_ERROR, NULL);
141 }
142
143 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
144 $data = curl_exec($ch);
145 if (curl_errno($ch)) {
146 return array(self::STATUS_DL_ERROR, $data);
147 }
148 else {
149 curl_close($ch);
150 }
151
152 return array(self::STATUS_OK, $data);
153 }
154
155 /**
156 * Send an HTTP POST for a remote resource.
157 *
158 * @param string $remoteFile
159 * URL of a .zip file.
160 * @param array $params
161 *
162 * @return array
163 * array(0 => STATUS_OK|STATUS_DL_ERROR, 1 => string)
164 */
165 public function post($remoteFile, $params) {
166 // Download extension zip file ...
167 if (!function_exists('curl_init')) {
168 //CRM_Core_Error::fatal('Cannot install this extension - curl is not installed!');
169 return array(self::STATUS_DL_ERROR, NULL);
170 }
171
172 list($ch, $caConfig) = $this->createCurl($remoteFile);
173
174 if (preg_match('/^https:/', $remoteFile) && !$caConfig->isEnableSSL()) {
175 // CRM_Core_Error::fatal('Cannot install this extension - does not support SSL');
176 return array(self::STATUS_DL_ERROR, NULL);
177 }
178
179 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
180 curl_setopt($ch, CURLOPT_POST, TRUE);
181 curl_setopt($ch, CURLOPT_POST, count($params));
182 curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
183 $data = curl_exec($ch);
184 if (curl_errno($ch)) {
185 return array(self::STATUS_DL_ERROR, $data);
186 }
187 else {
188 curl_close($ch);
189 }
190
191 return array(self::STATUS_OK, $data);
192 }
193
194 /**
195 * @param string $remoteFile
196 * @return array
197 * (0 => resource, 1 => CA_Config_Curl)
198 */
199 protected function createCurl($remoteFile) {
200 $caConfig = CA_Config_Curl::probe(array(
201 'verify_peer' => (bool) CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'verifySSL'),
202 ));
203
204 $ch = curl_init();
205 curl_setopt($ch, CURLOPT_URL, $remoteFile);
206 curl_setopt($ch, CURLOPT_HEADER, FALSE);
207 curl_setopt($ch, CURLOPT_ENCODING, 'gzip');
208 curl_setopt($ch, CURLOPT_VERBOSE, 0);
209 if ($this->isRedirectSupported()) {
210 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
211 }
212 if ($this->connectionTimeout !== NULL) {
213 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->connectionTimeout);
214 }
215 if (preg_match('/^https:/', $remoteFile) && $caConfig->isEnableSSL()) {
216 curl_setopt_array($ch, $caConfig->toCurlOptions());
217 }
218
219 return array($ch, $caConfig);
220 }
221
222 /**
223 * @return bool
224 */
225 public function isRedirectSupported() {
226 return (ini_get('open_basedir') == '') && (ini_get('safe_mode') == 'Off' || ini_get('safe_mode') == '' || ini_get('safe_mode') === FALSE);
227 }
228
229 }