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

Integrate handling for AJAX actions #5

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
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
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
composer.lock
public/
vendor/
/composer.lock
/public/
/vendor/
118 changes: 87 additions & 31 deletions Classes/Backend/ConfirmationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
Expand All @@ -43,6 +44,8 @@ class ConfirmationController implements LoggerAwareInterface
use LoggerAwareTrait;
use LoggerAccessorTrait;

private const LANGUAGE_PREFIX = 'LLL:EXT:sudo_mode/Resources/Private/Language/locallang.xlf';

protected const FLAG_INVALID_PASSWORD = 1;

/**
Expand All @@ -60,10 +63,21 @@ class ConfirmationController implements LoggerAwareInterface
*/
protected $uriBuilder;

public function __construct(ConfirmationHandler $handler = null, BackendUserAuthentication $user = null, UriBuilder $uriBuilder = null)
/**
* @var LanguageService
*/
protected $languageService;

public function __construct(
ConfirmationHandler $handler = null,
BackendUserAuthentication $user = null,
UriBuilder $uriBuilder = null,
LanguageService $languageService = null
)
{
$this->handler = $handler ?? GeneralUtility::makeInstance(ConfirmationHandler::class);
$this->uriBuilder = $uriBuilder ?? GeneralUtility::makeInstance(UriBuilder::class);
$this->languageService = $languageService ?? GeneralUtility::makeInstance(LanguageService::class);
$this->user = $user ?? $GLOBALS['BE_USER'];
}

Expand All @@ -90,23 +104,44 @@ public function mainAction(ServerRequestInterface $request): ResponseInterface

