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;
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: *
148: * If html is one of the preferred types, no content type will be set, this
149: * is to avoid issues with browsers that prefer html and several other content types.
150: *
151: * @return void
152: */
153: protected function _setExtension() {
154: $accept = $this->request->parseAccept();
155: if (empty($accept)) {
156: return;
157: }
158: $extensions = Router::extensions();
159: $preferred = array_shift($accept);
160: $preferredTypes = $this->response->mapType($preferred);
161: if (!in_array('xhtml', $preferredTypes) && !in_array('html', $preferredTypes)) {
162: $similarTypes = array_intersect($extensions, $preferredTypes);
163: if (count($similarTypes) === 1) {
164: $this->ext = array_shift($similarTypes);
165: }
166: }
167: }
168:
169: /**
170: * The startup method of the RequestHandler enables several automatic behaviors
171: * related to the detection of certain properties of the HTTP request, including:
172: *
173: * - Disabling layout rendering for Ajax requests (based on the HTTP_X_REQUESTED_WITH header)
174: * - If Router::parseExtensions() is enabled, the layout and template type are
175: * switched based on the parsed extension or Accept-Type header. For example, if `controller/action.xml`
176: * is requested, the view path becomes `app/View/Controller/xml/action.ctp`. Also if
177: * `controller/action` is requested with `Accept-Type: application/xml` in the headers
178: * the view path will become `app/View/Controller/xml/action.ctp`. Layout and template
179: * types will only switch to mime-types recognized by CakeResponse. If you need to declare
180: * additional mime-types, you can do so using CakeResponse::type() in your controllers beforeFilter()
181: * method.
182: * - If a helper with the same name as the extension exists, it is added to the controller.
183: * - If the extension is of a type that RequestHandler understands, it will set that
184: * Content-type in the response header.
185: * - If the XML data is POSTed, the data is parsed into an XML object, which is assigned
186: * to the $data property of the controller, which can then be saved to a model object.
187: *
188: * @param Controller $controller A reference to the controller
189: * @return void
190: */
191: public function startup(Controller $controller) {
192: $controller->request->params['isAjax'] = $this->request->is('ajax');
193: $isRecognized = (
194: !in_array($this->ext, array('html', 'htm')) &&
195: $this->response->getMimeType($this->ext)
196: );
197:
198: if (!empty($this->ext) && $isRecognized) {
199: $this->renderAs($controller, $this->ext);
200: } elseif ($this->request->is('ajax')) {
201: $this->renderAs($controller, 'ajax');
202: } elseif (empty($this->ext) || in_array($this->ext, array('html', 'htm'))) {
203: $this->respondAs('html', array('charset' => Configure::read('App.encoding')));
204: }
205:
206: foreach ($this->_inputTypeMap as $type => $handler) {
207: if ($this->requestedWith($type)) {
208: $input = call_user_func_array(array($controller->request, 'input'), $handler);
209: $controller->request->data = $input;
210: }
211: }
212: }
213:
214: /**
215: * Helper method to parse xml input data, due to lack of anonymous functions
216: * this lives here.
217: *
218: * @param string $xml
219: * @return array Xml array data
220: */
221: public function convertXml($xml) {
222: try {
223: $xml = Xml::build($xml);
224: if (isset($xml->data)) {
225: return Xml::toArray($xml->data);
226: }
227: return Xml::toArray($xml);
228: } catch (XmlException $e) {
229: return array();
230: }
231: }
232:
233: /**
234: * Handles (fakes) redirects for Ajax requests using requestAction()
235: * Modifies the $_POST and $_SERVER['REQUEST_METHOD'] to simulate a new GET request.
236: *
237: * @param Controller $controller A reference to the controller
238: * @param string|array $url A string or array containing the redirect location
239: * @param integer|array $status HTTP Status for redirect
240: * @param boolean $exit
241: * @return void
242: */
243: public function beforeRedirect(Controller $controller, $url, $status = null, $exit = true) {
244: if (!$this->request->is('ajax')) {
245: return;
246: }
247: if (empty($url)) {
248: return;
249: }
250: $_SERVER['REQUEST_METHOD'] = 'GET';
251: foreach ($_POST as $key => $val) {
252: unset($_POST[$key]);
253: }
254: if (is_array($url)) {
255: $url = Router::url($url + array('base' => false));
256: }
257: if (!empty($status)) {
258: $statusCode = $this->response->httpCodes($status);
259: $code = key($statusCode);
260: $this->response->statusCode($code);
261: }
262: $this->response->body($this->requestAction($url, array('return', 'bare' => false)));
263: $this->response->send();
264: $this->_stop();
265: }
266:
267: /**
268: * Checks if the response can be considered different according to the request
269: * headers, and the caching response headers. If it was not modified, then the
270: * render process is skipped. And the client will get a blank response with a
271: * "304 Not Modified" header.
272: *
273: * @params Controller $controller
274: * @return boolean false if the render process should be aborted
275: */
276: public function beforeRender(Controller $controller) {
277: if ($this->settings['checkHttpCache'] && $this->response->checkNotModified($this->request)) {
278: return false;
279: }
280: }
281:
282: /**
283: * Returns true if the current HTTP request is Ajax, false otherwise
284: *
285: * @return boolean True if call is Ajax
286: * @deprecated use `$this->request->is('ajax')` instead.
287: */
288: public function isAjax() {
289: return $this->request->is('ajax');
290: }
291:
292: /**
293: * Returns true if the current HTTP request is coming from a Flash-based client
294: *
295: * @return boolean True if call is from Flash
296: * @deprecated use `$this->request->is('flash')` instead.
297: */
298: public function isFlash() {
299: return $this->request->is('flash');
300: }
301:
302: /**
303: * Returns true if the current request is over HTTPS, false otherwise.
304: *
305: * @return boolean True if call is over HTTPS
306: * @deprecated use `$this->request->is('ssl')` instead.
307: */
308: public function isSSL() {
309: return $this->request->is('ssl');
310: }
311:
312: /**
313: * Returns true if the current call accepts an XML response, false otherwise
314: *
315: * @return boolean True if client accepts an XML response
316: */
317: public function isXml() {
318: return $this->prefers('xml');
319: }
320:
321: /**
322: * Returns true if the current call accepts an RSS response, false otherwise
323: *
324: * @return boolean True if client accepts an RSS response
325: */
326: public function isRss() {
327: return $this->prefers('rss');
328: }
329:
330: /**
331: * Returns true if the current call accepts an Atom response, false otherwise
332: *
333: * @return boolean True if client accepts an RSS response
334: */
335: public function isAtom() {
336: return $this->prefers('atom');
337: }
338:
339: /**
340: * Returns true if user agent string matches a mobile web browser, or if the
341: * client accepts WAP content.
342: *
343: * @return boolean True if user agent is a mobile web browser
344: */
345: public function isMobile() {
346: return $this->request->is('mobile') || $this->accepts('wap');
347: }
348:
349: /**
350: * Returns true if the client accepts WAP content
351: *
352: * @return boolean
353: */
354: public function isWap() {
355: return $this->prefers('wap');
356: }
357:
358: /**
359: * Returns true if the current call a POST request
360: *
361: * @return boolean True if call is a POST
362: * @deprecated Use $this->request->is('post'); from your controller.
363: */
364: public function isPost() {
365: return $this->request->is('post');
366: }
367:
368: /**
369: * Returns true if the current call a PUT request
370: *
371: * @return boolean True if call is a PUT
372: * @deprecated Use $this->request->is('put'); from your controller.
373: */
374: public function isPut() {
375: return $this->request->is('put');
376: }
377:
378: /**
379: * Returns true if the current call a GET request
380: *
381: * @return boolean True if call is a GET
382: * @deprecated Use $this->request->is('get'); from your controller.
383: */
384: public function isGet() {
385: return $this->request->is('get');
386: }
387:
388: /**
389: * Returns true if the current call a DELETE request
390: *
391: * @return boolean True if call is a DELETE
392: * @deprecated Use $this->request->is('delete'); from your controller.
393: */
394: public function isDelete() {
395: return $this->request->is('delete');
396: }
397:
398: /**
399: * Gets Prototype version if call is Ajax, otherwise empty string.
400: * The Prototype library sets a special "Prototype version" HTTP header.
401: *
402: * @return string|boolean When Ajax the prototype version of component making the call otherwise false
403: */
404: public function getAjaxVersion() {
405: $httpX = env('HTTP_X_PROTOTYPE_VERSION');
406: return ($httpX === null) ? false : $httpX;
407: }
408:
409: /**
410: * Adds/sets the Content-type(s) for the given name. This method allows
411: * content-types to be mapped to friendly aliases (or extensions), which allows
412: * RequestHandler to automatically respond to requests of that type in the
413: * startup method.
414: *
415: * @param string $name The name of the Content-type, i.e. "html", "xml", "css"
416: * @param string|array $type The Content-type or array of Content-types assigned to the name,
417: * i.e. "text/html", or "application/xml"
418: * @return void
419: * @deprecated use `$this->response->type()` instead.
420: */
421: public function setContent($name, $type = null) {
422: $this->response->type(array($name => $type));
423: }
424:
425: /**
426: * Gets the server name from which this request was referred
427: *
428: * @return string Server address
429: * @deprecated use $this->request->referer() from your controller instead
430: */
431: public function getReferer() {
432: return $this->request->referer(false);
433: }
434:
435: /**
436: * Gets remote client IP
437: *
438: * @param boolean $safe
439: * @return string Client IP address
440: * @deprecated use $this->request->clientIp() from your, controller instead.
441: */
442: public function getClientIP($safe = true) {
443: return $this->request->clientIp($safe);
444: }
445:
446: /**
447: * Determines which content types the client accepts. Acceptance is based on
448: * the file extension parsed by the Router (if present), and by the HTTP_ACCEPT
449: * header. Unlike CakeRequest::accepts() this method deals entirely with mapped content types.
450: *
451: * Usage:
452: *
453: * `$this->RequestHandler->accepts(array('xml', 'html', 'json'));`
454: *
455: * Returns true if the client accepts any of the supplied types.
456: *
457: * `$this->RequestHandler->accepts('xml');`
458: *
459: * Returns true if the client accepts xml.
460: *
461: * @param string|array $type Can be null (or no parameter), a string type name, or an
462: * array of types
463: * @return mixed If null or no parameter is passed, returns an array of content
464: * types the client accepts. If a string is passed, returns true
465: * if the client accepts it. If an array is passed, returns true
466: * if the client accepts one or more elements in the array.
467: * @see RequestHandlerComponent::setContent()
468: */
469: public function accepts($type = null) {
470: $accepted = $this->request->accepts();
471:
472: if (!$type) {
473: return $this->mapType($accepted);
474: }
475: if (is_array($type)) {
476: foreach ($type as $t) {
477: $t = $this->mapAlias($t);
478: if (in_array($t, $accepted)) {
479: return true;
480: }
481: }
482: return false;
483: }
484: if (is_string($type)) {
485: return in_array($this->mapAlias($type), $accepted);
486: }
487: return false;
488: }
489:
490: /**
491: * Determines the content type of the data the client has sent (i.e. in a POST request)
492: *
493: * @param string|array $type Can be null (or no parameter), a string type name, or an array of types
494: * @return mixed If a single type is supplied a boolean will be returned. If no type is provided
495: * The mapped value of CONTENT_TYPE will be returned. If an array is supplied the first type
496: * in the request content type will be returned.
497: */
498: public function requestedWith($type = null) {
499: if (!$this->request->is('post') && !$this->request->is('put')) {
500: return null;
501: }
502: if (is_array($type)) {
503: foreach ($type as $t) {
504: if ($this->requestedWith($t)) {
505: return $t;
506: }
507: }
508: return false;
509: }
510:
511: list($contentType) = explode(';', env('CONTENT_TYPE'));
512: if (!$type) {
513: return $this->mapType($contentType);
514: }
515: if (is_string($type)) {
516: return ($type == $this->mapType($contentType));
517: }
518: }
519:
520: /**
521: * Determines which content-types the client prefers. If no parameters are given,
522: * the single content-type that the client most likely prefers is returned. If $type is
523: * an array, the first item in the array that the client accepts is returned.
524: * Preference is determined primarily by the file extension parsed by the Router
525: * if provided, and secondarily by the list of content-types provided in
526: * HTTP_ACCEPT.
527: *
528: * @param string|array $type An optional array of 'friendly' content-type names, i.e.
529: * 'html', 'xml', 'js', etc.
530: * @return mixed If $type is null or not provided, the first content-type in the
531: * list, based on preference, is returned. If a single type is provided
532: * a boolean will be returned if that type is preferred.
533: * If an array of types are provided then the first preferred type is returned.
534: * If no type is provided the first preferred type is returned.
535: * @see RequestHandlerComponent::setContent()
536: */
537: public function prefers($type = null) {
538: $acceptRaw = $this->request->parseAccept();
539:
540: if (empty($acceptRaw)) {
541: return $this->ext;
542: }
543: $accepts = $this->mapType(array_shift($acceptRaw));
544:
545: if (!$type) {
546: if (empty($this->ext) && !empty($accepts)) {
547: return $accepts[0];
548: }
549: return $this->ext;
550: }
551:
552: $types = (array)$type;
553:
554: if (count($types) === 1) {
555: if (!empty($this->ext)) {
556: return in_array($this->ext, $types);
557: }
558: return in_array($types[0], $accepts);
559: }
560:
561: $intersect = array_values(array_intersect($accepts, $types));
562: if (empty($intersect)) {
563: return false;
564: }
565: return $intersect[0];
566: }
567:
568: /**
569: * Sets the layout and template paths for the content type defined by $type.
570: *
571: * ### Usage:
572: *
573: * Render the response as an 'ajax' response.
574: *
575: * `$this->RequestHandler->renderAs($this, 'ajax');`
576: *
577: * Render the response as an xml file and force the result as a file download.
578: *
579: * `$this->RequestHandler->renderAs($this, 'xml', array('attachment' => 'myfile.xml');`
580: *
581: * @param Controller $controller A reference to a controller object
582: * @param string $type Type of response to send (e.g: 'ajax')
583: * @param array $options Array of options to use
584: * @return void
585: * @see RequestHandlerComponent::setContent()
586: * @see RequestHandlerComponent::respondAs()
587: */
588: public function renderAs(Controller $controller, $type, $options = array()) {
589: $defaults = array('charset' => 'UTF-8');
590:
591: if (Configure::read('App.encoding') !== null) {
592: $defaults['charset'] = Configure::read('App.encoding');
593: }
594: $options = array_merge($defaults, $options);
595:
596: if ($type === 'ajax') {
597: $controller->layout = $this->ajaxLayout;
598: return $this->respondAs('html', $options);
599: }
600: $controller->ext = '.ctp';
601:
602: $pluginDot = null;
603: $viewClassMap = $this->viewClassMap();
604: if (array_key_exists($type, $viewClassMap)) {
605: list($pluginDot, $viewClass) = pluginSplit($viewClassMap[$type], true);
606: } else {
607: $viewClass = Inflector::classify($type);
608: }
609: $viewName = $viewClass . 'View';
610: if (!class_exists($viewName)) {
611: App::uses($viewName, $pluginDot . 'View');
612: }
613: if (class_exists($viewName)) {
614: $controller->viewClass = $viewClass;
615: } elseif (empty($this->_renderType)) {
616: $controller->viewPath .= DS . $type;
617: } else {
618: $controller->viewPath = preg_replace(
619: "/([\/\\\\]{$this->_renderType})$/",
620: DS . $type,
621: $controller->viewPath
622: );
623: }
624: $this->_renderType = $type;
625: $controller->layoutPath = $type;
626:
627: if ($this->response->getMimeType($type)) {
628: $this->respondAs($type, $options);
629: }
630:
631: $helper = ucfirst($type);
632:
633: if (!in_array($helper, $controller->helpers) && empty($controller->helpers[$helper])) {
634: App::uses('AppHelper', 'View/Helper');
635: App::uses($helper . 'Helper', 'View/Helper');
636: if (class_exists($helper . 'Helper')) {
637: $controller->helpers[] = $helper;
638: }
639: }
640: }
641:
642: /**
643: * Sets the response header based on type map index name. This wraps several methods
644: * available on CakeResponse. It also allows you to use Content-Type aliases.
645: *
646: * @param string|array $type Friendly type name, i.e. 'html' or 'xml', or a full content-type,
647: * like 'application/x-shockwave'.
648: * @param array $options If $type is a friendly type name that is associated with
649: * more than one type of content, $index is used to select which content-type to use.
650: * @return boolean Returns false if the friendly type name given in $type does
651: * not exist in the type map, or if the Content-type header has
652: * already been set by this method.
653: * @see RequestHandlerComponent::setContent()
654: */
655: public function respondAs($type, $options = array()) {
656: $defaults = array('index' => null, 'charset' => null, 'attachment' => false);
657: $options = $options + $defaults;
658:
659: $cType = $type;
660: if (strpos($type, '/') === false) {
661: $cType = $this->response->getMimeType($type);
662: }
663: if (is_array($cType)) {
664: if (isset($cType[$options['index']])) {
665: $cType = $cType[$options['index']];
666: }
667:
668: if ($this->prefers($cType)) {
669: $cType = $this->prefers($cType);
670: } else {
671: $cType = $cType[0];
672: }
673: }
674:
675: if (!$type) {
676: return false;
677: }
678: if (empty($this->request->params['requested'])) {
679: $this->response->type($cType);
680: }
681: if (!empty($options['charset'])) {
682: $this->response->charset($options['charset']);
683: }
684: if (!empty($options['attachment'])) {
685: $this->response->download($options['attachment']);
686: }
687: return true;
688: }
689:
690: /**
691: * Returns the current response type (Content-type header), or null if not alias exists
692: *
693: * @return mixed A string content type alias, or raw content type if no alias map exists,
694: * otherwise null
695: */
696: public function responseType() {
697: return $this->mapType($this->response->type());
698: }
699:
700: /**
701: * Maps a content-type back to an alias
702: *
703: * @param string|array $cType Either a string content type to map, or an array of types.
704: * @return string|array Aliases for the types provided.
705: * @deprecated Use $this->response->mapType() in your controller instead.
706: */
707: public function mapType($cType) {
708: return $this->response->mapType($cType);
709: }
710:
711: /**
712: * Maps a content type alias back to its mime-type(s)
713: *
714: * @param string|array $alias String alias to convert back into a content type. Or an array of aliases to map.
715: * @return string Null on an undefined alias. String value of the mapped alias type. If an
716: * alias maps to more than one content type, the first one will be returned.
717: */
718: public function mapAlias($alias) {
719: if (is_array($alias)) {
720: return array_map(array($this, 'mapAlias'), $alias);
721: }
722: $type = $this->response->getMimeType($alias);
723: if ($type) {
724: if (is_array($type)) {
725: return $type[0];
726: }
727: return $type;
728: }
729: return null;
730: }
731:
732: /**
733: * Add a new mapped input type. Mapped input types are automatically
734: * converted by RequestHandlerComponent during the startup() callback.
735: *
736: * @param string $type The type alias being converted, ie. json
737: * @param array $handler The handler array for the type. The first index should
738: * be the handling callback, all other arguments should be additional parameters
739: * for the handler.
740: * @return void
741: * @throws CakeException
742: */
743: public function addInputType($type, $handler) {
744: if (!is_array($handler) || !isset($handler[0]) || !is_callable($handler[0])) {
745: throw new CakeException(__d('cake_dev', 'You must give a handler callback.'));
746: }
747: $this->_inputTypeMap[$type] = $handler;
748: }
749:
750: /**
751: * Getter/setter for viewClassMap
752: *
753: * @param array|string $type The type string or array with format `array('type' => 'viewClass')` to map one or more
754: * @param array $viewClass The viewClass to be used for the type without `View` appended
755: * @return array|string Returns viewClass when only string $type is set, else array with viewClassMap
756: */
757: public function viewClassMap($type = null, $viewClass = null) {
758: if (!$viewClass && is_string($type) && isset($this->_viewClassMap[$type])) {
759: return $this->_viewClassMap[$type];
760: }
761: if (is_string($type)) {
762: $this->_viewClassMap[$type] = $viewClass;
763: } elseif (is_array($type)) {
764: foreach ($type as $key => $value) {
765: $this->viewClassMap($key, $value);
766: }
767: }
768: return $this->_viewClassMap;
769: }
770:
771: }
772: