Upgrade phpCAS
[piwik-CASLogin.git] / CAS / CAS / ProxiedService / Http / Abstract.php
1 <?php
2
3 /**
4  * Licensed to Jasig under one or more contributor license
5  * agreements. See the NOTICE file distributed with this work for
6  * additional information regarding copyright ownership.
7  *
8  * Jasig licenses this file to you under the Apache License,
9  * Version 2.0 (the "License"); you may not use this file except in
10  * compliance with the License. You may obtain a copy of the License at:
11  *
12  * http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  *
20  * PHP Version 5
21  *
22  * @file     CAS/ProxiedService/Http/Abstract.php
23  * @category Authentication
24  * @package  PhpCAS
25  * @author   Adam Franco <afranco@middlebury.edu>
26  * @license  http://www.apache.org/licenses/LICENSE-2.0  Apache License 2.0
27  * @link     https://wiki.jasig.org/display/CASC/phpCAS
28  */
29
30 /**
31  * This class implements common methods for ProxiedService implementations included
32  * with phpCAS.
33  *
34  * @class    CAS_ProxiedService_Http_Abstract
35  * @category Authentication
36  * @package  PhpCAS
37  * @author   Adam Franco <afranco@middlebury.edu>
38  * @license  http://www.apache.org/licenses/LICENSE-2.0  Apache License 2.0
39  * @link     https://wiki.jasig.org/display/CASC/phpCAS
40  */
41 abstract class CAS_ProxiedService_Http_Abstract
42 extends CAS_ProxiedService_Abstract
43 implements CAS_ProxiedService_Http
44 {
45     /**
46      * The HTTP request mechanism talking to the target service.
47      *
48      * @var CAS_Request_RequestInterface $requestHandler
49      */
50     protected $requestHandler;
51
52     /**
53      * The storage mechanism for cookies set by the target service.
54      *
55      * @var CAS_CookieJar $_cookieJar
56      */
57     private $_cookieJar;
58
59     /**
60      * Constructor.
61      *
62      * @param CAS_Request_RequestInterface $requestHandler request handler object
63      * @param CAS_CookieJar                $cookieJar      cookieJar object
64      *
65      * @return void
66      */
67     public function __construct (CAS_Request_RequestInterface $requestHandler, CAS_CookieJar $cookieJar)
68     {
69         $this->requestHandler = $requestHandler;
70         $this->_cookieJar = $cookieJar;
71     }
72
73     /**
74      * The target service url.
75      * @var string $_url;
76      */
77     private $_url;
78
79     /**
80      * Answer a service identifier (URL) for whom we should fetch a proxy ticket.
81      *
82      * @return string
83      * @throws Exception If no service url is available.
84      */
85     public function getServiceUrl ()
86     {
87         if (empty($this->_url)) {
88             throw new CAS_ProxiedService_Exception('No URL set via '.get_class($this).'->setUrl($url).');
89         }
90
91         return $this->_url;
92     }
93
94     /*********************************************************
95      * Configure the Request
96     *********************************************************/
97
98     /**
99      * Set the URL of the Request
100      *
101      * @param string $url url to set
102      *
103      * @return void
104      * @throws CAS_OutOfSequenceException If called after the Request has been sent.
105      */
106     public function setUrl ($url)
107     {
108         if ($this->hasBeenSent()) {
109             throw new CAS_OutOfSequenceException('Cannot set the URL, request already sent.');
110         }
111         if (!is_string($url)) {
112             throw new CAS_InvalidArgumentException('$url must be a string.');
113         }
114
115         $this->_url = $url;
116     }
117
118     /*********************************************************
119      * 2. Send the Request
120     *********************************************************/
121
122     /**
123      * Perform the request.
124      *
125      * @return void
126      * @throws CAS_OutOfSequenceException If called multiple times.
127      * @throws CAS_ProxyTicketException If there is a proxy-ticket failure.
128      *          The code of the Exception will be one of:
129      *                  PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE
130      *                  PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE
131      *                  PHPCAS_SERVICE_PT_FAILURE
132      * @throws CAS_ProxiedService_Exception If there is a failure sending the
133      * request to the target service.
134      */
135     public function send ()
136     {
137         if ($this->hasBeenSent()) {
138             throw new CAS_OutOfSequenceException('Cannot send, request already sent.');
139         }
140
141         phpCAS::traceBegin();
142
143         // Get our proxy ticket and append it to our URL.
144         $this->initializeProxyTicket();
145         $url = $this->getServiceUrl();
146         if (strstr($url, '?') === false) {
147             $url = $url.'?ticket='.$this->getProxyTicket();
148         } else {
149             $url = $url.'&ticket='.$this->getProxyTicket();
150         }
151
152         try {
153             $this->makeRequest($url);
154         } catch (Exception $e) {
155             phpCAS::traceEnd();
156             throw $e;
157         }
158     }
159
160     /**
161      * Indicator of the number of requests (including redirects performed.
162      *
163      * @var int $_numRequests;
164      */
165     private $_numRequests = 0;
166
167     /**
168      * The response headers.
169      *
170      * @var array $_responseHeaders;
171      */
172     private $_responseHeaders = array();
173
174     /**
175      * The response status code.
176      *
177      * @var string $_responseStatusCode;
178      */
179     private $_responseStatusCode = '';
180
181     /**
182      * The response headers.
183      *
184      * @var string $_responseBody;
185      */
186     private $_responseBody = '';
187
188     /**
189      * Build and perform a request, following redirects
190      *
191      * @param string $url url for the request
192      *
193      * @return void
194      * @throws CAS_ProxyTicketException If there is a proxy-ticket failure.
195      *          The code of the Exception will be one of:
196      *                  PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE
197      *                  PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE
198      *                  PHPCAS_SERVICE_PT_FAILURE
199      * @throws CAS_ProxiedService_Exception If there is a failure sending the
200      * request to the target service.
201      */
202     protected function makeRequest ($url)
203     {
204         // Verify that we are not in a redirect loop
205         $this->_numRequests++;
206         if ($this->_numRequests > 4) {
207             $message = 'Exceeded the maximum number of redirects (3) in proxied service request.';
208             phpCAS::trace($message);
209             throw new CAS_ProxiedService_Exception($message);
210         }
211
212         // Create a new request.
213         $request = clone $this->requestHandler;
214         $request->setUrl($url);
215
216         // Add any cookies to the request.
217         $request->addCookies($this->_cookieJar->getCookies($url));
218
219         // Add any other parts of the request needed by concrete classes
220         $this->populateRequest($request);
221
222         // Perform the request.
223         phpCAS::trace('Performing proxied service request to \''.$url.'\'');
224         if (!$request->send()) {
225             $message = 'Could not perform proxied service request to URL`'.$url.'\'. '.$request->getErrorMessage();
226             phpCAS::trace($message);
227             throw new CAS_ProxiedService_Exception($message);
228         }
229
230         // Store any cookies from the response;
231         $this->_cookieJar->storeCookies($url, $request->getResponseHeaders());
232
233         // Follow any redirects
234         if ($redirectUrl = $this->getRedirectUrl($request->getResponseHeaders())) {
235             phpCAS :: trace('Found redirect:'.$redirectUrl);
236             $this->makeRequest($redirectUrl);
237         } else {
238
239             $this->_responseHeaders = $request->getResponseHeaders();
240             $this->_responseBody = $request->getResponseBody();
241             $this->_responseStatusCode = $request->getResponseStatusCode();
242         }
243     }
244
245     /**
246      * Add any other parts of the request needed by concrete classes
247      *
248      * @param CAS_Request_RequestInterface $request request interface object
249      *
250      * @return void
251      */
252     abstract protected function populateRequest (CAS_Request_RequestInterface $request);
253
254     /**
255      * Answer a redirect URL if a redirect header is found, otherwise null.
256      *
257      * @param array $responseHeaders response header to extract a redirect from
258      *
259      * @return string or null
260      */
261     protected function getRedirectUrl (array $responseHeaders)
262     {
263         // Check for the redirect after authentication
264         foreach ($responseHeaders as $header) {
265             if (preg_match('/^(Location:|URI:)\s*([^\s]+.*)$/', $header, $matches)) {
266                 return trim(array_pop($matches));
267             }
268         }
269         return null;
270     }
271
272     /*********************************************************
273      * 3. Access the response
274     *********************************************************/
275
276     /**
277      * Answer true if our request has been sent yet.
278      *
279      * @return bool
280      */
281     protected function hasBeenSent ()
282     {
283         return ($this->_numRequests > 0);
284     }
285
286     /**
287      * Answer the headers of the response.
288      *
289      * @return array An array of header strings.
290      * @throws CAS_OutOfSequenceException If called before the Request has been sent.
291      */
292     public function getResponseHeaders ()
293     {
294         if (!$this->hasBeenSent()) {
295             throw new CAS_OutOfSequenceException('Cannot access response, request not sent yet.');
296         }
297
298         return $this->_responseHeaders;
299     }
300
301     /**
302      * Answer HTTP status code of the response
303      *
304      * @return int
305      * @throws CAS_OutOfSequenceException If called before the Request has been sent.
306      */
307     public function getResponseStatusCode ()
308     {
309         if (!$this->hasBeenSent()) {
310             throw new CAS_OutOfSequenceException('Cannot access response, request not sent yet.');
311         }
312
313         return $this->_responseStatusCode;
314     }
315
316     /**
317      * Answer the body of response.
318      *
319      * @return string
320      * @throws CAS_OutOfSequenceException If called before the Request has been sent.
321      */
322     public function getResponseBody ()
323     {
324         if (!$this->hasBeenSent()) {
325             throw new CAS_OutOfSequenceException('Cannot access response, request not sent yet.');
326         }
327
328         return $this->_responseBody;
329     }
330
331     /**
332      * Answer the cookies from the response. This may include cookies set during
333      * redirect responses.
334      *
335      * @return array An array containing cookies. E.g. array('name' => 'val');
336      */
337     public function getCookies ()
338     {
339         return $this->_cookieJar->getCookies($this->getServiceUrl());
340     }
341
342 }
343 ?>