1 <?php
2 /*
3 Copyright (c) 2012, University of Cambridge Computing Service
4
5 This file is part of the Lookup/Ibis client library.
6
7 This library is free software: you can redistribute it and/or modify
8 it under the terms of the GNU Lesser General Public License as published
9 by the Free Software Foundation, either version 3 of the License, or
10 (at your option) any later version.
11
12 This library is distributed in the hope that it will be useful, but
13 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
15 License for more details.
16
17 You should have received a copy of the GNU Lesser General Public License
18 along with this library. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21 require_once "ClientConnection.php";
22 require_once dirname(__FILE__) . "/../dto/IbisResult.php";
23
24 /**
25 * Default implementation of the {@link ClientConnection} interface, to allow
26 * methods in the Lookup/Ibis web service API to be invoked.
27 *
28 * @author Dean Rasheed (dev-group@ucs.cam.ac.uk)
29 */
30 class IbisClientConnection implements ClientConnection
31 {
32 /** The base URL to the Lookup/Ibis web service API. */
33 protected $urlBase = "";
34
35 /** Whether or not to allow self-signed certificates. */
36 protected $allowSelfSigned = false;
37
38 /** Username for HTTP basic authentication. */
39 private $username = "anonymous";
40
41 /** Password for HTTP basic authentication. */
42 private $password = "";
43
44 /** The HTTP basic authentication authorization string. */
45 private $authorization = "";
46
47 /** Whether to ask for flattened XML (recommended for efficiency). */
48 protected $flatXML = true;
49
50 /**
51 * Create an IbisClientConnection to the Lookup/Ibis web service API at
52 * {@link https://www.lookup.cam.ac.uk/}.
53 *
54 * The connection is initially anonymous, but this may be changed using
55 * {@link setUsername()} and {@link setPassword()}.
56 *
57 * @return IbisClientConnection the connection to the Lookup/Ibis server.
58 */
59 public static function createConnection()
60 {
61 return new IbisClientConnection("https://www.lookup.cam.ac.uk/", true);
62 }
63
64 /**
65 * Create an IbisClientConnection to the Lookup/Ibis test web service API
66 * at {@link https://lookup-test.srv.uis.cam.ac.uk/}.
67 *
68 * The connection is initially anonymous, but this may be changed using
69 * {@link setUsername()} and {@link setPassword()}.
70 *
71 * NOTE: This test server is not guaranteed to always be available, and
72 * the data in it may be out of sync with the data on the live system.
73 *
74 * @return IbisClientConnection the connection to the Lookup/Ibis test
75 * server.
76 */
77 public static function createTestConnection()
78 {
79 return new IbisClientConnection("https://lookup-test.srv.uis.cam.ac.uk/", true);
80 }
81
82 /**
83 * Create an IbisClientConnection to a Lookup/Ibis web service API
84 * running locally on {@link https://localhost:8443/ibis/}.
85 *
86 * The connection is initially anonymous, but this may be changed using
87 * {@link setUsername()} and {@link setPassword()}.
88 *
89 * This is intended for testing during development. The local server is
90 * assumed to be using self-signed certificates, which will not be
91 * checked.
92 *
93 * @return IbisClientConnection the connection to a local Lookup/Ibis
94 * server.
95 */
96 public static function createLocalConnection()
97 {
98 return new IbisClientConnection("https://localhost:8443/ibis/", false);
99 }
100
101 /**
102 * Create a new IbisClientConnection using the specified URL base, which
103 * should be something like {@link https://www.lookup.cam.ac.uk/}.
104 * It is strongly recommended that certificate checking be enabled.
105 *
106 * The connection is initially anonymous, but this may be changed using
107 * {@link setUsername()} and {@link setPassword()}.
108 *
109 * @param string $urlBase The base URL to the Lookup/Ibis web service
110 * API.
111 * @param boolean $checkCertificates If this is ``true`` the server's
112 * certificates will be checked. Otherwise, the they will not, and the
113 * connection may be insecure.
114 * @see createConnection()
115 * @see createTestConnection()
116 */
117 public function __construct($urlBase, $checkCertificates)
118 {
119 $this->urlBase = $urlBase;
120 $this->allowSelfSigned = !$checkCertificates;
121
122 // Initially use anonymous authentication
123 $this->setUsername("anonymous");
124 $this->setPassword("");
125 }
126
127 /*
128 * Update the authorization string for HTTP basic authentication, in
129 * response to a change in the username or password.
130 */
131 private function updateAuthorization()
132 {
133 $credentials = $this->username . ":" . $this->password;
134 $auth = base64_encode($credentials);
135 $this->authorization = "Authorization: Basic " . $auth;
136 }
137
138 /* @see ClientConnection::setUsername(string) */
139 public function setUsername($username)
140 {
141 $this->username = $username;
142 $this->updateAuthorization();
143 }
144
145 /* @see ClientConnection::setPassword(string) */
146 public function setPassword($password)
147 {
148 $this->password = $password;
149 $this->updateAuthorization();
150 }
151
152 /*
153 * Convert an arbitrary value to a string for use as a parameter to be
154 * sent to the server.
155 */
156 private function valueToString($value)
157 {
158 if (is_bool($value))
159 return $value ? "true" : "false";
160 if ($value instanceof DateTime)
161 return $value->format("d M Y");
162 if ($value instanceof IbisAttribute)
163 return $value->encodedString();
164 return (string )$value;
165 }
166
167 /**
168 * Build the full URL needed to invoke a method in the web service API.
169 *
170 * The path may contain standard Java format specifiers, which will be
171 * substituted from the path parameters (suitably URL-encoded). Thus
172 * for example, given the following arguments:
173 *
174 * * path = "api/v1/person/%1$s/%2$s"
175 * * pathParams = ["crsid", "dar17"]
176 * * queryParams = ["fetch" => "email,title"]
177 *
178 * this method will create a URL like
179 * https://www.lookup.cam.ac.uk/api/v1/person/crsid/dar17?fetch=email%2Ctitle.
180 *
181 * Note that all parameter values are automatically URL-encoded.
182 *
183 * @param string $path The basic path to the method, relative to the URL
184 * base.
185 * @param string[] $pathParams Any path parameters that should be inserted
186 * into the path in place of any format specifiers.
187 * @param array $queryParams Any query parameters to add as part of the
188 * URL's query string.
189 * @return string The complete URL.
190 */
191 protected function buildURL($path, $pathParams, $queryParams)
192 {
193 $url = $this->urlBase;
194 if (strcasecmp(substr($url, 0, 5), "https") != 0)
195 throw new Exception("Illegal URL protocol - must use HTTPS");
196
197 $haveQueryParams = false;
198 $haveFlattenParam = false;
199
200 // Substitute any path parameters
201 $path = is_null($path) ? "" : $path;
202 if (isset($pathParams))
203 {
204 $encodedPathParams = array();
205 foreach ($pathParams as $pathParam)
206 $encodedPathParams[] = urlencode($pathParam);
207 $path = vsprintf($path, $encodedPathParams);
208 }
209
210 // Add the path to the common URL base
211 if (substr($url, -1) !== "/")
212 $url .= "/";
213
214 while (substr($path, 0, 1) === "/")
215 $path = substr($path, 1);
216 while (substr($path, -1) === "/")
217 $path = substr($path, 0, -1);
218
219 if (isset($path))
220 $url .= $path;
221
222 // Add any query parameters
223 if (isset($queryParams))
224 {
225 foreach ($queryParams as $queryParam => $value)
226 {
227 if (isset($queryParam) && isset($value))
228 {
229 $name = (string )$queryParam;
230 $val = $this->valueToString($value);
231
232 $url .= $haveQueryParams ? "&" : "?";
233 $url .= urlencode($name);
234 $url .= "=";
235 $url .= urlencode($val);
236 $haveQueryParams = true;
237 if ($queryParam === "flatten")
238 $haveFlattenParam = true;
239 }
240 }
241 }
242
243 // If the flattened XML representation is being used, add the
244 // "flatten" parameter, unless it has already been specified
245 if ($this->flatXML && !$haveFlattenParam)
246 {
247 $url .= $haveQueryParams ? "&" : "?";
248 $url .= "flatten=true";
249 }
250
251 return $url;
252 }
253
254 /* @see ClientConnection::invokeGetMethod(string, string[], array) */
255 public function invokeGetMethod($path, $pathParams, $queryParams)
256 {
257 return $this->invokeMethod("GET", $path, $pathParams, $queryParams);
258 }
259
260 /* @see ClientConnection::invokeMethod(string, string, string[], array, array) */
261 public function invokeMethod($method, $path, $pathParams,
262 $queryParams, $formParams=null)
263 {
264 // Build the URL
265 $headers = array($this->authorization, "Accept: application/xml");
266 $url = $this->buildURL($path, $pathParams, $queryParams);
267 $content = "";
268
269 // Build any content to send from any form parameters
270 if (isset($formParams) && !empty($formParams))
271 {
272 $strFormParams = array();
273 foreach ($formParams as $formParam => $value)
274 {
275 $name = (string )$formParam;
276 $val = $this->valueToString($value);
277 $strFormParams[$name] = $val;
278 }
279 $content = http_build_query($strFormParams);
280 $headers[] = "Content-type: application/x-www-form-urlencoded";
281 }
282
283 // Set up the HTTPS request headers
284 $http_options = array("method" => $method,
285 "header" => $headers,
286 "content" => $content,
287 "ignore_errors" => true);
288
289 $ssl_options = array("verify_peer" => true,
290 "allow_self_signed" => $this->allowSelfSigned);
291
292 $ctx_params = array("http" => $http_options,
293 "ssl" => $ssl_options);
294
295 // Send the request and check if we got XML back
296 $ctx = stream_context_create($ctx_params);
297 $file = fopen($url, "r", false, $ctx);
298 $status = "200";
299 $code = "OK";
300 $gotXml = false;
301
302 foreach ($http_response_header as $header)
303 {
304 if (stripos($header, "http") === 0)
305 {
306 $a = explode(" ", $header);
307 $status = $a[1];
308 $code = $a[2];
309 }
310 if (stripos($header, "content-type: application/xml") !== false)
311 $gotXml = true;
312 }
313
314 if (!$gotXml)
315 {
316 // We didn't get XML back so create an IbisResult containing a
317 // suitable IbisError
318 $error = new IbisError(array("status" => $status,
319 "code" => $code));
320 $error->message = "Unexpected result from server";
321 $error->details = fread($file, 1000000);
322 fclose($file);
323
324 $result = new IbisResult();
325 $result->error = $error;
326
327 return $result;
328 }
329
330 // Parse the XML result into an IbisResult object
331 $parser = new IbisResultParser();
332 $result = $parser->parseXmlFile($file);
333 fclose($file);
334
335 return $result;
336 }
337 }
338