Skip to content

Commit

Permalink
Add RequestFactory and RequestTransformer.
Browse files Browse the repository at this point in the history
The ServerRequestFactory is responsible for:

* Building a request from the SAPI super globals.
* Extracting the base and webroot directories for backwards
  compatibility with the CakeRequest.
* Updating the request path to reflect only the application's
  'virtual path'

The RequestTransformer handles:

* Converting a PSR7 request object into the equivalent Cake\Network\Http
  request.
* Ensuring that the required routing parameters are set even if the PSR7
  request is missing the 'params' attribute.

In order to shim the backwards compatibility, we'll use 3 attributes
which keep track of CakePHP specific path information and our routing
parameters. I felt this was the cleanest approach as I wasn't
comfortable subclassing Diactoros\ServerRequest to add methods for these
just yet.
  • Loading branch information
markstory committed Apr 7, 2016
1 parent b89a845 commit ed8cbc1
Show file tree
Hide file tree
Showing 4 changed files with 725 additions and 0 deletions.
140 changes: 140 additions & 0 deletions src/Http/RequestTransformer.php
@@ -0,0 +1,140 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since 3.3.0
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Http;

use Cake\Core\Configure;
use Cake\Network\Request as CakeRequest;
use Cake\Utility\Hash;
use Psr\Http\Message\ServerRequestInterface as PsrRequest;

/**
* Translate and transform from PSR7 requests into CakePHP requests.
*
* This is an important step for maintaining backwards compatibility
* with existing CakePHP applications, which depend on the CakePHP request object.
*
* There is no reverse transform as the 'application' cannot return a mutated
* request object.
*
* @internal
*/
class RequestTransformer
{
/**
* Transform a PSR7 request into a CakePHP one.
*
* @param \Psr\Http\Message\ServerRequestInterface $request The PSR7 request.
* @return \Cake\Network\Request The transformed request.
*/
public static function toCake(PsrRequest $request)
{
$post = $request->getParsedBody();
$server = $request->getServerParams();

$files = static::getFiles($request);
if (!empty($files)) {
$post = Hash::merge($post, $files);
}

return new CakeRequest([
'query' => $request->getQueryParams(),
'post' => $post,
'cookies' => $request->getCookieParams(),
'environment' => $server,
'params' => static::getParams($request),
'url' => $request->getUri()->getPath(),
'base' => $request->getAttribute('base', ''),
'webroot' => $request->getAttribute('webroot', '/'),
]);
}

/**
* Extract the routing parameters out of the request object.
*
* @param \Psr\Http\Message\ServerRequestInterface $request The request to extract params from.
* @return array The routing parameters.
*/
protected static function getParams(PsrRequest $request)
{
$params = (array)$request->getAttribute('params', []);
$params += [
'plugin' => null,
'controller' => null,
'action' => null,
'_ext' => null,
'pass' => []
];
return $params;
}

/**
* Extract the uploaded files out of the request object.
*
* CakePHP expects to get arrays of file information and
* not the parsed objects that PSR7 requests contain. Downsample the data here.
*
* @param \Psr\Http\Message\ServerRequestInterface $request The request to extract files from.
* @return array The routing parameters.
*/
protected static function getFiles($request)
{
return static::convertFiles([], $request->getUploadedFiles());
}

/**
* Convert a nested array of files to arrays.
*
* @param array $data The data to add files to.
* @param array $files The file objects to convert.
* @param string $path The current array path.
* @return array Converted file data
*/
protected static function convertFiles($data, $files, $path = '')
{
foreach ($files as $key => $file) {
$newPath = $path;
if ($newPath === '') {
$newPath = $key;
}
if ($newPath !== $key) {
$newPath .= '.' . $key;
}

if (is_array($file)) {
$data = static::convertFiles($data, $file, $newPath);
} else {
$data = Hash::insert($data, $newPath, static::convertFile($file));
}
}
return $data;
}

/**
* Convert a single file back into an array.
*
* @param \Psr\Http\Message\UploadedFileInterface $file The file to convert.
* @return array
*/
protected static function convertFile($file)
{
return [
'name' => $file->getClientFilename(),
'type' => $file->getClientMediaType(),
'tmp_name' => $file->getStream()->getMetadata('uri'),
'error' => $file->getError(),
'size' => $file->getSize(),
];
}
}
136 changes: 136 additions & 0 deletions src/Http/ServerRequestFactory.php
@@ -0,0 +1,136 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since 3.3.0
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Http;