protected function requestAction(ServerRequestInterface $request, ConfirmationBundle $bundle): ResponseInterface
{
$isJsonRequest = $this->isJsonRequest($request);
$flags = (int)($request->getQueryParams()['flags'] ?? 0);

if (!$this->isJsonRequest($request)) {
$view = $this->createView('Request');
$view->assignMultiple([
'bundle' => $bundle,
'verifyUri' => (string)$this->buildActionUriFromBundle('verify', $bundle),
'flagInvalidPassword' => $flags & self::FLAG_INVALID_PASSWORD,
]);
if (!empty($bundle->getRequestMetaData()->getReturnUrl())) {
$view->assign('cancelUri', (string)$this->buildActionUriFromBundle('cancel', $bundle));
}
$verifyUri = (string)$this->buildActionUriFromBundle('verify', $bundle);
$cancelUri = (string)$this->buildActionUriFromBundle('cancel', $bundle);

$view = $this->createView('Request');
$view->assignMultiple([
'bundle' => $bundle,
'verifyUri' => $isJsonRequest ? '#' : $verifyUri,
'flagInvalidPassword' => $flags & self::FLAG_INVALID_PASSWORD,
'layout' => $isJsonRequest ? 'None' : 'Module',
'isJsonRequest' => $isJsonRequest,
]);
// @todo Cannot cancel without knowing where to redirect to afterwards
if (!empty($bundle->getRequestMetaData()->getReturnUrl())) {
$view->assign('cancelUri', $cancelUri);
}

if (!$isJsonRequest) {
$this->applyAdditionalJavaScriptModules();
return new HtmlResponse($view->render());
}
// @todo Add JSON handling
return new JsonResponse([], 500);
return new JsonResponse([
'formId' => 'confirm-sudo',
'invalidId' => 'invalid-sudo',
'severity' => 1, // warning in Modal/Severity,
'title' => $this->resolveLabel('sudoPasswordConfirm'),
'content' => $this->reduceSpaces($view->render()),
'uri' => [
'verify' => $verifyUri,
'cancel' => $cancelUri,
],
'button' => [
'cancel' => $this->resolveLabel('cancel'),
'confirm' => $this->resolveLabel('confirm'),
],
], 200);
}

/**
Expand All @@ -120,62 +155,71 @@ protected function applyAdditionalJavaScriptModules(): void
protected function verifyAction(ServerRequestInterface $request, ConfirmationBundle $bundle): ResponseInterface
{
$parsedBody = $request->getParsedBody();
$isJsonRequest = $this->isJsonRequest($request);
$confirmationPassword = empty($parsedBody['confirmationPasswordInternal'])
? (string)($parsedBody['confirmationPassword'] ?? '') // default field
: $parsedBody['confirmationPasswordInternal']; // filled e.g. by `ext:rsaauth`
$loggerContext = $this->createLoggerContext($bundle, $this->user);

if (!$this->isJsonRequest($request)) {
if ($this->isValidPassword($confirmationPassword)) {
$this->handler->grantSubjects($bundle, $this->user);
$this->logger->info('Password verification succeeded', $loggerContext);
if ($this->isValidPassword($confirmationPassword)) {
$this->handler->grantSubjects($bundle, $this->user);
$this->logger->info('Password verification succeeded', $loggerContext);
if (!$isJsonRequest) {
throw new ServerRequestInstructionException($bundle->getRequestInstruction());
} else {
return new JsonResponse(['status' => true], 200);
}
}

$this->logger->warning('Password verification failed', $loggerContext);
$uri = $this->buildActionUriFromBundle('request', $bundle, self::FLAG_INVALID_PASSWORD);
$this->logger->warning('Password verification failed', $loggerContext);
$uri = $this->buildActionUriFromBundle('request', $bundle, self::FLAG_INVALID_PASSWORD);

if (!$isJsonRequest) {
return new RedirectResponse($uri, 401);
}
// @todo Add JSON handling
return new JsonResponse([], 500);
return new JsonResponse(['status' => false], 401);
}

protected function cancelAction(ServerRequestInterface $request, ConfirmationBundle $bundle): ResponseInterface
{
$loggerContext = $this->createLoggerContext($bundle, $this->user);

$this->handler->removeConfirmationBundle($bundle, $this->user);
$this->logger->notice('Password verification cancelled', $loggerContext);

if (!$this->isJsonRequest($request)) {
$this->handler->removeConfirmationBundle($bundle, $this->user);
$this->logger->notice('Password verification cancelled', $loggerContext);
return new RedirectResponse($bundle->getRequestMetaData()->getReturnUrl(), 401);
}
// @todo Add JSON handling
return new JsonResponse([], 500);
return new JsonResponse(['status' => true], 200);
}

protected function errorAction(ServerRequestInterface $request): ResponseInterface
{
$view = $this->createView('Error');
$view->assign('returnUrl', $request->getQueryParams()['returnUrl'] ?? '');
$view->assignMultiple([
'returnUrl' => $request->getQueryParams()['returnUrl'] ?? '',
]);
return new HtmlResponse($view->render());
}

public function buildActionUriFromBundle(string $actionName, ConfirmationBundle $bundle, int $flags = null): UriInterface
{
return $this->buildActionUri(
$actionName,
$bundle->getRequestMetaData()->getReturnUrl(),
$bundle->getIdentifier(),
$bundle->getRequestMetaData()->getReturnUrl(),
$bundle->getRequestMetaData()->getScope(),
$flags
);
}

protected function buildActionUri(string $actionName, string $returnUrl, string $bundleIdentifier, int $flags = null): UriInterface
protected function buildActionUri(string $actionName, string $bundleIdentifier, string $returnUrl = null, string $scope = null, int $flags = null): UriInterface
{
$parameters = [
'action' => $actionName,
'returnUrl' => $returnUrl,
'bundle' => $bundleIdentifier,
'returnUrl' => $returnUrl,
'scope' => $scope,
'flags' => $flags,
];
$parameters['hmac'] = $this->signParameters($parameters);
Expand All @@ -195,7 +239,7 @@ protected function resolveUriParameters(ServerRequestInterface $request): array

protected function filterParameters(array $parameters): array
{
return array_intersect_key($parameters, array_flip(['action', 'returnUrl', 'bundle', 'flags', 'hmac']));
return array_intersect_key($parameters, array_flip(['action', 'bundle', 'returnUrl', 'scope', 'flags', 'hmac']));
}

protected function signParameters(array $parameters): string
Expand Down Expand Up @@ -280,6 +324,18 @@ protected function getAuthServices(string $subType, array $loginData, array $aut

protected function isJsonRequest(ServerRequestInterface $request): bool
{
return strpos($request->getHeaderLine('content-type'), 'application/json') === 0;
return strpos($request->getHeaderLine('content-type'), 'application/json') === 0
|| ($request->getQueryParams()['scope'] ?? null) === 'json';
}

protected function reduceSpaces(string $value): string
{
$value = preg_replace('#\s{2,}#', ' ', $value);
return trim($value);
}

private function resolveLabel(string $identifier): string
{
return $this->languageService->sL(self::LANGUAGE_PREFIX . ':' . $identifier);
}
}
9 changes: 9 additions & 0 deletions Classes/Backend/ConfirmationFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,18 @@ public function createRequestFromArray(array $data): ConfirmationRequest
public function createRequestMetaDataFromArray(array $data): RequestMetaData
{
$target = GeneralUtility::makeInstance(RequestMetaData::class);
if (isset($data['scope'])) {
$target = $target->withScope($data['scope']);
}
if (isset($data['returnUrl'])) {
$target = $target->withReturnUrl($data['returnUrl']);
}
if (isset($data['eventName'])) {
$target = $target->withEventName($data['eventName']);
}
if (isset($data['jsonData'])) {
$target = $target->withJsonData($data['jsonData']);
}
return $target;
}
}
9 changes: 7 additions & 2 deletions Classes/Backend/ConfirmationHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ class ConfirmationHandler implements LoggerAwareInterface
*/
protected $currentTimestamp;

/**
* @var bool
*/
private $immediatePurge = false;

public function __construct(ConfirmationFactory $factory = null, Behavior $behavior = null)
{
$this->factory = $factory ?? GeneralUtility::makeInstance(ConfirmationFactory::class);
Expand Down Expand Up @@ -159,7 +164,7 @@ protected function purgeSubjects(AbstractUserAuthentication $user, array $sessio
continue;
}
foreach ($sessionData[$type] as $key => $item) {
if (is_int($item) && $item >= $this->currentTimestamp) {
if (!$this->immediatePurge && is_int($item) && $item >= $this->currentTimestamp) {
continue;
}
unset($sessionData[$type][$key]);
Expand All @@ -174,7 +179,7 @@ protected function purgeBundles(AbstractUserAuthentication $user, array $session
$purged = false;
foreach ($sessionData as $key => $item) {
$expirationTimestamp = $item['expiration'] ?? 0;
if ($expirationTimestamp >= $this->currentTimestamp) {
if (!$this->immediatePurge && $expirationTimestamp >= $this->currentTimestamp) {
continue;
}
$data = json_decode($item['bundle'] ?? '', true);
Expand Down
60 changes: 60 additions & 0 deletions Classes/Backend/RequestMetaData.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,88 @@

class RequestMetaData implements \JsonSerializable
{
/**
* @var string|null
*/
protected $scope;

/**
* @var string|null
*/
protected $returnUrl;

/**
* @var string|null
*/
protected $eventName;

/**
* @var array|null
*/
protected $jsonData;

public function jsonSerialize(): array
{
return get_object_vars($this);
}

public function withScope(string $scope): self
{
$target = clone $this;
$target->scope = $scope;
return $target;
}

public function withReturnUrl(string $returnUrl): self
{
$target = clone $this;
$target->returnUrl = $returnUrl;
return $target;
}

public function withEventName(string $eventName): self
{
$target = clone $this;
$target->eventName = $eventName;
return $target;
}

public function withJsonData(array $jsonData): self
{
$target = clone $this;
$target->jsonData = $jsonData;
return $target;
}

/**
* @return string|null
*/
public function getScope(): ?string
{
return $this->scope;
}

/**
* @return string|null
*/
public function getReturnUrl(): ?string
{
return $this->returnUrl;
}

/**
* @return string|null
*/
public function getEventName(): ?string
{
return $this->eventName;
}

/**
* @return array|null
*/
public function getJsonData(): ?array
{
return $this->jsonData;
}
}
8 changes: 5 additions & 3 deletions Classes/Backend/RouteManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
* The TYPO3 project - inspiring people to share!
*/

use FriendsOfTYPO3\SudoMode\Scope\CoreRouteHandler;
use FriendsOfTYPO3\SudoMode\Scope\CoreHtmlRouteHandler;
use FriendsOfTYPO3\SudoMode\Scope\CoreJsonRouteHandler;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Routing\Route;
use TYPO3\CMS\Core\Utility\GeneralUtility;
Expand All @@ -24,10 +25,11 @@ class RouteManager
{
/**
* @var RouteHandlerInterface[]
// @todo Add possibility to register 3rd party handlers
* @todo Add possibility to register 3rd party handlers
*/
protected $handlerClassNames = [
CoreRouteHandler::class,
CoreHtmlRouteHandler::class,
CoreJsonRouteHandler::class,
];

/**
Expand Down
Loading