Completed
Pull Request — master (#55)
by Reginaldo
65:36 queued 30:55
created

RequestHandlerComponent::isPost()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
/**
3
 * Request object for handling alternative HTTP requests
4
 *
5
 * Alternative HTTP requests can come from wireless units like mobile phones, palmtop computers,
6
 * and the like. These units have no use for Ajax requests, and this Component can tell how Cake
7
 * should respond to the different needs of a handheld computer and a desktop machine.
8
 *
9
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
10
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
11
 *
12
 * Licensed under The MIT License
13
 * For full copyright and license information, please see the LICENSE.txt
14
 * Redistributions of files must retain the above copyright notice.
15
 *
16
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
17
 * @link          http://cakephp.org CakePHP(tm) Project
18
 * @package       Cake.Controller.Component
19
 * @since         CakePHP(tm) v 0.10.4.1076
20
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
21
 */
22
23
App::uses('Component', 'Controller');
24
App::uses('Xml', 'Utility');
25
26
/**
27
 * Request object for handling alternative HTTP requests
28
 *
29
 * Alternative HTTP requests can come from wireless units like mobile phones, palmtop computers,
30
 * and the like. These units have no use for Ajax requests, and this Component can tell how Cake
31
 * should respond to the different needs of a handheld computer and a desktop machine.
32
 *
33
 * @package       Cake.Controller.Component
34
 * @link http://book.cakephp.org/2.0/en/core-libraries/components/request-handling.html
35
 *
36
 */
37
class RequestHandlerComponent extends Component {
38
39
/**
40
 * The layout that will be switched to for Ajax requests
41
 *
42
 * @var string
43
 * @see RequestHandler::setAjax()
44
 */
45
	public $ajaxLayout = 'ajax';
46
47
/**
48
 * Determines whether or not callbacks will be fired on this component
49
 *
50
 * @var boolean
51
 */
52
	public $enabled = true;
53
54
/**
55
 * Holds the reference to Controller::$request
56
 *
57
 * @var CakeRequest
58
 */
59
	public $request;
60
61
/**
62
 * Holds the reference to Controller::$response
63
 *
64
 * @var CakeResponse
65
 */
66
	public $response;
67
68
/**
69
 * Contains the file extension parsed out by the Router
70
 *
71
 * @var string
72
 * @see Router::parseExtensions()
73
 */
74
	public $ext = null;
75
76
/**
77
 * The template to use when rendering the given content type.
78
 *
79
 * @var string
80
 */
81
	protected $_renderType = null;
82
83
/**
84
 * A mapping between extensions and deserializers for request bodies of that type.
85
 * By default only JSON and XML are mapped, use RequestHandlerComponent::addInputType()
86
 *
87
 * @var array
88
 */
89
	protected $_inputTypeMap = array(
90
		'json' => array('json_decode', true)
91
	);
92
93
/**
94
 * A mapping between type and viewClass
95
 * By default only JSON and XML are mapped, use RequestHandlerComponent::viewClassMap()
96
 *
97
 * @var array
98
 */
99
	protected $_viewClassMap = array(
100
		'json' => 'Json',
101
		'xml' => 'Xml'
102
	);
103
104
/**
105
 * Constructor. Parses the accepted content types accepted by the client using HTTP_ACCEPT
106
 *
107
 * @param ComponentCollection $collection ComponentCollection object.
108
 * @param array $settings Array of settings.
109
 */
110
	public function __construct(ComponentCollection $collection, $settings = array()) {
111
		parent::__construct($collection, $settings + array('checkHttpCache' => true));
112
		$this->addInputType('xml', array(array($this, 'convertXml')));
113
114
		$Controller = $collection->getController();
115
		$this->request = $Controller->request;
116
		$this->response = $Controller->response;
117
	}
118
119
/**
120
 * Checks to see if a file extension has been parsed by the Router, or if the
121
 * HTTP_ACCEPT_TYPE has matches only one content type with the supported extensions.
122
 * If there is only one matching type between the supported content types & extensions,
123
 * and the requested mime-types, RequestHandler::$ext is set to that value.
124
 *
125
 * @param Controller $controller A reference to the controller
126
 * @return void
127
 * @see Router::parseExtensions()
128
 */
129
	public function initialize(Controller $controller) {
130
		if (isset($this->request->params['ext'])) {
131
			$this->ext = $this->request->params['ext'];
132
		}
133
		if (empty($this->ext) || $this->ext === 'html') {
134
			$this->_setExtension();
135
		}
136
		$this->params = $controller->params;
0 ignored issues
show
Documentation introduced by
The property params does not exist on object<Controller>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
137
		if (!empty($this->settings['viewClassMap'])) {
138
			$this->viewClassMap($this->settings['viewClassMap']);
139
		}
140
	}
141
142
/**
143
 * Set the extension based on the accept headers.
144
 * Compares the accepted types and configured extensions.
145
 * If there is one common type, that is assigned as the ext/content type
146
 * for the response.
147
 * Type with the highest weight will be set. If the highest weight has more
148
 * then one type matching the extensions, the order in which extensions are specified
149
 * determines which type will be set.
150
 *
151
 * If html is one of the preferred types, no content type will be set, this
152
 * is to avoid issues with browsers that prefer html and several other content types.
153
 *
154
 * @return void
155
 */
156
	protected function _setExtension() {
157
		$accept = $this->request->parseAccept();
158
		if (empty($accept)) {
159
			return;
160
		}
161
162
		$accepts = $this->response->mapType($accept);
163
		$preferedTypes = current($accepts);
164
		if (array_intersect($preferedTypes, array('html', 'xhtml'))) {
165
			return null;
166
		}
167
168
		$extensions = Router::extensions();
169
		foreach ($accepts as $types) {
0 ignored issues
show
Bug introduced by
The expression $accepts of type array|integer|string|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
170
			$ext = array_intersect($extensions, $types);
171
			if ($ext) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ext of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
172
				$this->ext = current($ext);
173
				break;
174
			}
175
		}
176
	}
177
178
/**
179
 * The startup method of the RequestHandler enables several automatic behaviors
180
 * related to the detection of certain properties of the HTTP request, including:
181
 *
182
 * - Disabling layout rendering for Ajax requests (based on the HTTP_X_REQUESTED_WITH header)
183
 * - If Router::parseExtensions() is enabled, the layout and template type are
184
 *   switched based on the parsed extension or Accept-Type header. For example, if `controller/action.xml`
185
 *   is requested, the view path becomes `app/View/Controller/xml/action.ctp`. Also if
186
 *   `controller/action` is requested with `Accept-Type: application/xml` in the headers
187
 *   the view path will become `app/View/Controller/xml/action.ctp`. Layout and template
188
 *   types will only switch to mime-types recognized by CakeResponse. If you need to declare
189
 *   additional mime-types, you can do so using CakeResponse::type() in your controllers beforeFilter()
190
 *   method.
191
 * - If a helper with the same name as the extension exists, it is added to the controller.
192
 * - If the extension is of a type that RequestHandler understands, it will set that
193
 *   Content-type in the response header.
194
 * - If the XML data is POSTed, the data is parsed into an XML object, which is assigned
195
 *   to the $data property of the controller, which can then be saved to a model object.
196
 *
197
 * @param Controller $controller A reference to the controller
198
 * @return void
199
 */
200
	public function startup(Controller $controller) {
201
		$controller->request->params['isAjax'] = $this->request->is('ajax');
202
		$isRecognized = (
203
			!in_array($this->ext, array('html', 'htm')) &&
204
			$this->response->getMimeType($this->ext)
205
		);
206
207
		if (!empty($this->ext) && $isRecognized) {
208
			$this->renderAs($controller, $this->ext);
209
		} elseif ($this->request->is('ajax')) {
210
			$this->renderAs($controller, 'ajax');
211
		} elseif (empty($this->ext) || in_array($this->ext, array('html', 'htm'))) {
212
			$this->respondAs('html', array('charset' => Configure::read('App.encoding')));
213
		}
214
215
		foreach ($this->_inputTypeMap as $type => $handler) {
216
			if ($this->requestedWith($type)) {
217
				$input = call_user_func_array(array($controller->request, 'input'), $handler);
218
				$controller->request->data = $input;
0 ignored issues
show
Documentation Bug introduced by
It seems like $input of type * is incompatible with the declared type array of property $data.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
219
			}
220
		}
221
	}
222
223
/**
224
 * Helper method to parse xml input data, due to lack of anonymous functions
225
 * this lives here.
226
 *
227
 * @param string $xml
228
 * @return array Xml array data
229
 */
230
	public function convertXml($xml) {
231
		try {
232
			$xml = Xml::build($xml);
233
			if (isset($xml->data)) {
234
				return Xml::toArray($xml->data);
235
			}
236
			return Xml::toArray($xml);
237
		} catch (XmlException $e) {
238
			return array();
239
		}
240
	}
241
242
/**
243
 * Handles (fakes) redirects for Ajax requests using requestAction()
244
 * Modifies the $_POST and $_SERVER['REQUEST_METHOD'] to simulate a new GET request.
245
 *
246
 * @param Controller $controller A reference to the controller
247
 * @param string|array $url A string or array containing the redirect location
248
 * @param integer|array $status HTTP Status for redirect
249
 * @param boolean $exit
250
 * @return void
251
 */
252
	public function beforeRedirect(Controller $controller, $url, $status = null, $exit = true) {
0 ignored issues
show
Coding Style introduced by
beforeRedirect uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
beforeRedirect uses the super-global variable $_POST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
253
		if (!$this->request->is('ajax')) {
254
			return;
255
		}
256
		if (empty($url)) {
257
			return;
258
		}
259
		$_SERVER['REQUEST_METHOD'] = 'GET';
260
		foreach ($_POST as $key => $val) {
261
			unset($_POST[$key]);
262
		}
263
		if (is_array($url)) {
264
			$url = Router::url($url + array('base' => false));
265
		}
266
		if (!empty($status)) {
267
			$statusCode = $this->response->httpCodes($status);
268
			$code = key($statusCode);
269
			$this->response->statusCode($code);
270
		}
271
		$this->response->body($this->requestAction($url, array('return', 'bare' => false)));
0 ignored issues
show
Security Bug introduced by
It seems like $this->requestAction($ur...urn', 'bare' => false)) targeting Object::requestAction() can also be of type false; however, CakeResponse::body() does only seem to accept string|null, did you maybe forget to handle an error condition?
Loading history...
272
		$this->response->send();
273
		$this->_stop();
274
	}
275
276
/**
277
 * Checks if the response can be considered different according to the request
278
 * headers, and the caching response headers. If it was not modified, then the
279
 * render process is skipped. And the client will get a blank response with a
280
 * "304 Not Modified" header.
281
 *
282
 * @params Controller $controller
283
 * @return boolean false if the render process should be aborted
284
 */
285
	public function beforeRender(Controller $controller) {
286
		if ($this->settings['checkHttpCache'] && $this->response->checkNotModified($this->request)) {
287
			return false;
288
		}
289
	}
290
291
/**
292
 * Returns true if the current HTTP request is Ajax, false otherwise
293
 *
294
 * @return boolean True if call is Ajax
295
 * @deprecated use `$this->request->is('ajax')` instead.
296
 */
297
	public function isAjax() {
298
		return $this->request->is('ajax');
299
	}
300
301
/**
302
 * Returns true if the current HTTP request is coming from a Flash-based client
303
 *
304
 * @return boolean True if call is from Flash
305
 * @deprecated use `$this->request->is('flash')` instead.
306
 */
307
	public function isFlash() {
308
		return $this->request->is('flash');
309
	}
310
311
/**
312
 * Returns true if the current request is over HTTPS, false otherwise.
313
 *
314
 * @return boolean True if call is over HTTPS
315
 * @deprecated use `$this->request->is('ssl')` instead.
316
 */
317
	public function isSSL() {
318
		return $this->request->is('ssl');
319
	}
320
321
/**
322
 * Returns true if the current call accepts an XML response, false otherwise
323
 *
324
 * @return boolean True if client accepts an XML response
325
 */
326
	public function isXml() {
327
		return $this->prefers('xml');
328
	}
329
330
/**
331
 * Returns true if the current call accepts an RSS response, false otherwise
332
 *
333
 * @return boolean True if client accepts an RSS response
334
 */
335
	public function isRss() {
336
		return $this->prefers('rss');
337
	}
338
339
/**
340
 * Returns true if the current call accepts an Atom response, false otherwise
341
 *
342
 * @return boolean True if client accepts an RSS response
343
 */
344
	public function isAtom() {
345
		return $this->prefers('atom');
346
	}
347
348
/**
349
 * Returns true if user agent string matches a mobile web browser, or if the
350
 * client accepts WAP content.
351
 *
352
 * @return boolean True if user agent is a mobile web browser
353
 */
354
	public function isMobile() {
355
		return $this->request->is('mobile') || $this->accepts('wap');
356
	}
357
358
/**
359
 * Returns true if the client accepts WAP content
360
 *
361
 * @return boolean
362
 */
363
	public function isWap() {
364
		return $this->prefers('wap');
365
	}
366
367
/**
368
 * Returns true if the current call a POST request
369
 *
370
 * @return boolean True if call is a POST
371
 * @deprecated Use $this->request->is('post'); from your controller.
372
 */
373
	public function isPost() {
374
		return $this->request->is('post');
375
	}
376
377
/**
378
 * Returns true if the current call a PUT request
379
 *
380
 * @return boolean True if call is a PUT
381
 * @deprecated Use $this->request->is('put'); from your controller.
382
 */
383
	public function isPut() {
384
		return $this->request->is('put');
385
	}
386
387
/**
388
 * Returns true if the current call a GET request
389
 *
390
 * @return boolean True if call is a GET
391
 * @deprecated Use $this->request->is('get'); from your controller.
392
 */
393
	public function isGet() {
394
		return $this->request->is('get');
395
	}
396
397
/**
398
 * Returns true if the current call a DELETE request
399
 *
400
 * @return boolean True if call is a DELETE
401
 * @deprecated Use $this->request->is('delete'); from your controller.
402
 */
403
	public function isDelete() {
404
		return $this->request->is('delete');
405
	}
406
407
/**
408
 * Gets Prototype version if call is Ajax, otherwise empty string.
409
 * The Prototype library sets a special "Prototype version" HTTP header.
410
 *
411
 * @return string|boolean When Ajax the prototype version of component making the call otherwise false
412
 */
413
	public function getAjaxVersion() {
414
		$httpX = env('HTTP_X_PROTOTYPE_VERSION');
415
		return ($httpX === null) ? false : $httpX;
416
	}
417
418
/**
419
 * Adds/sets the Content-type(s) for the given name. This method allows
420
 * content-types to be mapped to friendly aliases (or extensions), which allows
421
 * RequestHandler to automatically respond to requests of that type in the
422
 * startup method.
423
 *
424
 * @param string $name The name of the Content-type, i.e. "html", "xml", "css"
425
 * @param string|array $type The Content-type or array of Content-types assigned to the name,
426
 *    i.e. "text/html", or "application/xml"
427
 * @return void
428
 * @deprecated use `$this->response->type()` instead.
429
 */
430
	public function setContent($name, $type = null) {
431
		$this->response->type(array($name => $type));
432
	}
433
434
/**
435
 * Gets the server name from which this request was referred
436
 *
437
 * @return string Server address
438
 * @deprecated use $this->request->referer() from your controller instead
439
 */
440
	public function getReferer() {
441
		return $this->request->referer(false);
442
	}
443
444
/**
445
 * Gets remote client IP
446
 *
447
 * @param boolean $safe
448
 * @return string Client IP address
449
 * @deprecated use $this->request->clientIp() from your,  controller instead.
450
 */
451
	public function getClientIP($safe = true) {
452
		return $this->request->clientIp($safe);
453
	}
454
455
/**
456
 * Determines which content types the client accepts. Acceptance is based on
457
 * the file extension parsed by the Router (if present), and by the HTTP_ACCEPT
458
 * header. Unlike CakeRequest::accepts() this method deals entirely with mapped content types.
459
 *
460
 * Usage:
461
 *
462
 * `$this->RequestHandler->accepts(array('xml', 'html', 'json'));`
463
 *
464
 * Returns true if the client accepts any of the supplied types.
465
 *
466
 * `$this->RequestHandler->accepts('xml');`
467
 *
468
 * Returns true if the client accepts xml.
469
 *
470
 * @param string|array $type Can be null (or no parameter), a string type name, or an
471
 *   array of types
472
 * @return mixed If null or no parameter is passed, returns an array of content
473
 *   types the client accepts. If a string is passed, returns true
474
 *   if the client accepts it. If an array is passed, returns true
475
 *   if the client accepts one or more elements in the array.
476
 * @see RequestHandlerComponent::setContent()
477
 */
478
	public function accepts($type = null) {
479
		$accepted = $this->request->accepts();
480
481
		if (!$type) {
482
			return $this->mapType($accepted);
0 ignored issues
show
Bug introduced by
It seems like $accepted defined by $this->request->accepts() on line 479 can also be of type boolean; however, RequestHandlerComponent::mapType() does only seem to accept string|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Deprecated Code introduced by
The method RequestHandlerComponent::mapType() has been deprecated with message: Use $this->response->mapType() in your controller instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
483
		}
484
		if (is_array($type)) {
485
			foreach ($type as $t) {
486
				$t = $this->mapAlias($t);
487
				if (in_array($t, $accepted)) {
488
					return true;
489
				}
490
			}
491
			return false;
492
		}
493
		if (is_string($type)) {
494
			return in_array($this->mapAlias($type), $accepted);
495
		}
496
		return false;
497
	}
498
499
/**
500
 * Determines the content type of the data the client has sent (i.e. in a POST request)
501
 *
502
 * @param string|array $type Can be null (or no parameter), a string type name, or an array of types
503
 * @return mixed If a single type is supplied a boolean will be returned. If no type is provided
504
 *   The mapped value of CONTENT_TYPE will be returned. If an array is supplied the first type
505
 *   in the request content type will be returned.
506
 */
507
	public function requestedWith($type = null) {
508
		if (!$this->request->is('post') && !$this->request->is('put')) {
509
			return null;
510
		}
511
		if (is_array($type)) {
512
			foreach ($type as $t) {
513
				if ($this->requestedWith($t)) {
514
					return $t;
515
				}
516
			}
517
			return false;
518
		}
519
520
		list($contentType) = explode(';', env('CONTENT_TYPE'));
521
		if ($contentType === '') {
522
			list($contentType) = explode(';', CakeRequest::header('CONTENT_TYPE'));
523
		}
524
		if (!$type) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $type of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
525
			return $this->mapType($contentType);
0 ignored issues
show
Deprecated Code introduced by
The method RequestHandlerComponent::mapType() has been deprecated with message: Use $this->response->mapType() in your controller instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
526
		}
527
		if (is_string($type)) {
528
			return ($type === $this->mapType($contentType));
0 ignored issues
show
Deprecated Code introduced by
The method RequestHandlerComponent::mapType() has been deprecated with message: Use $this->response->mapType() in your controller instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
529
		}
530
	}
531
532
/**
533
 * Determines which content-types the client prefers. If no parameters are given,
534
 * the single content-type that the client most likely prefers is returned. If $type is
535
 * an array, the first item in the array that the client accepts is returned.
536
 * Preference is determined primarily by the file extension parsed by the Router
537
 * if provided, and secondarily by the list of content-types provided in
538
 * HTTP_ACCEPT.
539
 *
540
 * @param string|array $type An optional array of 'friendly' content-type names, i.e.
541
 *   'html', 'xml', 'js', etc.
542
 * @return mixed If $type is null or not provided, the first content-type in the
543
 *    list, based on preference, is returned. If a single type is provided
544
 *    a boolean will be returned if that type is preferred.
545
 *    If an array of types are provided then the first preferred type is returned.
546
 *    If no type is provided the first preferred type is returned.
547
 * @see RequestHandlerComponent::setContent()
548
 */
549
	public function prefers($type = null) {
550
		$acceptRaw = $this->request->parseAccept();
551
552
		if (empty($acceptRaw)) {
553
			return $this->ext;
554
		}
555
		$accepts = $this->mapType(array_shift($acceptRaw));
0 ignored issues
show
Deprecated Code introduced by
The method RequestHandlerComponent::mapType() has been deprecated with message: Use $this->response->mapType() in your controller instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
556
557
		if (!$type) {
558
			if (empty($this->ext) && !empty($accepts)) {
559
				return $accepts[0];
560
			}
561
			return $this->ext;
562
		}
563
564
		$types = (array)$type;
565
566
		if (count($types) === 1) {
567
			if (!empty($this->ext)) {
568
				return in_array($this->ext, $types);
569
			}
570
			return in_array($types[0], $accepts);
571
		}
572
573
		$intersect = array_values(array_intersect($accepts, $types));
574
		if (empty($intersect)) {
575
			return false;
576
		}
577
		return $intersect[0];
578
	}
579
580
/**
581
 * Sets the layout and template paths for the content type defined by $type.
582
 *
583
 * ### Usage:
584
 *
585
 * Render the response as an 'ajax' response.
586
 *
587
 * `$this->RequestHandler->renderAs($this, 'ajax');`
588
 *
589
 * Render the response as an xml file and force the result as a file download.
590
 *
591
 * `$this->RequestHandler->renderAs($this, 'xml', array('attachment' => 'myfile.xml');`
592
 *
593
 * @param Controller $controller A reference to a controller object
594
 * @param string $type Type of response to send (e.g: 'ajax')
595
 * @param array $options Array of options to use
596
 * @return void
597
 * @see RequestHandlerComponent::setContent()
598
 * @see RequestHandlerComponent::respondAs()
599
 */
600
	public function renderAs(Controller $controller, $type, $options = array()) {
601
		$defaults = array('charset' => 'UTF-8');
602
603
		if (Configure::read('App.encoding') !== null) {
604
			$defaults['charset'] = Configure::read('App.encoding');
605
		}
606
		$options = array_merge($defaults, $options);
607
608
		if ($type === 'ajax') {
609
			$controller->layout = $this->ajaxLayout;
610
			return $this->respondAs('html', $options);
611
		}
612
		$controller->ext = '.ctp';
613
614
		$pluginDot = null;
615
		$viewClassMap = $this->viewClassMap();
616
		if (array_key_exists($type, $viewClassMap)) {
617
			list($pluginDot, $viewClass) = pluginSplit($viewClassMap[$type], true);
618
		} else {
619
			$viewClass = Inflector::classify($type);
620
		}
621
		$viewName = $viewClass . 'View';
622
		if (!class_exists($viewName)) {
623
			App::uses($viewName, $pluginDot . 'View');
624
		}
625
		if (class_exists($viewName)) {
626
			$controller->viewClass = $viewClass;
627
		} elseif (empty($this->_renderType)) {
628
			$controller->viewPath .= DS . $type;
629
		} else {
630
			$controller->viewPath = preg_replace(
631
				"/([\/\\\\]{$this->_renderType})$/",
632
				DS . $type,
633
				$controller->viewPath
634
			);
635
		}
636
		$this->_renderType = $type;
637
		$controller->layoutPath = $type;
638
639
		if ($this->response->getMimeType($type)) {
640
			$this->respondAs($type, $options);
641
		}
642
643
		$helper = ucfirst($type);
644
645
		if (!in_array($helper, $controller->helpers) && empty($controller->helpers[$helper])) {
646
			App::uses('AppHelper', 'View/Helper');
647
			App::uses($helper . 'Helper', 'View/Helper');
648
			if (class_exists($helper . 'Helper')) {
649
				$controller->helpers[] = $helper;
650
			}
651
		}
652
	}
653
654
/**
655
 * Sets the response header based on type map index name. This wraps several methods
656
 * available on CakeResponse. It also allows you to use Content-Type aliases.
657
 *
658
 * @param string|array $type Friendly type name, i.e. 'html' or 'xml', or a full content-type,
659
 *    like 'application/x-shockwave'.
660
 * @param array $options If $type is a friendly type name that is associated with
661
 *    more than one type of content, $index is used to select which content-type to use.
662
 * @return boolean Returns false if the friendly type name given in $type does
663
 *    not exist in the type map, or if the Content-type header has
664
 *    already been set by this method.
665
 * @see RequestHandlerComponent::setContent()
666
 */
667
	public function respondAs($type, $options = array()) {
668
		$defaults = array('index' => null, 'charset' => null, 'attachment' => false);
669
		$options = $options + $defaults;
670
671
		$cType = $type;
672
		if (strpos($type, '/') === false) {
673
			$cType = $this->response->getMimeType($type);
0 ignored issues
show
Bug introduced by
It seems like $type defined by parameter $type on line 667 can also be of type array; however, CakeResponse::getMimeType() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
674
		}
675
		if (is_array($cType)) {
676
			if (isset($cType[$options['index']])) {
677
				$cType = $cType[$options['index']];
678
			}
679
680
			if ($this->prefers($cType)) {
681
				$cType = $this->prefers($cType);
682
			} else {
683
				$cType = $cType[0];
684
			}
685
		}
686
687
		if (!$type) {
688
			return false;
689
		}
690
		if (empty($this->request->params['requested'])) {
691
			$this->response->type($cType);
692
		}
693
		if (!empty($options['charset'])) {
694
			$this->response->charset($options['charset']);
695
		}
696
		if (!empty($options['attachment'])) {
697
			$this->response->download($options['attachment']);
698
		}
699
		return true;
700
	}
701
702
/**
703
 * Returns the current response type (Content-type header), or null if not alias exists
704
 *
705
 * @return mixed A string content type alias, or raw content type if no alias map exists,
706
 *	otherwise null
707
 */
708
	public function responseType() {
709
		return $this->mapType($this->response->type());
0 ignored issues
show
Deprecated Code introduced by
The method RequestHandlerComponent::mapType() has been deprecated with message: Use $this->response->mapType() in your controller instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
710
	}
711
712
/**
713
 * Maps a content-type back to an alias
714
 *
715
 * @param string|array $cType Either a string content type to map, or an array of types.
716
 * @return string|array Aliases for the types provided.
717
 * @deprecated Use $this->response->mapType() in your controller instead.
718
 */
719
	public function mapType($cType) {
720
		return $this->response->mapType($cType);
721
	}
722
723
/**
724
 * Maps a content type alias back to its mime-type(s)
725
 *
726
 * @param string|array $alias String alias to convert back into a content type. Or an array of aliases to map.
727
 * @return string Null on an undefined alias. String value of the mapped alias type. If an
728
 *   alias maps to more than one content type, the first one will be returned.
729
 */
730
	public function mapAlias($alias) {
731
		if (is_array($alias)) {
732
			return array_map(array($this, 'mapAlias'), $alias);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array_map(array($..., 'mapAlias'), $alias); (array) is incompatible with the return type documented by RequestHandlerComponent::mapAlias of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
733
		}
734
		$type = $this->response->getMimeType($alias);
735
		if ($type) {
736
			if (is_array($type)) {
737
				return $type[0];
738
			}
739
			return $type;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $type; (object|integer|double|string|null|boolean) is incompatible with the return type documented by RequestHandlerComponent::mapAlias of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
740
		}
741
		return null;
742
	}
743
744
/**
745
 * Add a new mapped input type. Mapped input types are automatically
746
 * converted by RequestHandlerComponent during the startup() callback.
747
 *
748
 * @param string $type The type alias being converted, ie. json
749
 * @param array $handler The handler array for the type. The first index should
750
 *    be the handling callback, all other arguments should be additional parameters
751
 *    for the handler.
752
 * @return void
753
 * @throws CakeException
754
 */
755
	public function addInputType($type, $handler) {
756
		if (!is_array($handler) || !isset($handler[0]) || !is_callable($handler[0])) {
757
			throw new CakeException(__d('cake_dev', 'You must give a handler callback.'));
758
		}
759
		$this->_inputTypeMap[$type] = $handler;
760
	}
761
762
/**
763
 * Getter/setter for viewClassMap
764
 *
765
 * @param array|string $type The type string or array with format `array('type' => 'viewClass')` to map one or more
766
 * @param array $viewClass The viewClass to be used for the type without `View` appended
767
 * @return array|string Returns viewClass when only string $type is set, else array with viewClassMap
768
 */
769
	public function viewClassMap($type = null, $viewClass = null) {
770
		if (!$viewClass && is_string($type) && isset($this->_viewClassMap[$type])) {
771
			return $this->_viewClassMap[$type];
772
		}
773
		if (is_string($type)) {
774
			$this->_viewClassMap[$type] = $viewClass;
775
		} elseif (is_array($type)) {
776
			foreach ($type as $key => $value) {
777
				$this->viewClassMap($key, $value);
778
			}
779
		}
780
		return $this->_viewClassMap;
781
	}
782
783
}
784