use Cake\Core\Configure;
use Cake\Utility\Hash;
use Zend\Diactoros\ServerRequestFactory as BaseFactory;

/**
* Factory for making ServerRequest instances.
*
* This subclass adds in CakePHP specific behavior to populate
* the basePath and webroot attributes. Furthermore the Uri's path
* is corrected to only contain the 'virtual' path for the request.
*/
abstract class ServerRequestFactory extends BaseFactory
{
/**
* {@inheritDoc}
*/
public static function fromGlobals(
array $server = null,
array $query = null,
array $body = null,
array $cookies = null,
array $files = null
) {
$request = parent::fromGlobals($server, $query, $body, $cookies, $files);
list($base, $webroot) = static::getBase($request);
$request = $request->withAttribute('base', $base)
->withAttribute('webroot', $webroot);
if ($base) {
$request = static::updatePath($base, $request);
}
return $request;
}

/**
* Updates the request URI to remove the base directory.
*
* @param string $base The base path to remove.
* @param \Psr\Http\Message\ServerRequestInterface $request The request to modify.
* @return \Psr\Http\Message\ServerRequestInterface The modified request.
*/
protected static function updatePath($base, $request)
{
$uri = $request->getUri();
$path = $uri->getPath();
if (strlen($base) > 0 && strpos($path, $base) === 0) {
$path = substr($path, strlen($base));
}
if (empty($path) || $path === '/' || $path === '//' || $path === '/index.php') {
$path = '/';
}
$endsWithIndex = '/webroot/index.php';
$endsWithLength = strlen($endsWithIndex);
if (strlen($path) >= $endsWithLength &&
substr($path, -$endsWithLength) === $endsWithIndex
) {
$path = '/';
}
return $request->withUri($uri->withPath($path));
}

/**
* Calculate the base directory and webroot directory.
*
* This code is a copy/paste from Cake\Network\Request::_base()
*
* @param \Psr\Http\Message\ServerRequestInterface
* @return array An array containing the [baseDir, webroot]
*/
protected static function getBase($request)
{
$path = $request->getUri()->getPath();
$server = $request->getServerParams();

$base = $webroot = $baseUrl = null;
$config = Configure::read('App');
extract($config);

if ($base !== false && $base !== null) {
return [$base, $base . '/'];
}

if (!$baseUrl) {
$base = dirname(Hash::get($server, 'PHP_SELF'));
// Clean up additional / which cause following code to fail..
$base = preg_replace('#/+#', '/', $base);

$indexPos = strpos($base, '/' . $webroot . '/index.php');
if ($indexPos !== false) {
$base = substr($base, 0, $indexPos) . '/' . $webroot;
}
if ($webroot === basename($base)) {
$base = dirname($base);
}

if ($base === DIRECTORY_SEPARATOR || $base === '.') {
$base = '';
}
$base = implode('/', array_map('rawurlencode', explode('/', $base)));
return [$base, $base . '/'];
}

$file = '/' . basename($baseUrl);
$base = dirname($baseUrl);

if ($base === DIRECTORY_SEPARATOR || $base === '.') {
$base = '';
}
$webrootDir = $base . '/';

$docRoot = Hash::get($server, 'DOCUMENT_ROOT');
$docRootContainsWebroot = strpos($docRoot, $webroot);

if (!empty($base) || !$docRootContainsWebroot) {
if (strpos($webrootDir, '/' . $webroot . '/') === false) {
$webrootDir .= $webroot . '/';
}
}
return [$base . $file, $webrootDir];
}
}

0 comments on commit ed8cbc1

Please sign in to comment.