Request.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2012, Union of RAD (http://union-of-rad.org)
  6. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  7. */
  8. namespace lithium\action;
  9. use lithium\util\Set;
  10. use lithium\util\Validator;
  11. /**
  12. * A `Request` object is passed into the `Dispatcher`, and is responsible for identifying and
  13. * storing all the information about an HTTP request made to an application, including status,
  14. * headers, and any GET, POST or PUT data, as well as any data returned from the
  15. * `Router`, after the `Request` object has been matched against a `Route`. Includes a property
  16. * accessor method (`__get()`) which allows any parameters returned from routing to be accessed as
  17. * properties of the `Request` object.
  18. *
  19. * @see lithium\action\Dispatcher
  20. * @see lithium\action\Controller
  21. * @see lithium\net\http\Router
  22. * @see lithium\net\http\Route
  23. * @see lithium\action\Request::__get()
  24. */
  25. class Request extends \lithium\net\http\Request {
  26. /**
  27. * Current url of request.
  28. *
  29. * @var string
  30. */
  31. public $url = null;
  32. /**
  33. * Params for request.
  34. *
  35. * @var array
  36. */
  37. public $params = array();
  38. /**
  39. * Route parameters that should persist when generating URLs in this request context.
  40. *
  41. * @var array
  42. */
  43. public $persist = array();
  44. /**
  45. * POST data.
  46. *
  47. * @var data
  48. */
  49. public $data = array();
  50. /**
  51. * GET data.
  52. *
  53. * @var string
  54. */
  55. public $query = array();
  56. /**
  57. * Base path.
  58. *
  59. * @var string
  60. */
  61. protected $_base = null;
  62. /**
  63. * Holds the environment variables for the request. Retrieved with env().
  64. *
  65. * @var array
  66. * @see lithium\action\Request::env()
  67. */
  68. protected $_env = array();
  69. /**
  70. * Classes used by `Request`.
  71. *
  72. * @var array
  73. */
  74. protected $_classes = array('media' => 'lithium\net\http\Media');
  75. /**
  76. * If POST / PUT data is coming from an input stream (rather than `$_POST`), this specified
  77. * where to read it from.
  78. *
  79. * @var stream
  80. */
  81. protected $_stream = null;
  82. /**
  83. * Options used to detect request type.
  84. *
  85. * @see lithium\action\Request::detect()
  86. * @var array
  87. */
  88. protected $_detectors = array(
  89. 'mobile' => array('HTTP_USER_AGENT', null),
  90. 'ajax' => array('HTTP_X_REQUESTED_WITH', 'XMLHttpRequest'),
  91. 'flash' => array('HTTP_USER_AGENT', 'Shockwave Flash'),
  92. 'ssl' => 'HTTPS',
  93. 'get' => array('REQUEST_METHOD', 'GET'),
  94. 'post' => array('REQUEST_METHOD', 'POST'),
  95. 'put' => array('REQUEST_METHOD', 'PUT'),
  96. 'delete' => array('REQUEST_METHOD', 'DELETE'),
  97. 'head' => array('REQUEST_METHOD', 'HEAD'),
  98. 'options' => array('REQUEST_METHOD', 'OPTIONS')
  99. );
  100. /**
  101. * Auto configuration properties.
  102. *
  103. * @var array
  104. */
  105. protected $_autoConfig = array(
  106. 'classes' => 'merge', 'env', 'detectors' => 'merge', 'base', 'type', 'stream'
  107. );
  108. /**
  109. * Contains an array of content-types, sorted by quality (the priority which the browser
  110. * requests each type).
  111. *
  112. * @var array
  113. */
  114. protected $_acceptContent = array();
  115. /**
  116. * Holds the value of the current locale, set through the `locale()` method.
  117. *
  118. * @var string
  119. */
  120. protected $_locale = null;
  121. /**
  122. * Initialize request object, pulling request data from superglobals.
  123. *
  124. * Defines an artificial `'PLATFORM'` environment variable as either
  125. * `'IIS'`, `'CGI'` or `null` to allow checking for the SAPI in a
  126. * normalized way.
  127. *
  128. * @return void
  129. */
  130. protected function _init() {
  131. parent::_init();
  132. $mobile = array(
  133. 'iPhone', 'MIDP', 'AvantGo', 'BlackBerry', 'J2ME', 'Opera Mini', 'DoCoMo', 'NetFront',
  134. 'Nokia', 'PalmOS', 'PalmSource', 'portalmmm', 'Plucker', 'ReqwirelessWeb', 'iPod',
  135. 'SonyEricsson', 'Symbian', 'UP\.Browser', 'Windows CE', 'Xiino', 'Android'
  136. );
  137. if (!empty($this->_config['detectors']['mobile'][1])) {
  138. $mobile = array_merge($mobile, (array) $this->_config['detectors']['mobile'][1]);
  139. }
  140. $this->_detectors['mobile'][1] = $mobile;
  141. $defaults = array('REQUEST_METHOD' => 'GET', 'CONTENT_TYPE' => 'text/html');
  142. $this->_env += (array) $_SERVER + (array) $_ENV + $defaults;
  143. $envs = array('isapi' => 'IIS', 'cgi' => 'CGI', 'cgi-fcgi' => 'CGI');
  144. $this->_env['PLATFORM'] = isset($envs[PHP_SAPI]) ? $envs[PHP_SAPI] : null;
  145. $this->_base = $this->_base();
  146. $this->url = $this->_url();
  147. if (!empty($this->_config['query'])) {
  148. $this->query = $this->_config['query'];
  149. }
  150. if (isset($_GET)) {
  151. $this->query += $_GET;
  152. }
  153. if (!empty($this->_config['data'])) {
  154. $this->data = $this->_config['data'];
  155. }
  156. if (isset($_POST)) {
  157. $this->data += $_POST;
  158. }
  159. if (isset($this->data['_method'])) {
  160. $this->_env['HTTP_X_HTTP_METHOD_OVERRIDE'] = strtoupper($this->data['_method']);
  161. unset($this->data['_method']);
  162. }
  163. if (!empty($this->_env['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
  164. $this->_env['REQUEST_METHOD'] = $this->_env['HTTP_X_HTTP_METHOD_OVERRIDE'];
  165. }
  166. $type = $this->type($this->_config['type'] ?: $this->env('CONTENT_TYPE'));
  167. $this->method = $method = strtoupper($this->_env['REQUEST_METHOD']);
  168. $hasBody = in_array($method, array('POST', 'PUT', 'PATCH'));
  169. if (!$this->data && $hasBody && $type !== 'html') {
  170. $this->_stream = $this->_stream ?: fopen('php://input', 'r');
  171. $media = $this->_classes['media'];
  172. $this->data = (array) $media::decode($type, stream_get_contents($this->_stream));
  173. fclose($this->_stream);
  174. }
  175. $this->data = Set::merge((array) $this->data, $this->_parseFiles());
  176. }
  177. /**
  178. * Allows request parameters to be accessed as object properties, i.e. `$this->request->action`
  179. * instead of `$this->request->params['action']`.
  180. *
  181. * @see lithium\action\Request::$params
  182. * @param string $name The property name/parameter key to return.
  183. * @return mixed Returns the value of `$params[$name]` if it is set, otherwise returns null.
  184. */
  185. public function __get($name) {
  186. if (isset($this->params[$name])) {
  187. return $this->params[$name];
  188. }
  189. }
  190. /**
  191. * Allows request parameters to be checked using short-hand notation. See the `__get()` method
  192. * for more details.
  193. *
  194. * @see lithium\action\Request::__get()
  195. * @param string $name The name of the request parameter to check.
  196. * @return boolean Returns true if the key in `$name` is set in the `$params` array, otherwise
  197. * `false`.
  198. */
  199. public function __isset($name) {
  200. return isset($this->params[$name]);
  201. }
  202. /**
  203. * Queries PHP's environment settings, and provides an abstraction for standardizing expected
  204. * environment values across varying platforms, as well as specify custom environment flags.
  205. *
  206. * @param string $key The environment variable required.
  207. * @return string The requested variables value.
  208. * @todo Refactor to lazy-load environment settings
  209. */
  210. public function env($key) {
  211. if (strtolower($key) === 'base') {
  212. return $this->_base;
  213. }
  214. if ($key === 'SCRIPT_NAME' && !isset($this->_env['SCRIPT_NAME'])) {
  215. if ($this->_env['PLATFORM'] === 'CGI' || isset($this->_env['SCRIPT_URL'])) {
  216. $key = 'SCRIPT_URL';
  217. }
  218. }
  219. $val = array_key_exists($key, $this->_env) ? $this->_env[$key] : getenv($key);
  220. $this->_env[$key] = $val;
  221. if ($key == 'REMOTE_ADDR') {
  222. $https = array('HTTP_X_FORWARDED_FOR', 'HTTP_PC_REMOTE_ADDR', 'HTTP_X_REAL_IP');
  223. foreach ($https as $altKey) {
  224. if ($addr = $this->env($altKey)) {
  225. $val = $addr;
  226. break;
  227. }
  228. }
  229. }
  230. if ($val !== null && $val !== false && $key !== 'HTTPS') {
  231. return $val;
  232. }
  233. switch ($key) {
  234. case 'HTTPS':
  235. if (isset($this->_env['SCRIPT_URI'])) {
  236. return (strpos($this->_env['SCRIPT_URI'], 'https://') === 0);
  237. }
  238. if (isset($this->_env['HTTPS'])) {
  239. return (!empty($this->_env['HTTPS']) && $this->_env['HTTPS'] !== 'off');
  240. }
  241. return false;
  242. case 'SERVER_ADDR':
  243. if (empty($this->_env['SERVER_ADDR']) && !empty($this->_env['LOCAL_ADDR'])) {
  244. return $this->_env['LOCAL_ADDR'];
  245. }
  246. return $this->_env['SERVER_ADDR'];
  247. case 'SCRIPT_FILENAME':
  248. if ($this->_env['PLATFORM'] == 'IIS') {
  249. return str_replace('\\\\', '\\', $this->env('PATH_TRANSLATED'));
  250. }
  251. return $this->env('DOCUMENT_ROOT') . $this->env('PHP_SELF');
  252. case 'DOCUMENT_ROOT':
  253. $fileName = $this->env('SCRIPT_FILENAME');
  254. $offset = (!strpos($this->env('SCRIPT_NAME'), '.php')) ? 4 : 0;
  255. $offset = strlen($fileName) - (strlen($this->env('SCRIPT_NAME')) + $offset);
  256. return substr($fileName, 0, $offset);
  257. case 'PHP_SELF':
  258. return str_replace('\\', '/', str_replace(
  259. $this->env('DOCUMENT_ROOT'), '', $this->env('SCRIPT_FILENAME')
  260. ));
  261. case 'CGI':
  262. case 'CGI_MODE':
  263. return ($this->_env['PLATFORM'] === 'CGI');
  264. case 'HTTP_BASE':
  265. return preg_replace('/^([^.])*/i', null, $this->_env['HTTP_HOST']);
  266. }
  267. }
  268. /**
  269. * Returns information about the type of content that the client is requesting.
  270. *
  271. * @see lithium\net\http\Media::negotiate()
  272. * @param $type mixed If not specified, returns the media type name that the client prefers,
  273. * using content negotiation. If a media type name (string) is passed, returns
  274. * `true` or `false`, indicating whether or not that type is accepted by the client
  275. * at all. If `true`, returns the raw content types from the `Accept` header,
  276. * parsed into an array and sorted by client preference.
  277. * @return string Returns a simple type name if the type is registered (i.e. `'json'`), or
  278. * a fully-qualified content-type if not (i.e. `'image/jpeg'`), or a boolean or array,
  279. * depending on the value of `$type`.
  280. */
  281. public function accepts($type = null) {
  282. if ($type === true) {
  283. return $this->_parseAccept();
  284. }
  285. if (!$type && isset($this->params['type'])) {
  286. return $this->params['type'];
  287. }
  288. $media = $this->_classes['media'];
  289. return $media::negotiate($this) ?: 'html';
  290. }
  291. protected function _parseAccept() {
  292. if ($this->_acceptContent) {
  293. return $this->_acceptContent;
  294. }
  295. $accept = $this->env('HTTP_ACCEPT');
  296. $accept = (preg_match('/[a-z,-]/i', $accept)) ? explode(',', $accept) : array('text/html');
  297. foreach (array_reverse($accept) as $i => $type) {
  298. unset($accept[$i]);
  299. list($type, $q) = (explode(';q=', $type, 2) + array($type, 1.0 + $i / 100));
  300. $accept[$type] = ($type === '*/*') ? 0.1 : floatval($q);
  301. }
  302. arsort($accept, SORT_NUMERIC);
  303. if (isset($accept['application/xhtml+xml']) && $accept['application/xhtml+xml'] >= 1) {
  304. unset($accept['application/xml']);
  305. }
  306. $media = $this->_classes['media'];
  307. if (isset($this->params['type']) && ($handler = $media::type($this->params['type']))) {
  308. if (isset($handler['content'])) {
  309. $type = (array) $handler['content'];
  310. $accept = array(current($type) => 1) + $accept;
  311. }
  312. }
  313. return $this->_acceptContent = array_keys($accept);
  314. }
  315. /**
  316. * This method allows easy extraction of any request data using a prefixed key syntax. By
  317. * passing keys in the form of `'prefix:key'`, it is possible to query different information of
  318. * various different types, including GET and POST data, and server environment variables. The
  319. * full list of prefixes is as follows:
  320. *
  321. * - `'data'`: Retrieves values from POST data.
  322. * - `'params'`: Retrieves query parameters returned from the routing system.
  323. * - `'query'`: Retrieves values from GET data.
  324. * - `'env'`: Retrieves values from the server or environment, such as `'env:https'`, or custom
  325. * environment values, like `'env:base'`. See the `env()` method for more info.
  326. * - `'http'`: Retrieves header values (i.e. `'http:accept'`), or the HTTP request method (i.e.
  327. * `'http:method'`).
  328. *
  329. * This method is used in several different places in the framework in order to provide the
  330. * ability to act conditionally on different aspects of the request. See `Media::type()` (the
  331. * section on content negotiation) and the routing system for more information.
  332. *
  333. * _Note_: All keys should be _lower-cased_, even when getting HTTP headers.
  334. * @see lithium\action\Request::env()
  335. * @see lithium\net\http\Media::type()
  336. * @see lithium\net\http\Router
  337. * @param string $key A prefixed key indicating what part of the request data the requested
  338. * value should come from, and the name of the value to retrieve, in lower case.
  339. * @return string Returns the value of a GET, POST, routing or environment variable, or an
  340. * HTTP header or method name.
  341. */
  342. public function get($key) {
  343. list($var, $key) = explode(':', $key);
  344. switch (true) {
  345. case in_array($var, array('params', 'data', 'query')):
  346. return isset($this->{$var}[$key]) ? $this->{$var}[$key] : null;
  347. case ($var === 'env'):
  348. return $this->env(strtoupper($key));
  349. case ($var === 'http' && $key === 'method'):
  350. return $this->env('REQUEST_METHOD');
  351. case ($var === 'http'):
  352. return $this->env('HTTP_' . strtoupper($key));
  353. }
  354. }
  355. /**
  356. * Provides a simple syntax for making assertions about the properties of a request.
  357. * By default, the `Request` object is configured with several different types of assertions,
  358. * which are individually known as _detectors_. Detectors are invoked by calling the `is()` and
  359. * passing the name of the detector flag, i.e. `$request->is('<name>')`, which returns `true` or
  360. * `false`, depending on whether or not the the properties (usually headers or data) contained
  361. * in the request match the detector. The default detectors include the following:
  362. *
  363. * - `'mobile'`: Uses a regular expression to match common mobile browser user agents.
  364. * - `'ajax'`: Checks to see if the `X-Requested-With` header is present, and matches the value
  365. * `'XMLHttpRequest'`.
  366. * - `'flash'`: Checks to see if the user agent is `'Shockwave Flash'`.
  367. * - `'ssl'`: Verifies that the request is SSL-secured.
  368. * - `'get'` / `'post'` / `'put'` / `'delete'` / `'head'` / `'options'`: Checks that the HTTP
  369. * request method matches the one specified.
  370. *
  371. * In addition to the above, this method also accepts media type names (see `Media::type()`) to
  372. * make assertions against the format of the request body (for POST or PUT requests), i.e.
  373. * `$request->is('json')`. This will return `true` if the client has made a POST request with
  374. * JSON data.
  375. *
  376. * For information about adding custom detectors or overriding the ones in the core, see the
  377. * `detect()` method.
  378. *
  379. * While these detectors are useful in controllers or other similar contexts, they're also
  380. * useful when performing _content negotiation_, which is the process of modifying the response
  381. * format to suit the client (see the `'conditions'` field of the `$options` parameter in
  382. * `Media::type()`).
  383. *
  384. * @see lithium\action\Request::detect()
  385. * @see lithium\net\http\Media::type()
  386. * @param string $flag The name of the flag to check, which should be the name of a valid
  387. * detector (that is either built-in or defined with `detect()`).
  388. * @return boolean Returns `true` if the detector check succeeds (see the details for the
  389. * built-in detectors above, or `detect()`), otherwise `false`.
  390. */
  391. public function is($flag) {
  392. $media = $this->_classes['media'];
  393. if (!isset($this->_detectors[$flag])) {
  394. if (!in_array($flag, $media::types())) {
  395. return false;
  396. }
  397. return $this->type() == $flag;
  398. }
  399. $detector = $this->_detectors[$flag];
  400. if (!is_array($detector) && is_callable($detector)) {
  401. return $detector($this);
  402. }
  403. if (!is_array($detector)) {
  404. return (boolean) $this->env($detector);
  405. }
  406. list($key, $check) = $detector + array('', '');
  407. if (is_array($check)) {
  408. $check = '/' . join('|', $check) . '/i';
  409. }
  410. if (Validator::isRegex($check)) {
  411. return (boolean) preg_match($check, $this->env($key));
  412. }
  413. return ($this->env($key) == $check);
  414. }
  415. /**
  416. * Sets/Gets the content type. If `'type'` is null, the method will attempt to determine the
  417. * type from the params, then from the environment setting
  418. *
  419. * @param string $type a full content type i.e. `'application/json'` or simple name `'json'`
  420. * @return string A simple content type name, i.e. `'html'`, `'xml'`, `'json'`, etc., depending
  421. * on the content type of the request.
  422. */
  423. public function type($type = null) {
  424. if (!$type && !empty($this->params['type'])) {
  425. $type = $this->params['type'];
  426. }
  427. return parent::type($type);
  428. }
  429. /**
  430. * Creates a _detector_ used with `Request::is()`. A detector is a boolean check that is
  431. * created to determine something about a request.
  432. *
  433. * A detector check can be either an exact string match or a regular expression match against a
  434. * header or environment variable. A detector check can also be a closure that accepts the
  435. * `Request` object instance as a parameter.
  436. *
  437. * For example, to detect whether a request is from an iPhone, you can do the following:
  438. * {{{ embed:lithium\tests\cases\action\RequestTest::testDetect(11-12) }}}
  439. *
  440. * @see lithium\action\Request::is()
  441. * @param string $flag The name of the detector check. Used in subsequent calls to
  442. * `Request::is()`.
  443. * @param mixed $detector Detectors can be specified in four different ways:
  444. * - The name of an HTTP header or environment variable. If a string, calling the
  445. * detector will check that the header or environment variable exists and is set
  446. * to a non-empty value.
  447. * - A two-element array containing a header/environment variable name, and a value
  448. * to match against. The second element of the array must be an exact match to
  449. * the header or variable value.
  450. * - A two-element array containing a header/environment variable name, and a
  451. * regular expression that matches against the value, as in the example above.
  452. * - A closure which accepts an instance of the `Request` object and returns a
  453. * boolean value.
  454. * @return void
  455. */
  456. public function detect($flag, $detector = null) {
  457. if (is_array($flag)) {
  458. $this->_detectors = $flag + $this->_detectors;
  459. } else {
  460. $this->_detectors[$flag] = $detector;
  461. }
  462. }
  463. /**
  464. * Gets the referring URL of this request.
  465. *
  466. * @param string $default Default URL to use if HTTP_REFERER cannot be read from headers.
  467. * @param boolean $local If true, restrict referring URLs to local server.
  468. * @return string Referring URL.
  469. */
  470. public function referer($default = null, $local = false) {
  471. if ($ref = $this->env('HTTP_REFERER')) {
  472. if (!$local) {
  473. return $ref;
  474. }
  475. if (strpos($ref, '://') === false) {
  476. return $ref;
  477. }
  478. }
  479. return ($default != null) ? $default : '/';
  480. }
  481. /**
  482. * Overrides `lithium\net\http\Request::to()` to provide the correct options for generating
  483. * URLs. For information about this method, see the parent implementation.
  484. *
  485. * @see lithium\net\http\Request::to()
  486. * @param string $format The format to convert to.
  487. * @param array $options Override options.
  488. * @return mixed The return value type depends on `$format`.
  489. */
  490. public function to($format, array $options = array()) {
  491. $defaults = array(
  492. 'scheme' => $this->env('HTTPS') ? 'https' : 'http',
  493. 'host' => $this->env('HTTP_HOST'),
  494. 'path' => $this->_base . '/' . $this->url
  495. );
  496. return parent::to($format, $options + $defaults);
  497. }
  498. /**
  499. * Sets or returns the current locale string. For more information, see
  500. * "[Globalization](http://lithify.me/docs/manual/07_globalization)" in the manual.
  501. *
  502. * @param string $locale An optional locale string like `'en'`, `'en_US'` or `'de_DE'`. If
  503. * specified, will overwrite the existing locale.
  504. * @return Returns the currently set locale string.
  505. */
  506. public function locale($locale = null) {
  507. if ($locale) {
  508. $this->_locale = $locale;
  509. }
  510. if ($this->_locale) {
  511. return $this->_locale;
  512. }
  513. if (isset($this->params['locale'])) {
  514. return $this->params['locale'];
  515. }
  516. }
  517. /**
  518. * Find the base path of the current request.
  519. *
  520. * @todo Replace string directory names with configuration.
  521. * @return string
  522. */
  523. protected function _base() {
  524. if (isset($this->_base)) {
  525. return $this->_base;
  526. }
  527. $base = str_replace('\\', '/', dirname($this->env('PHP_SELF')));
  528. return rtrim(str_replace(array("/app/webroot", '/webroot'), '', $base), '/');
  529. }
  530. /**
  531. * Return the full url of the current request.
  532. *
  533. * @return string
  534. */
  535. protected function _url() {
  536. if (isset($this->_config['url'])) {
  537. return rtrim($this->_config['url'], '/');
  538. }
  539. if (!empty($_GET['url'])) {
  540. return rtrim($_GET['url'], '/');
  541. }
  542. if ($uri = $this->env('REQUEST_URI')) {
  543. list($uri) = explode('?', $uri, 2);
  544. $base = '/^' . preg_quote($this->env('base'), '/') . '/';
  545. return trim(preg_replace($base, '', $uri), '/') ?: '/';
  546. }
  547. return '/';
  548. }
  549. /**
  550. * Normalize the data in $_FILES
  551. *
  552. * @return array
  553. */
  554. protected function _parseFiles() {
  555. if (isset($_FILES) && $_FILES) {
  556. $result = array();
  557. $normalize = function($key, $value) use ($result, &$normalize){
  558. foreach ($value as $param => $content) {
  559. foreach ($content as $num => $val) {
  560. if (is_numeric($num)) {
  561. $result[$key][$num][$param] = $val;
  562. continue;
  563. }
  564. if (is_array($val)) {
  565. foreach ($val as $next => $one) {
  566. $result[$key][$num][$next][$param] = $one;
  567. }
  568. continue;
  569. }
  570. $result[$key][$num][$param] = $val;
  571. }
  572. }
  573. return $result;
  574. };
  575. foreach ($_FILES as $key => $value) {
  576. if (isset($value['name'])) {
  577. if (is_string($value['name'])) {
  578. $result[$key] = $value;
  579. continue;
  580. }
  581. if (is_array($value['name'])) {
  582. $result += $normalize($key, $value);
  583. }
  584. }
  585. }
  586. return $result;
  587. }
  588. return array();
  589. }
  590. }
  591. ?>