PageRenderTime 36ms CodeModel.GetById 13ms app.highlight 18ms RepoModel.GetById 1ms app.codeStats 0ms

/classes/controller/rest.php

http://github.com/fuel/core
PHP | 525 lines | 320 code | 73 blank | 132 comment | 46 complexity | 8d8fc3e2d8c696a9737a470197df325f MD5 | raw file
  1<?php
  2/**
  3 * Fuel is a fast, lightweight, community driven PHP 5.4+ framework.
  4 *
  5 * @package    Fuel
  6 * @version    1.9-dev
  7 * @author     Fuel Development Team
  8 * @license    MIT License
  9 * @copyright  2010 - 2019 Fuel Development Team
 10 * @link       https://fuelphp.com
 11 */
 12
 13namespace Fuel\Core;
 14
 15abstract class Controller_Rest extends \Controller
 16{
 17	/**
 18	 * @var  null|string  Set this in a controller to use a default format
 19	 */
 20	protected $rest_format = null;
 21
 22	/**
 23	 * @var  array  contains a list of method properties such as limit, log and level
 24	 */
 25	protected $methods = array();
 26
 27	/**
 28	 * @var  integer  status code to return in case a not defined action is called
 29	 */
 30	protected $no_method_status = 405;
 31
 32	/**
 33	 * @var  integer  status code to return in case the called action doesn't return data
 34	 */
 35	protected $no_data_status = 204;
 36
 37	/**
 38	 * @var  string  authentication to be used for this controller
 39	 */
 40	protected $auth = null;
 41
 42	/**
 43	 * @var  string  the detected response format
 44	 */
 45	protected $format = null;
 46
 47	/**
 48	 * @var  integer  default response http status
 49	 */
 50	protected $http_status = 200;
 51
 52	/**
 53	 * @var  string  xml basenode name
 54	 */
 55	protected $xml_basenode = null;
 56
 57	/**
 58	 * @var  array  List all supported methods
 59	 */
 60	protected $_supported_formats = array(
 61		'xml' => 'application/xml',
 62		'rawxml' => 'application/xml',
 63		'json' => 'application/json',
 64		'jsonp'=> 'text/javascript',
 65		'serialized' => 'application/vnd.php.serialized',
 66		'php' => 'text/plain',
 67		'html' => 'text/html',
 68		'csv' => 'application/csv',
 69	);
 70
 71	public function before()
 72	{
 73		parent::before();
 74
 75		// Some Methods cant have a body
 76		$this->request->body = null;
 77
 78		// Which format should the data be returned in?
 79		$this->request->lang = $this->_detect_lang();
 80
 81		$this->response = \Response::forge();
 82	}
 83
 84	public function after($response)
 85	{
 86		// If the response is an array
 87		if (is_array($response))
 88		{
 89			// set the response
 90			$response = $this->response($response);
 91		}
 92
 93		// If the response is a Response object, we will use their
 94		// instead of ours.
 95		if ( ! $response instanceof \Response)
 96		{
 97			$response = $this->response;
 98		}
 99
100		return parent::after($response);
101	}
102
103	/**
104	 * Router
105	 *
106	 * Requests are not made to methods directly The request will be for an "object".
107	 * this simply maps the object and method to the correct Controller method.
108	 *
109	 * @param  string $resource
110	 * @param  array $arguments
111	 * @return bool|mixed
112	 */
113	public function router($resource, $arguments)
114	{
115		\Config::load('rest', true);
116
117		// If no (or an invalid) format is given, auto detect the format
118		if (is_null($this->format) or ! array_key_exists($this->format, $this->_supported_formats))
119		{
120			// auto-detect the format
121			$this->format = array_key_exists(\Input::extension(), $this->_supported_formats) ? \Input::extension() : $this->_detect_format();
122		}
123
124		// Get the configured auth method if none is defined
125		$this->auth === null and $this->auth = \Config::get('rest.auth');
126
127		//Check method is authorized if required, and if we're authorized
128		if ($this->auth == 'basic')
129		{
130			$valid_login = $this->_prepare_basic_auth();
131		}
132		elseif ($this->auth == 'digest')
133		{
134			$valid_login = $this->_prepare_digest_auth();
135		}
136		elseif (method_exists($this, $this->auth))
137		{
138			if (($valid_login = $this->{$this->auth}()) instanceOf \Response)
139			{
140				return $valid_login;
141			}
142		}
143		else
144		{
145			$valid_login = false;
146		}
147
148		//If the request passes auth then execute as normal
149		if(empty($this->auth) or $valid_login)
150		{
151			// If they call user, go to $this->post_user();
152			$controller_method = strtolower(\Input::method()) . '_' . $resource;
153
154			// Fall back to action_ if no rest method is provided
155			if ( ! method_exists($this, $controller_method))
156			{
157				$controller_method = 'action_'.$resource;
158			}
159
160			// If method is not available, set status code to 404
161			if (method_exists($this, $controller_method))
162			{
163				return call_fuel_func_array(array($this, $controller_method), $arguments);
164			}
165			else
166			{
167				$this->response->status = $this->no_method_status;
168				return;
169			}
170		}
171		else
172		{
173			$this->response(array('status'=> 0, 'error'=> 'Not Authorized'), 401);
174		}
175	}
176
177	/**
178	 * Response
179	 *
180	 * Takes pure data and optionally a status code, then creates the response
181	 *
182	 * @param   mixed
183	 * @param   int
184	 * @return  object  Response instance
185	 */
186	protected function response($data = array(), $http_status = null)
187	{
188		// set the correct response header
189		if (method_exists('Format', 'to_'.$this->format))
190		{
191			$this->response->set_header('Content-Type', $this->_supported_formats[$this->format]);
192		}
193
194		// no data returned?
195		if ((is_array($data) and empty($data)) or ($data == ''))
196		{
197			// override the http status with the NO CONTENT status
198			$http_status = $this->no_data_status;
199		}
200
201		// make sure we have a valid return status
202		$http_status or $http_status = $this->http_status;
203
204		// If the format method exists, call and return the output in that format
205		if (method_exists('Format', 'to_'.$this->format))
206		{
207			// Handle XML output
208			if ($this->format === 'xml')
209			{
210				// Detect basenode
211				$xml_basenode = $this->xml_basenode;
212				$xml_basenode or $xml_basenode = \Config::get('rest.xml_basenode', 'xml');
213
214				// Set the XML response
215				$this->response->body(\Format::forge($data)->{'to_'.$this->format}(null, null, $xml_basenode));
216			}
217			else
218			{
219				// Set the formatted response
220				$this->response->body(\Format::forge($data)->{'to_'.$this->format}());
221			}
222		}
223
224		// Format not supported, but the output is an array or an object that can not be cast to string
225		elseif (is_array($data) or (is_object($data) and ! method_exists($data, '__toString')))
226		{
227			if (\Fuel::$env == \Fuel::PRODUCTION)
228			{
229				// not acceptable in production
230				if ($http_status == 200)
231				{	$http_status = 406;
232				}
233				$this->response->body('The requested REST method returned an array or object, which is not compatible with the output format "'.$this->format.'"');
234			}
235			else
236			{
237				// convert it to json so we can at least read it while we're developing
238				$this->response->body('The requested REST method returned an array or object:<br /><br />'.\Format::forge($data)->to_json(null, true));
239			}
240		}
241
242		// Format not supported, output directly
243		else
244		{
245			$this->response->body($data);
246		}
247
248		// Set the reponse http status
249		$http_status and $this->response->status = $http_status;
250
251		return $this->response;
252	}
253
254	/**
255	 * Set the Response http status.
256	 *
257	 * @param   integer  $status  response http status code
258	 * @return  void
259	 */
260	protected function http_status($status)
261	{
262		$this->http_status = $status;
263	}
264
265	/**
266	 * Detect format
267	 *
268	 * Detect which format should be used to output the data
269	 *
270	 * @return  string
271	 */
272	protected function _detect_format()
273	{
274		// A format has been passed as a named parameter in the route
275		if ($this->param('format') and array_key_exists($this->param('format'), $this->_supported_formats))
276		{
277			return $this->param('format');
278		}
279
280		// A format has been passed as an argument in the URL and it is supported
281		if (\Input::param('format') and array_key_exists(\Input::param('format'), $this->_supported_formats))
282		{
283			return \Input::param('format');
284		}
285
286		// Otherwise, check the HTTP_ACCEPT (if it exists and we are allowed)
287		if ($acceptable = \Input::server('HTTP_ACCEPT') and \Config::get('rest.ignore_http_accept') !== true)
288		{
289			// If anything is accepted, and we have a default, return that
290			if ($acceptable == '*/*' and ! empty($this->rest_format))
291			{
292				return $this->rest_format;
293			}
294
295			// Split the Accept header and build an array of quality scores for each format
296			$fragments = new \CachingIterator(new \ArrayIterator(preg_split('/[,;]/', $acceptable)));
297			$acceptable = array();
298			$next_is_quality = false;
299			foreach ($fragments as $fragment)
300			{
301				$quality = 1;
302				// Skip the fragment if it is a quality score
303				if ($next_is_quality)
304				{
305					$next_is_quality = false;
306					continue;
307				}
308
309				// If next fragment exists and is a quality score, set the quality score
310				elseif ($fragments->hasNext())
311				{
312					$next = $fragments->getInnerIterator()->current();
313					if (strpos($next, 'q=') === 0)
314					{
315						list($key, $quality) = explode('=', $next);
316						$next_is_quality = true;
317					}
318				}
319
320				$acceptable[$fragment] = $quality;
321			}
322
323			// Sort the formats by score in descending order
324			uasort($acceptable, function($a, $b)
325			{
326				$a = (float) $a;
327				$b = (float) $b;
328				return ($a > $b) ? -1 : 1;
329			});
330
331			// Check each of the acceptable formats against the supported formats
332			$find = array('\*', '/');
333			$replace = array('.*', '\/');
334			foreach ($acceptable as $pattern => $quality)
335			{
336				// The Accept header can contain wildcards in the format
337				$pattern = '/^' . str_replace($find, $replace, preg_quote($pattern)) . '$/';
338				foreach ($this->_supported_formats as $format => $mime)
339				{
340					if (preg_match($pattern, $mime))
341					{
342						return $format;
343					}
344				}
345			}
346		} // End HTTP_ACCEPT checking
347
348		// Well, none of that has worked! Let's see if the controller has a default
349		if ( ! empty($this->rest_format))
350		{
351			return $this->rest_format;
352		}
353
354		// Just use the default format
355		return \Config::get('rest.default_format');
356	}
357
358	/**
359	 * Detect language(s)
360	 *
361	 * What language do they want it in?
362	 *
363	 * @return  null|array|string
364	 */
365	protected function _detect_lang()
366	{
367		if (!$lang = \Input::server('HTTP_ACCEPT_LANGUAGE'))
368		{
369			return null;
370		}
371
372		// They might have sent a few, make it an array
373		if (strpos($lang, ',') !== false)
374		{
375			$langs = explode(',', $lang);
376
377			$return_langs = array();
378
379			foreach ($langs as $lang)
380			{
381				// Remove weight and strip space
382				list($lang) = explode(';', $lang);
383				$return_langs[] = trim($lang);
384			}
385
386			return $return_langs;
387		}
388
389		// Nope, just return the string
390		return $lang;
391	}
392
393	// SECURITY FUNCTIONS ---------------------------------------------------------
394
395	protected function _check_login($username = '', $password = null)
396	{
397		if (empty($username))
398		{
399			return false;
400		}
401
402		$valid_logins = \Config::get('rest.valid_logins');
403
404		if (!array_key_exists($username, $valid_logins))
405		{
406			return false;
407		}
408
409		// If actually null (not empty string) then do not check it
410		if ($password !== null and $valid_logins[$username] != $password)
411		{
412			return false;
413		}
414
415		return true;
416	}
417
418	protected function _prepare_basic_auth()
419	{
420		$username = null;
421		$password = null;
422
423		// mod_php
424		if (\Input::server('PHP_AUTH_USER'))
425		{
426			$username = \Input::server('PHP_AUTH_USER');
427			$password = \Input::server('PHP_AUTH_PW');
428		}
429
430		// most other servers
431		elseif (\Input::server('HTTP_AUTHENTICATION'))
432		{
433			if (strpos(strtolower(\Input::server('HTTP_AUTHENTICATION')), 'basic') === 0)
434			{
435				list($username, $password) = explode(':', base64_decode(substr(\Input::server('HTTP_AUTHORIZATION'), 6)));
436			}
437		}
438
439		if ( ! static::_check_login($username, $password))
440		{
441			static::_force_login();
442			return false;
443		}
444
445		return true;
446	}
447
448	protected function _prepare_digest_auth()
449	{
450		// Empty argument for backward compatibility
451		$uniqid = uniqid("");
452
453		// We need to test which server authentication variable to use
454		// because the PHP ISAPI module in IIS acts different from CGI
455		if (\Input::server('PHP_AUTH_DIGEST'))
456		{
457			$digest_string = \Input::server('PHP_AUTH_DIGEST');
458		}
459		elseif (\Input::server('HTTP_AUTHORIZATION'))
460		{
461			$digest_string = \Input::server('HTTP_AUTHORIZATION');
462		}
463		else
464		{
465			$digest_string = '';
466		}
467
468		// Prompt for authentication if we don't have a digest string
469		if (empty($digest_string))
470		{
471			static::_force_login($uniqid);
472			return false;
473		}
474
475		// We need to retrieve authentication informations from the $digest_string variable
476		$digest_params = explode(',', $digest_string);
477		foreach ($digest_params as $digest_param)
478		{
479			$digest_param = explode('=', trim($digest_param), 2);
480			if (isset($digest_param[1]))
481			{
482				$digest[$digest_param[0]] = trim($digest_param[1], '"');
483			}
484		}
485
486		// if no username, or an invalid username found, re-authenticate
487		if ( ! array_key_exists('username', $digest) or ! static::_check_login($digest['username']))
488		{
489			static::_force_login($uniqid);
490			return false;
491		}
492
493		// validate the configured login/password
494		$valid_logins = \Config::get('rest.valid_logins');
495		$valid_pass = $valid_logins[$digest['username']];
496
497		// This is the valid response expected
498		$A1 = md5($digest['username'] . ':' . \Config::get('rest.realm') . ':' . $valid_pass);
499		$A2 = md5(strtoupper(\Input::method()) . ':' . $digest['uri']);
500		$valid_response = md5($A1 . ':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' . $A2);
501
502		if ($digest['response'] != $valid_response)
503		{
504			return false;
505		}
506
507		return true;
508	}
509
510	protected function _force_login($nonce = '')
511	{
512		// Get the configured auth method if none is defined
513		$this->auth === null and $this->auth = \Config::get('rest.auth');
514
515		if ($this->auth == 'basic')
516		{
517			$this->response->set_header('WWW-Authenticate', 'Basic realm="'. \Config::get('rest.realm') . '"');
518		}
519		elseif ($this->auth == 'digest')
520		{
521			$this->response->set_header('WWW-Authenticate', 'Digest realm="' . \Config::get('rest.realm') . '", qop="auth", nonce="' . $nonce . '", opaque="' . md5(\Config::get('rest.realm')) . '"');
522		}
523	}
524
525}