Skip to content

Commit

Permalink
AppFramework StreamResponse
Browse files Browse the repository at this point in the history
First stab at the StreamResponse, see #12988

The idea is to use an interface ICallbackResponse (I'm not 100% happy with the name yet, suggestions?) that allow the response to output things in its own way, for instance stream the file using readfile

Unittests are atm lacking, plan is to

check if a mock of ICallbackResponse will be used by calling its callback (also unhappy with this name) method
Usage is:

$response = new StreamResponse('path/to/file');
  • Loading branch information
Bernhard Posselt authored and LukasReschke committed Feb 16, 2015
1 parent c6705ab commit b716c3b
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 30 deletions.
40 changes: 29 additions & 11 deletions lib/private/appframework/app.php
Expand Up @@ -24,9 +24,10 @@

namespace OC\AppFramework;

use \OC_App;
use \OC\AppFramework\DependencyInjection\DIContainer;
use \OCP\AppFramework\QueryException;
use OC_App;
use OC\AppFramework\DependencyInjection\DIContainer;
use OCP\AppFramework\QueryException;
use OCP\AppFramework\Http\ICallbackResponse;

/**
* Entry point for every request in your app. You can consider this as your
Expand Down Expand Up @@ -93,28 +94,45 @@ public static function main($controllerName, $methodName, DIContainer $container
// initialize the dispatcher and run all the middleware before the controller
$dispatcher = $container['Dispatcher'];

list($httpHeaders, $responseHeaders, $responseCookies, $output) =
$dispatcher->dispatch($controller, $methodName);
list(
$httpHeaders,
$responseHeaders,
$responseCookies,
$output,
$response
) = $dispatcher->dispatch($controller, $methodName);

$io = $container['IO'];

if(!is_null($httpHeaders)) {
header($httpHeaders);
$io->setHeader($httpHeaders);
}

foreach($responseHeaders as $name => $value) {
header($name . ': ' . $value);
$io->setHeader($name . ': ' . $value);
}

foreach($responseCookies as $name => $value) {
$expireDate = null;
if($value['expireDate'] instanceof \DateTime) {
$expireDate = $value['expireDate']->getTimestamp();
}
setcookie($name, $value['value'], $expireDate, $container->getServer()->getWebRoot(), null, $container->getServer()->getConfig()->getSystemValue('forcessl', false), true);
$io->setCookie(
$name,
$value['value'],
$expireDate,
$container->getServer()->getWebRoot(),
null,
$container->getServer()->getConfig()->getSystemValue('forcessl', false),
true
);
}

if(!is_null($output)) {
header('Content-Length: ' . strlen($output));
print($output);
if ($response instanceof ICallbackResponse) {
$response->callback();
} else if(!is_null($output)) {
$io->setHeader('Content-Length: ' . strlen($output));
$io->setOutput($output);
}

}
Expand Down
5 changes: 5 additions & 0 deletions lib/private/appframework/dependencyinjection/dicontainer.php
Expand Up @@ -26,6 +26,7 @@

use OC;
use OC\AppFramework\Http;
use OC\AppFramework\Http\IO;
use OC\AppFramework\Http\Request;
use OC\AppFramework\Http\Dispatcher;
use OC\AppFramework\Core\API;
Expand Down Expand Up @@ -310,6 +311,10 @@ public function __construct($appName, $urlParams = array()){
return new ControllerMethodReflector();
});

$this->registerService('IO', function($c){
return new IO();
});

}


Expand Down
10 changes: 4 additions & 6 deletions lib/private/appframework/http/dispatcher.php
Expand Up @@ -100,17 +100,15 @@ public function dispatch(Controller $controller, $methodName) {
$response = $this->middlewareDispatcher->afterController(
$controller, $methodName, $response);

// get the output which should be printed and run the after output
// middleware to modify the response
$output = $response->render();
$out[3] = $this->middlewareDispatcher->beforeOutput(
$controller, $methodName, $output);

// depending on the cache object the headers need to be changed
$out[0] = $this->protocol->getStatusHeader($response->getStatus(),
$response->getLastModified(), $response->getETag());
$out[1] = array_merge($response->getHeaders());
$out[2] = $response->getCookies();
$out[3] = $this->middlewareDispatcher->beforeOutput(
$controller, $methodName, $response->render()
);
$out[4] = $response;

return $out;
}
Expand Down
45 changes: 45 additions & 0 deletions lib/private/appframework/http/io.php
@@ -0,0 +1,45 @@
<?php
/**
* @author Bernhard Posselt
* @copyright 2015 Bernhard Posselt <dev@bernhard-posselt.com>
*
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
*/

namespace OC\AppFramework\Http;

/**
* Very thin wrapper class to make output testable
*/
class IO {

/**
* @param $out
*/
public function setOutput($out) {
print($out);
}

/**
* @param $header
*/
public function setHeader($header) {
header($header);
}

/**
* @param $name
* @param $value
* @param $expire
* @param $path
* @param $domain
* @param $secure
* @param $httponly
*/
public function setCookie($name, $value, $expire, $path, $domain, $secure, $httponly) {
setcookie($name, $value, $expire, $path, $domain, $secure, $httponly);
}

}
25 changes: 25 additions & 0 deletions lib/public/appframework/http/icallbackresponse.php
@@ -0,0 +1,25 @@
<?php
/**
* @author Bernhard Posselt
* @copyright 2015 Bernhard Posselt <dev@bernhard-posselt.com>
*
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
*/

namespace OCP\AppFramework\Http;

/**
* Interface ICallbackResponse
*
* @package OCP\AppFramework\Http
*/
interface ICallbackResponse {

/**
* Outputs the content that should be printed
*/
function callback();

}
37 changes: 37 additions & 0 deletions lib/public/appframework/http/streamresponse.php
@@ -0,0 +1,37 @@
<?php
/**
* @author Bernhard Posselt
* @copyright 2015 Bernhard Posselt <dev@bernhard-posselt.com>
*
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
*/

namespace OCP\AppFramework\Http;

/**
* Class StreamResponse
*
* @package OCP\AppFramework\Http
*/
class StreamResponse extends Response implements ICallbackResponse {
/** @var string */
private $filePath;

/**
* @param string $filePath the path to the file which should be streamed
*/
public function __construct ($filePath) {
$this->filePath = $filePath;
}


/**
* Streams the file using readfile
*/
public function callback () {
@readfile($this->filePath);
}

}
43 changes: 30 additions & 13 deletions tests/lib/appframework/AppTest.php
Expand Up @@ -24,6 +24,10 @@

namespace OC\AppFramework;

use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\IO;


function rrmdir($directory) {
$files = array_diff(scandir($directory), array('.','..'));
foreach ($files as $file) {
Expand All @@ -36,9 +40,11 @@ function rrmdir($directory) {
return rmdir($directory);
}


class AppTest extends \Test\TestCase {

private $container;
private $io;
private $api;
private $controller;
private $dispatcher;
Expand All @@ -62,6 +68,7 @@ protected function setUp() {
->disableOriginalConstructor()
->getMock();

$this->io = $this->getMockBuilder('OC\AppFramework\Http\IO')->getMock();

$this->headers = array('key' => 'value');
$this->output = 'hi';
Expand All @@ -70,6 +77,7 @@ protected function setUp() {

$this->container[$this->controllerName] = $this->controller;
$this->container['Dispatcher'] = $this->dispatcher;
$this->container['IO'] = $this->io;
$this->container['urlParams'] = array();

$this->appPath = __DIR__ . '/../../../apps/namespacetestapp/appinfo';
Expand All @@ -86,14 +94,15 @@ protected function setUp() {


public function testControllerNameAndMethodAreBeingPassed(){
$return = array(null, array(), array(), null);
$return = array(null, array(), array(), null, new Response());
$this->dispatcher->expects($this->once())
->method('dispatch')
->with($this->equalTo($this->controller),
$this->equalTo($this->controllerMethod))
->will($this->returnValue($return));

$this->expectOutputString('');
$this->io->expects($this->never())
->method('setOutput');

App::main($this->controllerName, $this->controllerMethod,
$this->container);
Expand Down Expand Up @@ -122,26 +131,34 @@ protected function tearDown() {
rrmdir($this->appPath);
}

/*
FIXME: this complains about shit headers which are already sent because
of the content length. Would be cool if someone could fix this

public function testOutputIsPrinted(){
$return = array(null, array(), $this->output);
$return = [null, [], [], $this->output, new Response()];
$this->dispatcher->expects($this->once())
->method('dispatch')
->with($this->equalTo($this->controller),
$this->equalTo($this->controllerMethod))
->will($this->returnValue($return));
$this->expectOutputString($this->output);
App::main($this->controllerName, $this->controllerMethod, array(),
$this->container);
$this->io->expects($this->once())
->method('setOutput')
->with($this->equalTo($this->output));
App::main($this->controllerName, $this->controllerMethod, $this->container, []);
}
*/

// FIXME: if someone manages to test the headers output, I'd be grateful

public function testCallbackIsCalled(){
$mock = $this->getMockBuilder('OCP\AppFramework\Http\ICallbackResponse')
->getMock();

$return = [null, [], [], $this->output, $mock];
$this->dispatcher->expects($this->once())
->method('dispatch')
->with($this->equalTo($this->controller),
$this->equalTo($this->controllerMethod))
->will($this->returnValue($return));
$mock->expects($this->once())
->method('callback');
App::main($this->controllerName, $this->controllerMethod, $this->container, []);
}

}

0 comments on commit b716c3b

Please sign in to comment.