Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Secured Links 2.0 #11

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
@@ -12,21 +12,28 @@
}
],
"require": {
"php": ">=5.4",
"php": ">=5.6",
"nette/application": "~2.2",
"nette/utils": "~2.2"
},
"require-dev": {
"nette/di": "~2.2",
"nette/tester": "~1.3",
"mockery/mockery": "~0.9"
"tracy/tracy": "~2.2",
"mockery/mockery": "~0.9",
"nikic/php-parser": "~2.0"
},
"suggest": {
"nette/di": "to use SecuredLinksExtension",
"nikic/php-parser": "to detect return types without @return annotation"
},
"extra": {
"branch-alias": {
"dev-master": "1.3-dev"
}
},
"autoload": {
"psr-4": { "Nextras\\Application\\UI\\": "src/" }
"psr-4": { "Nextras\\SecuredLinks\\": "src/" }
},
"replace": {
"nextras/application": "self.version"
209 changes: 209 additions & 0 deletions src/Bridges/NetteDI/SecuredLinksExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
<?php

/**
* This file is part of the Nextras Secured Links library.
*
* @license MIT
* @link https://github.com/nextras/secured-links
*/

namespace Nextras\SecuredLinks\Bridges\NetteDI;

use Generator;
use Nette;
use Nette\Application\IRouter;
use Nette\Application\UI\Presenter;
use Nette\DI\PhpReflection;
use Nette\Neon\Neon;
use Nette\Utils\Strings;
use Nextras\SecuredLinks\Bridges\PhpParser\ReturnTypeResolver;
use Nextras\SecuredLinks\RedirectChecker;
use Nextras\SecuredLinks\SecuredRouterFactory;
use PhpParser\Node;
use ReflectionClass;
use ReflectionMethod;


class SecuredLinksExtension extends Nette\DI\CompilerExtension
{

/** @var array */
public $defaults = [
'annotation' => 'secured', // can be NULL to disable
'destinations' => [],
'strictMode' => TRUE,
];


/**
* @inheritdoc
*/
public function beforeCompile()
{
$builder = $this->getContainerBuilder();
$builder->addDefinition($this->prefix('routerFactory'))
->setImplement(SecuredRouterFactory::class)
->setParameters(['Nette\Application\IRouter innerRouter'])
->setArguments([
$builder->literal('$innerRouter'),
'@Nette\Application\IPresenterFactory',
'@Nette\Http\Session',
$this->findSecuredRequests()
]);

$innerRouter = $builder->getByType(IRouter::class);
$builder->getDefinition($innerRouter)
->setAutowired(FALSE);

$builder->addDefinition($this->prefix('router'))
->setClass(IRouter::class)
->setFactory("@{$this->name}.routerFactory::create", ["@$innerRouter"])
->setAutowired(TRUE);

$builder->addDefinition($this->prefix('redirectChecker'))
->setClass(RedirectChecker::class);

$builder->getDefinition($builder->getByType(Nette\Application\Application::class))
->addSetup('?->onResponse[] = [?, ?]', ['@self', '@Nextras\SecuredLinks\RedirectChecker', 'checkResponse']);
}


/**
* @return array
*/
private function findSecuredRequests()
{
$securedRequests = [];

foreach ($this->findSecuredDestinations() as $presenterClass => $destinations) {
foreach ($destinations as $destination => $ignoredParams) {
if (Strings::endsWith($destination, '!')) {
$key = 'do';
$value = Strings::substring($destination, 0, -1);

} else {
$key = 'action';
$value = $destination;
}

$securedRequests[$presenterClass][$key][$value] = $ignoredParams;
}
}

return $securedRequests;
}


/**
* @return Generator
*/
private function findSecuredDestinations()
{
$config = $this->validateConfig($this->defaults);

foreach ($config['destinations'] as $presenterClass => $destinations) {
yield $presenterClass => $destinations;
}

if ($config['annotation']) {
$presenters = $this->getContainerBuilder()->findByType(Presenter::class);
foreach ($presenters as $presenterDef) {
$presenterClass = $presenterDef->getClass();
if (!isset($config['destinations'][$presenterClass])) {
$presenterRef = new \ReflectionClass($presenterClass);
yield $presenterClass => $this->findSecuredMethods($presenterRef);
}
}
}
}


/**
* @param ReflectionClass $classRef
* @return Generator
*/
private function findSecuredMethods(ReflectionClass $classRef)
{
foreach ($this->findTargetMethods($classRef) as $destination => $methodRef) {
if ($this->isSecured($methodRef, $ignoredParams)) {
yield $destination => $ignoredParams;
}
}
}


/**
* @param ReflectionClass $classRef
* @return Generator|ReflectionMethod[]
*/
private function findTargetMethods(ReflectionClass $classRef)
{
foreach ($classRef->getMethods() as $methodRef) {
$methodName = $methodRef->getName();

if (Strings::startsWith($methodName, 'action') && $classRef->isSubclassOf(Presenter::class)) {
$destination = Strings::firstLower(Strings::after($methodName, 'action'));
yield $destination => $methodRef;

} elseif (Strings::startsWith($methodName, 'handle')) {
$destination = Strings::firstLower(Strings::after($methodName, 'handle')) . '!';
yield $destination => $methodRef;

} elseif (Strings::startsWith($methodName, 'createComponent')) {
$returnType = $this->getMethodReturnType($methodRef);
if ($returnType !== NULL) {
$returnTypeRef = new ReflectionClass($returnType);
$componentName = Strings::firstLower(Strings::after($methodName, 'createComponent'));
foreach ($this->findTargetMethods($returnTypeRef) as $innerDestination => $innerRef) {
yield "$componentName-$innerDestination" => $innerRef;
}

} elseif ($this->config['strictMode']) {
$className = $methodRef->getDeclaringClass()->getName();
throw new \LogicException(
"Unable to deduce return type for method $className::$methodName(); " .
"add @return annotation, install nikic/php-parser or disable strictMode in config"
);
}
}
}
}


/**
* @param ReflectionMethod $ref
* @param array|bool $params
* @return bool
*/
private function isSecured(ReflectionMethod $ref, & $params)
{
$annotation = preg_quote(isset($this->config['annotation'])
? $this->config['annotation']
: $this->defaults['annotation'],
'#'
);

if (preg_match("#^[ \\t/*]*@$annotation(?:[ \\t]+(\\[.*?\\])?|$)#m", $ref->getDocComment(), $matches)) {
$params = !empty($matches[1]) ? Neon::decode($matches[1]) : TRUE;
return TRUE;

} else {
return FALSE;
}
}


/**
* @param ReflectionMethod $methodRef
* @return NULL|string
*/
private function getMethodReturnType(ReflectionMethod $methodRef)
{
$returnType = PhpReflection::getReturnType($methodRef);
if ($returnType !== NULL || !interface_exists(\PhpParser\Node::class)) {
return $returnType;
} else {
return ReturnTypeResolver::getReturnType($methodRef);
}
}
}
188 changes: 188 additions & 0 deletions src/Bridges/PhpParser/ReturnTypeResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<?php

/**
* This file is part of the Nextras Secured Links library.
*
* @license MIT
* @link https://github.com/nextras/secured-links
*/

namespace Nextras\SecuredLinks\Bridges\PhpParser;

use Nette;
use Nette\DI\PhpReflection;
use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\NodeVisitorAbstract;
use PhpParser\ParserFactory;
use ReflectionMethod;


class ReturnTypeResolver extends NodeVisitorAbstract
{
/** @var string */
private $className;

/** @var string */
private $methodName;

/** @var string[] */
private $returnTypes = [];

/** @var string[][] */
private $varTypes = [];

/** @var bool */
private $inClass = FALSE;

/** @var bool */
private $inMethod = FALSE;


/**
* @param string $className
* @param string $methodName
*/
public function __construct($className, $methodName)
{
$this->className = $className;
$this->methodName = $methodName;
$this->varTypes['this'][] = $className;
}


/**
* @param ReflectionMethod $methodRef
* @return NULL|string
*/
public static function getReturnType(ReflectionMethod $methodRef)
{
$fileContent = file_get_contents($methodRef->getDeclaringClass()->getFileName());

$traverser = new NodeTraverser();
$traverser->addVisitor(new NameResolver);
$traverser->addVisitor($resolver = new self($methodRef->getDeclaringClass()->getName(), $methodRef->getName()));
$traverser->traverse((new ParserFactory)->create(ParserFactory::PREFER_PHP7)->parse($fileContent));

return count($resolver->returnTypes) === 1 ? $resolver->returnTypes[0] : NULL;
}


/**
* @inheritdoc
*/
public function enterNode(Node $node)
{
if ($node instanceof Node\Stmt\Class_ && $node->name === $this->className) {
$this->inClass = TRUE;

} elseif ($this->inClass && $node instanceof Node\Stmt\ClassMethod && $node->name === $this->methodName) {
$this->inMethod = TRUE;

} elseif ($this->inMethod) {
if ($node instanceof Node\Stmt\Return_ && $node->expr !== NULL) {
foreach ($this->getExpressionTypes($node->expr) as $type) {
$this->addReturnType($type);
}

} elseif ($node instanceof Node\Expr\Assign) {
foreach ($this->getExpressionTypes($node->expr) as $type) {
$this->addVarType($node, $type);
}
}
}
}


/**
* @inheritdoc
*/
public function leaveNode(Node $node)
{
if ($this->inMethod && $node instanceof Node\Stmt\ClassMethod) {
$this->inMethod = FALSE;

} elseif ($this->inClass && $node instanceof Node\Stmt\Class_) {
$this->inClass = FALSE;
}
}


/**
* @param Node\Expr $expr
* @return string[]
*/
private function getExpressionTypes(Node\Expr $expr)
{
$result = [];

if ($expr instanceof Node\Expr\New_) {
if ($expr->class instanceof Node\Name) {
$result[] = (string) $expr->class;
}

} elseif ($expr instanceof Node\Expr\Variable) {
if (is_string($expr->name) && isset($this->varTypes[$expr->name])) {
$result = $this->varTypes[$expr->name];
}

} elseif ($expr instanceof Node\Expr\PropertyFetch) {
if (is_string($expr->name)) {
foreach ($this->getExpressionTypes($expr->var) as $objType) {
$propertyRef = new \ReflectionProperty($objType, $expr->name);
$type = PhpReflection::parseAnnotation($propertyRef, 'var');
$type = $type ? PhpReflection::expandClassName($type, PhpReflection::getDeclaringClass($propertyRef)) : NULL;
$result[] = $type;
}
}

} elseif ($expr instanceof Node\Expr\MethodCall) {
if (is_string($expr->name)) {
foreach ($this->getExpressionTypes($expr->var) as $objType) {
$methodRef = new \ReflectionMethod($objType, $expr->name);
$result[] = PhpReflection::getReturnType($methodRef);
}
}

} elseif ($expr instanceof Node\Expr\Assign) {
foreach ($this->getExpressionTypes($expr->expr) as $type) {
$this->addVarType($expr, $type);
$result[] = $type;
}

} elseif ($expr instanceof Node\Expr\Clone_) {
$result = $this->getExpressionTypes($expr->expr);
}

return $result;
}


/**
* @param string $exprType
* @return void
*/
private function addReturnType($exprType)
{
if ($exprType !== NULL && class_exists($exprType) && !in_array($exprType, $this->returnTypes)) {
$this->returnTypes[] = $exprType;
}
}


/**
* @param Node\Expr\Assign $node
* @param string $exprType
* @return void
*/
private function addVarType($node, $exprType)
{
if ($node->var instanceof Node\Expr\Variable && is_string($node->var->name)
&& (empty($this->varTypes[$node->var->name]) || !in_array($exprType, $this->varTypes[$node->var->name]))
&& $exprType !== NULL && class_exists($exprType)
) {
$this->varTypes[$node->var->name][] = $exprType;
}
}
}
33 changes: 33 additions & 0 deletions src/RedirectChecker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/**
* This file is part of the Nextras Secured Links library.
* @license MIT
* @link https://github.com/nextras/secured-links
*/

namespace Nextras\SecuredLinks;

use Nette;
use Nette\Application\Application;
use Nette\Application\IResponse;
use Nette\Application\Responses\RedirectResponse;


class RedirectChecker
{
/**
* @param Application $app
* @param IResponse $response
* @return void
*/
public function checkResponse(Application $app, IResponse $response)
{
$requests = $app->getRequests();
$request = $requests[count($requests) - 1];

if ($request->hasFlag(SecuredRouter::SIGNED) && !$response instanceof RedirectResponse) {
throw new \LogicException('Secured request did not redirect. Possible CSRF-token reveal by HTTP referer header.');
}
}
}
75 changes: 0 additions & 75 deletions src/SecuredLinksControlTrait.php

This file was deleted.

158 changes: 158 additions & 0 deletions src/SecuredRouter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<?php

/**
* This file is part of the Nextras Secured Links library.
* @license MIT
* @link https://github.com/nextras/secured-links
*/

namespace Nextras\SecuredLinks;

use Nette;
use Nette\Application\IPresenterFactory;
use Nette\Application\IRouter;
use Nette\Application\Request;
use Nette\Http\Session;


class SecuredRouter implements IRouter
{
/** signed flag, marks requests which has been signed */
const SIGNED = 'signed';

/** length of secret token stored in session */
const SECURITY_TOKEN_LENGTH = 16;

/** name of security key which is passed in URL */
const SECURITY_KEY = '_sec';

/** @var IRouter */
private $inner;

/** @var IPresenterFactory */
private $presenterFactory;

/** @var Session */
private $session;

/** @var array */
private $secured;


/**
* @param IRouter $inner
* @param IPresenterFactory $presenterFactory
* @param Session $session
* @param array $secured describes what should be secured
*/
public function __construct(IRouter $inner, IPresenterFactory $presenterFactory, Session $session, array $secured)
{
$this->inner = $inner;
$this->presenterFactory = $presenterFactory;
$this->session = $session;
$this->secured = $secured;
}


/**
* @inheritdoc
*/
public function match(Nette\Http\IRequest $httpRequest)
{
$appRequest = $this->inner->match($httpRequest);
if ($appRequest !== NULL && $this->isSignatureOk($appRequest)) {
$appRequest->setFlag(self::SIGNED);
}

return $appRequest;
}


/**
* @inheritdoc
*/
public function constructUrl(Request $appRequest, Nette\Http\Url $refUrl)
{
if ($this->isSignatureRequired($appRequest, $ignoredParams)) {
$params = $appRequest->getParameters();
$params[self::SECURITY_KEY] = $this->getSignature($appRequest, $ignoredParams);
$appRequest->setParameters($params);
}

return $this->inner->constructUrl($appRequest, $refUrl);
}


/**
* @param Request $appRequest
* @return bool
*/
private function isSignatureOk(Request $appRequest)
{
if ($this->isSignatureRequired($appRequest, $ignoredParams)) {
$actualSignature = $appRequest->getParameter(self::SECURITY_KEY);
$expectedSignature = $this->getSignature($appRequest, $ignoredParams);
return ($actualSignature !== NULL && hash_equals($expectedSignature, $actualSignature));

} else {
return TRUE;
}
}


/**
* @param Request $appRequest
* @param array|bool $ignoredParams
* @return bool
*/
protected function isSignatureRequired(Request $appRequest, & $ignoredParams)
{
$presenterName = $appRequest->getPresenterName();
$presenterClass = $this->presenterFactory->getPresenterClass($presenterName);

if (!isset($this->secured[$presenterClass])) {
return FALSE;
}

$params = $appRequest->getParameters();
foreach ($this->secured[$presenterClass] as $key => $foobar) {
if (isset($params[$key], $this->secured[$presenterClass][$key][$params[$key]])) {
$ignoredParams = $this->secured[$presenterClass][$key][$params[$key]];
return TRUE;
}
}

return FALSE;
}


/**
* @param Request $appRequest
* @param array|bool $ignoredParams
* @return string
*/
private function getSignature(Request $appRequest, $ignoredParams)
{
$sessionSection = $this->session->getSection('Nextras.SecuredLinks');
if (!isset($sessionSection->token) || strlen($sessionSection->token) !== self::SECURITY_TOKEN_LENGTH) {
$sessionSection->token = function_exists('random_bytes')
? random_bytes(self::SECURITY_TOKEN_LENGTH)
: Nette\Utils\Random::generate(self::SECURITY_TOKEN_LENGTH, "\x00-\xFF");
}

if ($ignoredParams === TRUE) {
$params = $appRequest->getParameters();
} elseif ($ignoredParams === FALSE) {
$params = [];
} else {
$params = $appRequest->getParameters();
foreach ($ignoredParams as $key) {
unset($params[$key]);
}
}

$data = [$this->session->getId(), $appRequest->getPresenterName(), $params];
$hash = hash_hmac('sha1', json_encode($data), $sessionSection->token);
return substr($hash, 0, 6);
}
}
22 changes: 22 additions & 0 deletions src/SecuredRouterFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

/**
* This file is part of the Nextras Secured Links library.
* @license MIT
* @link https://github.com/nextras/secured-links
*/

namespace Nextras\SecuredLinks;

use Nette;
use Nette\Application\IRouter;


interface SecuredRouterFactory
{
/**
* @param IRouter $innerRouter
* @return SecuredRouter
*/
public function create(IRouter $innerRouter);
}
42 changes: 42 additions & 0 deletions src/deprecated/SecuredLinksControlTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

/**
* This file is part of the Nextras Secured Links library.
* @license MIT
* @link https://github.com/nextras/secured-links
*/

namespace Nextras\Application\UI;

use Nette;
use Nette\Application\UI\Presenter;
use Nextras\SecuredLinks\SecuredRouter;


/**
* @mixin Presenter
* @deprecated
*/
trait SecuredLinksControlTrait
{
/**
* @deprecated
*/
public function signalReceived($signal)
{
$methodName = $this->formatSignalMethod($signal);
if (method_exists($this, $methodName)) {
$methodRef = new Nette\Reflection\Method($this, $methodName);
if ($methodRef->hasAnnotation('secured') && !$this->request->hasFlag(SecuredRouter::SIGNED)) {
$who = $this instanceof Presenter ? 'Presenter' : 'Control';
throw new \LogicException(
"$who received request to secured signal which was not properly signed." .
"This indicate a bug in your installation of Nextras Secured Links." .
"Please consult documentation on how to properly migrate to Nextras Secured Links 2.0"
);
}
}

parent::signalReceived($signal);
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
<?php

/**
* This file is part of the Nextras community extensions of Nette Framework
*
* This file is part of the Nextras Secured Links library.
* @license MIT
* @link https://github.com/nextras
* @author Jan Skrasek
* @link https://github.com/nextras/secured-links
*/

namespace Nextras\Application\UI;
@@ -14,17 +12,16 @@
use Nette\Application\UI\PresenterComponent;


/**
* @deprecated
*/
trait SecuredLinksPresenterTrait
{
use SecuredLinksControlTrait;


/**
* @param PresenterComponent $component
* @param string $link created URL
* @param string $destination
* @return string
* @throws Nette\Application\UI\InvalidLinkException
* @deprecated
*/
public function createSecuredLink(PresenterComponent $component, $link, $destination)
{
@@ -106,11 +103,7 @@ public function createSecuredLink(PresenterComponent $component, $link, $destina


/**
* Returns unique token for method and params
* @param string $control
* @param string $method
* @param array $params
* @return string
* @deprecated
*/
public function getCsrfToken($control, $method, $params)
{
@@ -123,5 +116,4 @@ public function getCsrfToken($control, $method, $params)
$params = implode('|', array_keys($params)) . '|' . implode('|', array_values($params));
return substr(md5($control . $method . $params . $session->token . $this->getSession()->getId()), 0, 8);
}

}
104 changes: 104 additions & 0 deletions tests/cases/SecuredLinksExtensionTest.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

/**
* This file is part of the Nextras Secured Links library.
* @license MIT
* @link https://github.com/nextras/secured-links
*/

namespace NextrasTests\SecuredLinks;

use Mockery;
use Nette;
use Nette\Application\IRouter;
use Nette\Application\LinkGenerator;
use Nette\Application\Routers\Route;
use Nette\Bridges\ApplicationDI\ApplicationExtension;
use Nette\Bridges\HttpDI\HttpExtension;
use Nette\Bridges\HttpDI\SessionExtension;
use Nextras\SecuredLinks\Bridges\NetteDI\SecuredLinksExtension;
use Nextras\SecuredLinks\SecuredRouter;
use Tester;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';
require __DIR__ . '/../fixtures/TestPresenter.php';


class SecuredLinksExtensionTest extends Tester\TestCase
{
public function testFoo()
{
$dic = $this->createContainer();
$router = $dic->getByType(IRouter::class);
Assert::type(SecuredRouter::class, $router);

$linkGenerator = $dic->getByType(LinkGenerator::class);

Assert::same(
'http://example.com/test?action=delete&_sec=e646b0',
$linkGenerator->link('Test:delete')
);

Assert::same(
'http://example.com/test?do=pay&action=default&_sec=eed6d6',
$linkGenerator->link('Test:default', ['do' => 'pay'])
);

Assert::same(
'http://example.com/test?do=pay&amount=1&action=default&_sec=7eda1c',
$linkGenerator->link('Test:default', ['do' => 'pay', 'amount' => 1])
);

Assert::same(
'http://example.com/test?do=pay&amount=2&action=default&_sec=f9cc2b',
$linkGenerator->link('Test:default', ['do' => 'pay', 'amount' => 2])
);

Assert::same(
'http://example.com/test?do=pay2&amount=1&action=default&_sec=51a97a',
$linkGenerator->link('Test:default', ['do' => 'pay2', 'amount' => 1])
);

Assert::same(
'http://example.com/test?do=pay2&amount=2&action=default&_sec=51a97a', // intentionally the same hash
$linkGenerator->link('Test:default', ['do' => 'pay2', 'amount' => 2])
);
}


/**
* @return \Nette\DI\Container
*/
private function createContainer()
{
$compiler = new Nette\DI\Compiler;
$compiler->addExtension('nette.http', new HttpExtension);
$compiler->addExtension('nette.http.sessions', new SessionExtension);
$compiler->addExtension('nette.application', new ApplicationExtension(TRUE, [__DIR__ . '/../fixtures']));
$compiler->addExtension('nextras.securedLinks', new SecuredLinksExtension);

eval($compiler->compile(
[
'services' => [new Nette\DI\Statement(Route::class, ['//example.com/<presenter>'])]
],
'SecuredLinksExtensionContainer'
));

$sessionSection = Mockery::mock('alias:Nette\Http\SessionSection');
$sessionSection->token = 'abcdabcdabcdabcd';

$session = Mockery::mock('Nette\Http\Session');
$session->shouldReceive('getSection')->with('Nextras.SecuredLinks')->andReturn($sessionSection);
$session->shouldReceive('getId')->times(8)->andReturn('session_id_1');

/** @var Nette\DI\Container $dic */
$dic = new \SecuredLinksExtensionContainer();
$dic->removeService('nette.http.sessions.session');
$dic->addService('nette.http.sessions.session', $session);

return $dic;
}
}

(new SecuredLinksExtensionTest)->run();
100 changes: 100 additions & 0 deletions tests/fixtures/TestPresenter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

use Nette\Application\UI\Control;
use Nette\Application\UI\Presenter;


class TestControl extends Control
{
/** @secured */
public function handlePay($amount = 0)
{
}
}

interface TestControlFactory
{

/**
* @return TestControl
*/
public function create();
}


class TestPresenter extends Presenter
{
/** @var TestControl */
public $testControl;

/** @var TestControlFactory */
public $testControlFactory;


public function renderDefault()
{
$this->terminate();
}


/** @secured */
public function handlePay($amount)
{
}


/** @secured [amount] */
public function handlePay2($amount)
{
}


/** @secured */
public function handleList(array $sections)
{
}


/**
* @secured
*/
public function actionDelete()
{

}


/**
* @return TestControl
*/
protected function createComponentMyControlA()
{

}


protected function createComponentMyControlB()
{
return new TestControl();
}


protected function createComponentMyControlC()
{
$tmp = new TestControl();
$control = $tmp;
return $control;
}


protected function createComponentMyControlD()
{
return clone $this->testControl;
}


protected function createComponentMyControlE()
{
return $this->testControlFactory->create();
}
}