Media.php 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2013, Union of RAD (http://union-of-rad.org)
  6. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  7. */
  8. namespace lithium\net\http;
  9. use lithium\util\Set;
  10. use lithium\util\String;
  11. use lithium\core\Libraries;
  12. use lithium\core\Environment;
  13. use lithium\net\http\MediaException;
  14. /**
  15. * The `Media` class facilitates content-type mapping (mapping between content-types and file
  16. * extensions), handling static assets and globally configuring how the framework handles output in
  17. * different formats.
  18. *
  19. * Using the `Media` class, you can globally configure input and output of different types of
  20. * content, i.e.:
  21. * {{{ embed:lithium\tests\cases\net\http\MediaTest::testCustomEncodeHandler(4-12) }}}
  22. *
  23. * You may then render CSV content from anywhere in your application. For example, in a controller
  24. * you may do the following:
  25. *
  26. * {{{
  27. * $this->render(array('csv' => Post::find('all')));
  28. * }}}
  29. */
  30. class Media extends \lithium\core\StaticObject {
  31. /**
  32. * Maps file extensions to content-types. Used to set response types and determine request
  33. * types. Can be modified with `Media::type()`.
  34. *
  35. * @var array
  36. * @see lithium\net\http\Media::type()
  37. */
  38. protected static $_types = array();
  39. /**
  40. * A map of media handler objects or callbacks, mapped to media types.
  41. *
  42. * @var array
  43. */
  44. protected static $_handlers = array();
  45. /**
  46. * Contains default path settings for various asset types.
  47. *
  48. * For each type, the corresponding array key maps to the general type name, i.e. `'js'` or
  49. * `'image'`. Each type contains a set of keys which define their locations and default
  50. * behavior. For more information how each key works, see `Media::assets()`.
  51. *
  52. * @var array
  53. * @see lithium\net\http\Media::assets()
  54. */
  55. protected static $_assets = array();
  56. /**
  57. * Placeholder for class dependencies. This class' dependencies (i.e. templating classes) are
  58. * typically specified through other configuration.
  59. *
  60. * @var array
  61. */
  62. protected static $_classes = array();
  63. /**
  64. * Returns the list of registered media types. New types can be set with the `type()` method.
  65. *
  66. * @return array Returns an array of media type extensions or short-names, which comprise the
  67. * list of types handled.
  68. */
  69. public static function types() {
  70. return array_keys(static::_types());
  71. }
  72. /**
  73. * Alias for `types()`; included for interface compatibility with
  74. * `lithium\util\Collection::to()`, which allows a collection object to be exported to any
  75. * format supported by a `Media` handler. See the documentation for `Collection::to()` for more
  76. * information.
  77. *
  78. * @see lithium\net\http\Media
  79. * @return array Returns the value of `Media::types()`.
  80. */
  81. public static function formats() {
  82. return static::types();
  83. }
  84. /**
  85. * Alias for `encode()`; included for interface compatibility with
  86. * `lithium\util\Collection::to()`, which allows a collection object to be exported to any
  87. * format supported by a `Media` handler. See the documentation for `Collection::to()` for more
  88. * information.
  89. *
  90. * @param mixed $format Format into which data will be converted, i.e. `'json'`.
  91. * @param mixed $data Either an array or object (usually an instance of `Collection`) which will
  92. * be converted into the specified format.
  93. * @param array $options Additional handler-specific options to pass to the content handler.
  94. * @return mixed
  95. */
  96. public static function to($format, $data, array $options = array()) {
  97. return static::encode($format, $data, $options);
  98. }
  99. /**
  100. * Maps a type name to a particular content-type (or multiple types) with a set of options, or
  101. * retrieves information about a type that has been defined.
  102. *
  103. * Examples:
  104. * {{{ embed:lithium\tests\cases\net\http\MediaTest::testMediaTypes(1-2) }}}
  105. *
  106. * {{{ embed:lithium\tests\cases\net\http\MediaTest::testMediaTypes(19-23) }}}
  107. *
  108. * {{{ embed:lithium\tests\cases\net\http\MediaTest::testMediaTypes(43-44) }}}
  109. *
  110. * Alternatively, can be used to detect the type name of a registered content type:
  111. * {{{
  112. * Media::type('application/json'); // returns 'json'
  113. * Media::type('application/javascript'); // returns 'javascript'
  114. * Media::type('text/javascript'); // also returns 'javascript'
  115. *
  116. * Media::type('text/html'); // returns 'html'
  117. * Media::type('application/xhtml+xml'); // also returns 'html'
  118. * }}}
  119. *
  120. * #### Content negotiation
  121. *
  122. * When creating custom media types, specifying which content-type(s) to match isn't always
  123. * enough. For example, if you wish to serve a different set of templates to mobile web
  124. * browsers, you'd still want those templates served as HTML. You might add something like this:
  125. *
  126. * {{{
  127. * Media::type('mobile', array('application/xhtml+xml', 'text/html'));
  128. * }}}
  129. *
  130. * However, this would cause _all_ requests for HTML content to be interpreted as
  131. * `'mobile'`-type requests. Instead, we can use _content negotiation_ to granularly specify how
  132. * to match a particular type. Content negotiation is the process of examining the HTTP headers
  133. * provided in the request (including the content-types listed in the `Accept` header, and
  134. * optionally other things as well, like the `Accept-Language` or `User-Agent` headers), in
  135. * order to produce the best representation of the requested resource for the client; in other
  136. * words, the resource that most closely matches what the client is asking for.
  137. *
  138. * Content negotiation with media types is made possible through the `'conditions'` key of the
  139. * `$options` parameter, which contains an array of assertions made against the `Request`
  140. * object. Each assertion (array key) can be one of three different things:
  141. *
  142. * - `'type'` _boolean_: In the default routing, some routes have `{:type}` keys, which are
  143. * designed to match file extensions in URLs. These values act as overrides for the HTTP
  144. * `Accept` header, allowing different formats to be served with the same content type. For
  145. * example, if you're serving [ JSONP](http://en.wikipedia.org/wiki/JSON#JSONP), you'll want
  146. * to serve it with the same content-type as JavaScript (since it is JavaScript), but you
  147. * probably won't want to use the same template(s) or other settings. Therefore, when serving
  148. * JSONP content, you can specify that the extension defined in the type must be present in
  149. * the URL:
  150. * {{{
  151. * Media::type('jsonp', array('text/html'), array(
  152. * // template settings...
  153. * 'conditions' => array('type' => true)
  154. * ));
  155. * }}}
  156. * Then, JSONP content will only ever be served when the request URL ends in `.jsonp`.
  157. *
  158. * - `'<prefix>:<key>'` _string_: This type of assertion can be used to match against arbitrary
  159. * information in the request, including headers (i.e. `'http:user_agent'`), environment
  160. * variables (i.e. `'env:home'`), GET and POST data (i.e. `'query:foo'` or `'data:foo'`,
  161. * respectively), and the HTTP method (`'http:method'`) of the request. For more information
  162. * on possible keys, see `lithium\action\Request::get()`.
  163. *
  164. * - `'<detector>'` _boolean_: Uses detector checks added to the `Request` object to make
  165. * boolean assertions against the request. For example, if a detector called `'iPhone'` is
  166. * attached, you can add `'iPhone' => true` to the `'conditions'` array in order to filter for
  167. * iPhone requests only. See `lithium\action\Request::detect()` for more information on adding
  168. * detectors.
  169. *
  170. * @see lithium\net\http\Media::$_types
  171. * @see lithium\net\http\Media::$_handlers
  172. * @see lithium\net\http\Media::negotiate()
  173. * @see lithium\action\Request::get()
  174. * @see lithium\action\Request::is()
  175. * @see lithium\action\Request::detect()
  176. * @see lithium\util\String::insert()
  177. * @param string $type A file-extension-style type name, i.e. `'txt'`, `'js'`, or `'atom'`.
  178. * Alternatively, a mapped content type, i.e. `'text/html'`,
  179. * `'application/atom+xml'`, etc.; in which case, the matching type name (i.e.
  180. * '`html'` or `'atom'`) will be returned.
  181. * @param mixed $content Optional. A string or array containing the content-type(s) that
  182. * `$type` should map to. If `$type` is an array of content-types, the first one listed
  183. * should be the "primary" type, and will be used as the `Content-type` header of any
  184. * `Response` objects served through this type.
  185. * @param array $options Optional. The handling options for this media type. Possible keys are:
  186. * - `'view'` _string_: Specifies the view class to use when rendering this content.
  187. * Note that no `'view'` class is specified by default. If you want to
  188. * render templates using Lithium's default view class, use
  189. * `'lithium\template\View'`
  190. * - `'decode'` _mixed_: A (string) function name or (object) closure that handles
  191. * decoding or unserializing content from this format.
  192. * - `'encode'` _mixed_: A (string) function name or (object) closure that handles
  193. * encoding or serializing content into this format.
  194. * - `'cast'` _boolean_: Used with `'encode'`. If `true`, all data passed into the
  195. * specified encode function is first cast to array structures.
  196. * - `'paths'` _array_: Optional key/value pairs mapping paths for
  197. * `'template'`, `'layout'`, and `'element'` template files. Any keys ommitted
  198. * will use the default path. The values should be `String::insert()`-style
  199. * paths or an array of `String::insert()`-style paths. If it is an array,
  200. * each path will be tried in the order specified until a template is found.
  201. * This is useful for allowing custom templates while falling back on
  202. * default templates if no custom template was found. If you want to
  203. * render templates without a layout, use a `false` value for `'layout'`.
  204. * - `'conditions'` _array_: Optional key/value pairs used as assertions in content
  205. * negotiation. See the above section on **Content Negotiation**.
  206. * @return mixed If `$content` and `$options` are empty, returns an array with `'content'` and
  207. * `'options'` keys, where `'content'` is the content-type(s) that correspond to
  208. * `$type` (can be a string or array, if multiple content-types are available), and
  209. * `'options'` is the array of options which define how this content-type should be
  210. * handled. If `$content` or `$options` are non-empty, returns `null`.
  211. */
  212. public static function type($type, $content = null, array $options = array()) {
  213. $defaults = array(
  214. 'view' => false,
  215. 'paths' => array(
  216. 'template' => '{:library}/views/{:controller}/{:template}.{:type}.php',
  217. 'layout' => '{:library}/views/layouts/{:layout}.{:type}.php',
  218. 'element' => '{:library}/views/elements/{:template}.{:type}.php'
  219. ),
  220. 'encode' => false,
  221. 'decode' => false,
  222. 'cast' => true,
  223. 'conditions' => array()
  224. );
  225. if ($content === false) {
  226. unset(static::$_types[$type], static::$_handlers[$type]);
  227. }
  228. if (!$content && !$options) {
  229. if (!$content = static::_types($type)) {
  230. return;
  231. }
  232. if (strpos($type, '/')) {
  233. return $content;
  234. }
  235. if (is_array($content) && isset($content['alias'])) {
  236. return static::type($content['alias']);
  237. }
  238. return compact('content') + array('options' => static::handlers($type));
  239. }
  240. if ($content) {
  241. static::$_types[$type] = (array) $content;
  242. }
  243. static::$_handlers[$type] = $options ? Set::merge($defaults, $options) : array();
  244. }
  245. /**
  246. * Performs content-type negotiation on a `Request` object, by iterating over the accepted
  247. * types in sequence, from most preferred to least, and attempting to match each one against a
  248. * content type defined by `Media::type()`, until a match is found. If more than one defined
  249. * type matches for a given content type, they will be checked in the order they were added
  250. * (usually, this corresponds to the order they were defined in the application bootstrapping
  251. * process).
  252. *
  253. * @see lithium\net\http\Media::type()
  254. * @see lithium\net\http\Media::match()
  255. * @see lithium\action\Request
  256. * @param object $request The instance of `lithium\action\Request` which contains the details of
  257. * the request to be content-negotiated.
  258. * @return string Returns the first matching type name, i.e. `'html'` or `'json'`.
  259. */
  260. public static function negotiate($request) {
  261. $self = get_called_class();
  262. $match = function($name) use ($self, $request) {
  263. if (($cfg = $self::type($name)) && $self::match($request, compact('name') + $cfg)) {
  264. return true;
  265. }
  266. return false;
  267. };
  268. if (($type = $request->type) && $match($type)) {
  269. return $type;
  270. }
  271. foreach ($request->accepts(true) as $type) {
  272. if (!$types = (array) static::_types($type)) {
  273. continue;
  274. }
  275. foreach ($types as $name) {
  276. if (!$match($name)) {
  277. continue;
  278. }
  279. return $name;
  280. }
  281. }
  282. }
  283. /**
  284. * Assists `Media::negotiate()` in processing the negotiation conditions of a content type, by
  285. * iterating through the conditions and checking each one against the `Request` object.
  286. *
  287. * @see lithium\net\http\Media::negotiate()
  288. * @see lithium\net\http\Media::type()
  289. * @see lithium\action\Request
  290. * @param object $request The instance of `lithium\action\Request` to be checked against a
  291. * set of conditions (if applicable).
  292. * @param array $config Represents a content type configuration, which is an array containing 3
  293. * keys:
  294. * - `'name'` _string_: The type name, i.e. `'html'` or `'json'`.
  295. * - `'content'` _mixed_: One or more content types that the configuration
  296. * represents, i.e. `'text/html'`, `'application/xhtml+xml'` or
  297. * `'application/json'`, or an array containing multiple content types.
  298. * - `'options'` _array_: An array containing rendering information, and an
  299. * optional `'conditions'` key, which contains an array of matching parameters.
  300. * For more details on these matching parameters, see `Media::type()`.
  301. * @return boolean Returns `true` if the information in `$request` matches the type
  302. * configuration in `$config`, otherwise false.
  303. */
  304. public static function match($request, array $config) {
  305. if (!isset($config['options']['conditions'])) {
  306. return true;
  307. }
  308. $conditions = $config['options']['conditions'];
  309. foreach ($conditions as $key => $value) {
  310. switch (true) {
  311. case $key === 'type':
  312. if ($value !== ($request->type === $config['name'])) {
  313. return false;
  314. }
  315. break;
  316. case strpos($key, ':'):
  317. if ($request->get($key) !== $value) {
  318. return false;
  319. }
  320. break;
  321. case ($request->is($key) !== $value):
  322. return false;
  323. break;
  324. }
  325. }
  326. return true;
  327. }
  328. /**
  329. * Gets or sets options for various asset types.
  330. *
  331. * @see lithium\util\String::insert()
  332. * @param string $type The name of the asset type, i.e. `'js'` or `'css'`.
  333. * @param array $options If registering a new asset type or modifying an existing asset type,
  334. * contains settings for the asset type, where the available keys are as follows:
  335. * - `'suffix'`: The standard suffix for this content type, with leading dot ('.') if
  336. * applicable.
  337. * - `'filter'`: An array of key/value pairs representing simple string replacements to
  338. * be done on a path once it is generated.
  339. * - `'path'`: An array of key/value pairs where the keys are `String::insert()`
  340. * compatible paths, and the values are array lists of keys to be inserted into the
  341. * path string.
  342. * @return array If `$type` is empty, an associative array of all registered types and all
  343. * associated options is returned. If `$type` is a string and `$options` is empty,
  344. * returns an associative array with the options for `$type`. If `$type` and `$options`
  345. * are both non-empty, returns `null`.
  346. */
  347. public static function assets($type = null, $options = array()) {
  348. $defaults = array('suffix' => null, 'filter' => null, 'path' => array());
  349. if (!$type) {
  350. return static::_assets();
  351. }
  352. if ($options === false) {
  353. unset(static::$_assets[$type]);
  354. }
  355. if (!$options) {
  356. return static::_assets($type);
  357. }
  358. $options = (array) $options + $defaults;
  359. if ($base = static::_assets($type)) {
  360. $options = array_merge($base, array_filter($options));
  361. }
  362. static::$_assets[$type] = $options;
  363. }
  364. /**
  365. * Calculates the web-accessible path to a static asset, usually a JavaScript, CSS or image
  366. * file.
  367. *
  368. * @see lithium\net\http\Media::$_assets
  369. * @see lithium\action\Request::env()
  370. * @param string $path The path to the asset, relative to the given `$type`s path and without a
  371. * suffix. If the path contains a URI Scheme (eg. `http://`), no path munging will occur.
  372. * @param string $type The asset type. See `Media::$_assets` or `Media::assets()`.
  373. * @param array $options Contains setting for finding and handling the path, where the keys are
  374. * the following:
  375. * - `'base'`: The base URL of your application. Defaults to `null` for no base path.
  376. * This is usually set with the return value of a call to `env('base')` on an instance
  377. * of `lithium\action\Request`.
  378. * - `'check'`: Check for the existence of the file before returning. Defaults to
  379. * `false`.
  380. * - `'filter'`: An array of key/value pairs representing simple string replacements to
  381. * be done on a path once it is generated.
  382. * - `'path'`: An array of paths to search for the asset in. The paths should use
  383. * `String::insert()` formatting. See `Media::$_assets` for more.
  384. * - `suffix`: The suffix to attach to the path, generally a file extension.
  385. * - `'timestamp'`: Appends the last modified time of the file to the path if `true`.
  386. * Defaults to `false`.
  387. * - `'library'`: The name of the library from which to load the asset. Defaults to
  388. * `true`, for the default library.
  389. * @return string Returns the publicly-accessible absolute path to the static asset. If checking
  390. * for the asset's existence (`$options['check']`), returns `false` if it does not exist
  391. * in your `/webroot` directory, or the `/webroot` directories of one of your included
  392. * plugins.
  393. * @filter
  394. */
  395. public static function asset($path, $type, array $options = array()) {
  396. $defaults = array(
  397. 'base' => null,
  398. 'timestamp' => false,
  399. 'filter' => null,
  400. 'path' => array(),
  401. 'suffix' => null,
  402. 'check' => false,
  403. 'library' => true
  404. );
  405. if (!$base = static::_assets($type)) {
  406. $type = 'generic';
  407. $base = static::_assets('generic');
  408. }
  409. $options += ($base + $defaults);
  410. $params = compact('path', 'type', 'options');
  411. return static::_filter(__FUNCTION__, $params, function($self, $params) {
  412. $path = $params['path'];
  413. $type = $params['type'];
  414. $options = $params['options'];
  415. $library = $options['library'];
  416. if (preg_match('/^(?:[a-z0-9-]+:)?\/\//i', $path)) {
  417. return $path;
  418. }
  419. $config = Libraries::get($library);
  420. $paths = $options['path'];
  421. $config['default'] ? end($paths) : reset($paths);
  422. $options['library'] = basename($config['path']);
  423. if ($options['suffix'] && strpos($path, $options['suffix']) === false) {
  424. $path .= $options['suffix'];
  425. }
  426. return $self::filterAssetPath($path, $paths, $config, compact('type') + $options);
  427. });
  428. }
  429. /**
  430. * Performs checks and applies transformations to asset paths, including verifying that the
  431. * virtual path exists on the filesystem, appending a timestamp, prepending an asset host, or
  432. * applying a user-defined filter.
  433. *
  434. * @see lithium\net\http\Media::asset()
  435. * @param string $asset A full asset path, relative to the public web path of the application.
  436. * @param mixed $path Path information for the asset type.
  437. * @param array $config The configuration array of the library from which the asset is being
  438. * loaded.
  439. * @param array $options The array of options passed to `asset()` (see the `$options` parameter
  440. * of `Media::asset()`).
  441. * @return mixed Returns a modified path to a web asset, or `false`, if the path fails a check.
  442. */
  443. public static function filterAssetPath($asset, $path, array $config, array $options = array()) {
  444. $config += array('assets' => null);
  445. if ($options['check'] || $options['timestamp']) {
  446. $file = static::path($asset, $options['type'], $options);
  447. }
  448. if ($options['check'] && !is_file($file)) {
  449. return false;
  450. }
  451. $isAbsolute = ($asset && $asset[0] === '/');
  452. if ($isAbsolute && $options['base'] && strpos($asset, $options['base']) !== 0) {
  453. $asset = "{$options['base']}{$asset}";
  454. } elseif (!$isAbsolute) {
  455. $asset = String::insert(key($path), array('path' => $asset) + $options);
  456. }
  457. if (is_array($options['filter']) && !empty($options['filter'])) {
  458. $filter = $options['filter'];
  459. $asset = str_replace(array_keys($filter), array_values($filter), $asset);
  460. }
  461. if ($options['timestamp'] && is_file($file)) {
  462. $separator = (strpos($asset, '?') !== false) ? '&' : '?';
  463. $asset .= $separator . filemtime($file);
  464. }
  465. if ($host = $config['assets']) {
  466. $type = $options['type'];
  467. $env = Environment::get();
  468. $base = isset($host[$env][$type]) ? $host[$env][$type] : null;
  469. $base = (isset($host[$type]) && !$base) ? $host[$type] : $base;
  470. if ($base) {
  471. return "{$base}{$asset}";
  472. }
  473. }
  474. return $asset;
  475. }
  476. /**
  477. * Gets the physical path to the web assets (i.e. `/webroot`) directory of a library.
  478. *
  479. * @param string|boolean $library The name of the library for which to find the path, or `true`
  480. * for the default library.
  481. * @return string Returns the physical path to the web assets directory for a library. For
  482. * example, the `/webroot` directory of the default library would be
  483. * `LITHIUM_APP_PATH . '/webroot'`.
  484. */
  485. public static function webroot($library = true) {
  486. if (!$config = Libraries::get($library)) {
  487. return null;
  488. }
  489. if (isset($config['webroot'])) {
  490. return $config['webroot'];
  491. }
  492. if (isset($config['path'])) {
  493. return $config['path'] . '/webroot';
  494. }
  495. }
  496. /**
  497. * Returns the physical path to an asset in the `/webroot` directory of an application or
  498. * plugin.
  499. *
  500. * @param string $path The path to a web asset, relative to the root path for its type. For
  501. * example, for a JavaScript file in `/webroot/js/subpath/file.js`, the correct
  502. * value for `$path` would be `'subpath/file.js'`.
  503. * @param string $type A valid asset type, i.e. `'js'`, `'cs'`, `'image'`, or another type
  504. * registered with `Media::assets()`, or `'generic'`.
  505. * @param array $options The options used to calculate the path to the file.
  506. * @return string Returns the physical filesystem path to an asset in the `/webroot` directory.
  507. */
  508. public static function path($path, $type, array $options = array()) {
  509. $defaults = array(
  510. 'base' => null,
  511. 'path' => array(),
  512. 'suffix' => null,
  513. 'library' => true
  514. );
  515. if (!$base = static::_assets($type)) {
  516. $type = 'generic';
  517. $base = static::_assets('generic');
  518. }
  519. $options += ($base + $defaults);
  520. $config = Libraries::get($options['library']);
  521. $root = static::webroot($options['library']);
  522. $paths = $options['path'];
  523. $config['default'] ? end($paths) : reset($paths);
  524. $options['library'] = basename($config['path']);
  525. if ($qOffset = strpos($path, '?')) {
  526. $path = substr($path, 0, $qOffset);
  527. }
  528. if ($path[0] === '/') {
  529. $file = $root . $path;
  530. } else {
  531. $template = str_replace('{:library}/', '', key($paths));
  532. $insert = array('base' => $root) + compact('path');
  533. $file = String::insert($template, $insert);
  534. }
  535. return realpath($file);
  536. }
  537. /**
  538. * Renders data (usually the result of a controller action) and generates a string
  539. * representation of it, based on the type of expected output.
  540. *
  541. * @param object $response A Response object into which the operation will be
  542. * rendered. The content of the render operation will be assigned to the `$body`
  543. * property of the object, the `'Content-Type'` header will be set accordingly, and it
  544. * will be returned.
  545. * @param mixed $data The data (usually an associative array) to be rendered in the response.
  546. * @param array $options Any options specific to the response being rendered, such as type
  547. * information, keys (i.e. controller and action) used to generate template paths,
  548. * etc.
  549. * @return object Returns a modified `Response` object with headers and body defined.
  550. * @filter
  551. */
  552. public static function render($response, $data = null, array $options = array()) {
  553. $params = compact('response', 'data', 'options');
  554. $types = static::_types();
  555. $handlers = static::handlers();
  556. $func = __FUNCTION__;
  557. return static::_filter($func, $params, function($self, $params) use ($types, $handlers) {
  558. $defaults = array('encode' => null, 'template' => null, 'layout' => '', 'view' => null);
  559. $response = $params['response'];
  560. $data = $params['data'];
  561. $options = $params['options'] + array('type' => $response->type());
  562. $result = null;
  563. $type = $options['type'];
  564. if (!isset($handlers[$type])) {
  565. throw new MediaException("Unhandled media type `{$type}`.");
  566. }
  567. $handler = $options + $handlers[$type] + $defaults;
  568. $filter = function($v) { return $v !== null; };
  569. $handler = array_filter($handler, $filter) + $handlers['default'] + $defaults;
  570. if (isset($types[$type])) {
  571. $header = current((array) $types[$type]);
  572. $header .= $response->encoding ? "; charset={$response->encoding}" : '';
  573. $response->headers('Content-Type', $header);
  574. }
  575. $response->body($self::invokeMethod('_handle', array($handler, $data, $response)));
  576. return $response;
  577. });
  578. }
  579. /**
  580. * Configures a template object instance, based on a media handler configuration.
  581. *
  582. * @see lithium\net\http\Media::type()
  583. * @see lithium\template\View::render()
  584. * @see lithium\action\Response
  585. * @param mixed $handler Either a string specifying the name of a media type for which a handler
  586. * is defined, or an array representing a handler configuration. For more on types
  587. * and type handlers, see the `type()` method.
  588. * @param mixed $data The data to be rendered. Usually an array.
  589. * @param object $response The `Response` object associated with this dispatch cycle. Usually an
  590. * instance of `lithium\action\Response`.
  591. * @param array $options Any options that will be passed to the `render()` method of the
  592. * templating object.
  593. * @return object Returns an instance of a templating object, usually `lithium\template\View`.
  594. * @filter
  595. */
  596. public static function view($handler, $data, &$response = null, array $options = array()) {
  597. $params = array('response' => &$response) + compact('handler', 'data', 'options');
  598. return static::_filter(__FUNCTION__, $params, function($self, $params) {
  599. $data = $params['data'];
  600. $options = $params['options'];
  601. $handler = $params['handler'];
  602. $response =& $params['response'];
  603. $handler = is_array($handler) ? $handler : $self::handlers($handler);
  604. $class = $handler['view'];
  605. unset($handler['view']);
  606. $config = $handler + array('response' => &$response);
  607. return $self::invokeMethod('_instance', array($class, $config));
  608. });
  609. }
  610. /**
  611. * For media types registered in `$_handlers` which include an `'encode'` setting, encodes data
  612. * according to the specified media type.
  613. *
  614. * @see lithium\net\http\Media::type()
  615. * @param mixed $handler Specifies the media type into which `$data` will be encoded. This media
  616. * type must have an `'encode'` setting specified in `Media::$_handlers`.
  617. * Alternatively, `$type` can be an array, in which case it is used as the type
  618. * handler configuration. See the `type()` method for information on adding type
  619. * handlers, and the available configuration keys.
  620. * @param mixed $data Arbitrary data you wish to encode. Note that some encoders can only handle
  621. * arrays or objects.
  622. * @param object $response A reference to the `Response` object for this dispatch cycle.
  623. * @return mixed Returns the result of `$data`, encoded with the encoding configuration
  624. * specified by `$type`, the result of which is usually a string.
  625. * @filter
  626. */
  627. public static function encode($handler, $data, &$response = null) {
  628. $params = array('response' => &$response) + compact('handler', 'data');
  629. return static::_filter(__FUNCTION__, $params, function($self, $params) {
  630. $data = $params['data'];
  631. $handler = $params['handler'];
  632. $response =& $params['response'];
  633. $handler = is_array($handler) ? $handler : $self::handlers($handler);
  634. if (!$handler || empty($handler['encode'])) {
  635. return null;
  636. }
  637. $cast = function($data) {
  638. if (!is_object($data)) {
  639. return $data;
  640. }
  641. return method_exists($data, 'to') ? $data->to('array') : get_object_vars($data);
  642. };
  643. if (!isset($handler['cast']) || $handler['cast']) {
  644. $data = is_object($data) ? $cast($data) : $data;
  645. $data = is_array($data) ? array_map($cast, $data) : $data;
  646. }
  647. $method = $handler['encode'];
  648. return is_string($method) ? $method($data) : $method($data, $handler, $response);
  649. });
  650. }
  651. /**
  652. * For media types registered in `$_handlers` which include an `'decode'` setting, decodes data
  653. * according to the specified media type.
  654. *
  655. * @param string $type Specifies the media type into which `$data` will be encoded. This media
  656. * type must have an `'encode'` setting specified in `Media::$_handlers`.
  657. * @param mixed $data Arbitrary data you wish to encode. Note that some encoders can only handle
  658. * arrays or objects.
  659. * @param array $options Handler-specific options.
  660. * @return mixed
  661. */
  662. public static function decode($type, $data, array $options = array()) {
  663. if ((!$handler = static::handlers($type)) || empty($handler['decode'])) {
  664. return null;
  665. }
  666. $method = $handler['decode'];
  667. return is_string($method) ? $method($data) : $method($data, $handler + $options);
  668. }
  669. /**
  670. * Resets the `Media` class to its default state. Mainly used for ensuring a consistent state
  671. * during testing.
  672. *
  673. * @return void
  674. */
  675. public static function reset() {
  676. foreach (get_class_vars(__CLASS__) as $name => $value) {
  677. static::${$name} = array();
  678. }
  679. }
  680. /**
  681. * Called by `Media::render()` to render response content. Given a content handler and data,
  682. * calls the content handler and passes in the data, receiving back a rendered content string.
  683. *
  684. * @see lithium\action\Response
  685. * @param array $handler
  686. * @param array $data
  687. * @param object $response A reference to the `Response` object for this dispatch cycle.
  688. * @return string
  689. * @filter
  690. */
  691. protected static function _handle($handler, $data, &$response) {
  692. $params = array('response' => &$response) + compact('handler', 'data');
  693. return static::_filter(__FUNCTION__, $params, function($self, $params) {
  694. $response = $params['response'];
  695. $handler = $params['handler'];
  696. $data = $params['data'];
  697. $options = $handler;
  698. if (isset($options['request'])) {
  699. $options += $options['request']->params;
  700. unset($options['request']);
  701. }
  702. switch (true) {
  703. case $handler['encode']:
  704. return $self::encode($handler, $data, $response);
  705. case ($handler['template'] === false) && is_string($data):
  706. return $data;
  707. case $handler['view']:
  708. unset($options['view']);
  709. $instance = $self::view($handler, $data, $response, $options);
  710. return $instance->render('all', (array) $data, $options);
  711. default:
  712. throw new MediaException("Could not interpret type settings for handler.");
  713. }
  714. });
  715. }
  716. /**
  717. * Helper method for listing registered media types. Returns all types, or a single
  718. * content type if a specific type is specified.
  719. *
  720. * @param string $type Type to return.
  721. * @return mixed Array of types, or single type requested.
  722. */
  723. protected static function _types($type = null) {
  724. $types = static::$_types + array(
  725. 'html' => array('text/html', 'application/xhtml+xml', '*/*'),
  726. 'htm' => array('alias' => 'html'),
  727. 'form' => array('application/x-www-form-urlencoded', 'multipart/form-data'),
  728. 'json' => array('application/json'),
  729. 'rss' => array('application/rss+xml'),
  730. 'atom' => array('application/atom+xml'),
  731. 'css' => array('text/css'),
  732. 'js' => array('application/javascript', 'text/javascript'),
  733. 'text' => array('text/plain'),
  734. 'txt' => array('alias' => 'text'),
  735. 'xml' => array('application/xml', 'application/soap+xml', 'text/xml')
  736. );
  737. if (!$type) {
  738. return $types;
  739. }
  740. if (strpos($type, '/') === false) {
  741. return isset($types[$type]) ? $types[$type] : null;
  742. }
  743. if (strpos($type, ';')) {
  744. list($type) = explode(';', $type, 2);
  745. }
  746. $result = array();
  747. foreach ($types as $name => $cTypes) {
  748. if ($type === $cTypes || (is_array($cTypes) && in_array($type, $cTypes))) {
  749. $result[] = $name;
  750. }
  751. }
  752. if (count($result) === 1) {
  753. return reset($result);
  754. }
  755. return $result ?: null;
  756. }
  757. /**
  758. * Helper method for listing registered type handlers. Returns all handlers, or the
  759. * handler for a specific media type, if requested.
  760. *
  761. * @param string $type The type of handler to return.
  762. * @return mixed Array of all handlers, or the handler for a specific type.
  763. */
  764. public static function handlers($type = null) {
  765. $handlers = static::$_handlers + array(
  766. 'default' => array(
  767. 'view' => 'lithium\template\View',
  768. 'encode' => false,
  769. 'decode' => false,
  770. 'cast' => false,
  771. 'paths' => array(
  772. 'template' => '{:library}/views/{:controller}/{:template}.{:type}.php',
  773. 'layout' => '{:library}/views/layouts/{:layout}.{:type}.php',
  774. 'element' => '{:library}/views/elements/{:template}.{:type}.php'
  775. )
  776. ),
  777. 'html' => array(),
  778. 'json' => array(
  779. 'cast' => true,
  780. 'encode' => 'json_encode',
  781. 'decode' => function($data) {
  782. return json_decode($data, true);
  783. }
  784. ),
  785. 'text' => array('cast' => false, 'encode' => function($s) { return $s; }),
  786. 'form' => array(
  787. 'cast' => true,
  788. 'encode' => 'http_build_query',
  789. 'decode' => function($data) {
  790. $decoded = array();
  791. parse_str($data, $decoded);
  792. return $decoded;
  793. }
  794. )
  795. );
  796. if ($type) {
  797. return isset($handlers[$type]) ? $handlers[$type] : null;
  798. }
  799. return $handlers;
  800. }
  801. /**
  802. * Helper method to list all asset paths, or the path for a single type.
  803. *
  804. * @param string $type The type you wish to get paths for.
  805. * @return mixed An array of all paths, or a single array of paths for the
  806. * given type.
  807. */
  808. protected static function _assets($type = null) {
  809. $assets = static::$_assets + array(
  810. 'js' => array('suffix' => '.js', 'filter' => null, 'path' => array(
  811. '{:base}/{:library}/js/{:path}' => array('base', 'library', 'path'),
  812. '{:base}/js/{:path}' => array('base', 'path')
  813. )),
  814. 'css' => array('suffix' => '.css', 'filter' => null, 'path' => array(
  815. '{:base}/{:library}/css/{:path}' => array('base', 'library', 'path'),
  816. '{:base}/css/{:path}' => array('base', 'path')
  817. )),
  818. 'image' => array('suffix' => null, 'filter' => null, 'path' => array(
  819. '{:base}/{:library}/img/{:path}' => array('base', 'library', 'path'),
  820. '{:base}/img/{:path}' => array('base', 'path')
  821. )),
  822. 'generic' => array('suffix' => null, 'filter' => null, 'path' => array(
  823. '{:base}/{:library}/{:path}' => array('base', 'library', 'path'),
  824. '{:base}/{:path}' => array('base', 'path')
  825. ))
  826. );
  827. if ($type) {
  828. return isset($assets[$type]) ? $assets[$type] : null;
  829. }
  830. return $assets;
  831. }
  832. }
  833. ?>