Skip to content

Commit

Permalink
[BUGFIX] Generate notification action on rendering
Browse files Browse the repository at this point in the history
Due to scoping issues if a notification is generated in an iframe, the
actions are now generated when the notification is rendered.

This requires a change how the payload is passed to the notification,
instead of the action instance it now contains the action type and the
callback. The action instance is generated by a factory class.

To bypass issues with browser's garbage collection that kicks in when an
iframe changes, the callback methods are stringified and immediately
recovered by eval().

Resolves: #89173
Releases: master
Change-Id: I45624a26bad1527f9d6222ad10e1f9a1384ee07e
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/61706
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Tobi Kretschmann <tobi@tobishome.de>
Tested-by: Frank Nägler <frank.naegler@typo3.org>
Reviewed-by: Tobi Kretschmann <tobi@tobishome.de>
Reviewed-by: Steffen Frese <steffenf14@gmail.com>
Reviewed-by: Frank Nägler <frank.naegler@typo3.org>
  • Loading branch information
andreaskienast authored and NeoBlack committed Sep 17, 2019
1 parent c39bdf4 commit 78955a6
Show file tree
Hide file tree
Showing 11 changed files with 161 additions and 86 deletions.
@@ -0,0 +1,19 @@
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/

enum ActionEnum {
DEFERRED = 'deferred',
IMMEDIATE = 'immediate',
}

export = ActionEnum;
@@ -0,0 +1,38 @@
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/

import {AbstractAction} from './AbstractAction';
import ActionEnum = require('./ActionEnum');
import DeferredAction = require('./DeferredAction');
import ImmediateAction = require('./ImmediateAction');

class ActionFactory {
public static createAction(options: any): AbstractAction {
if (options.type === ActionEnum.DEFERRED) {
return new DeferredAction(ActionFactory.regenerateCallback(options.callback));
}

if (options.type === ActionEnum.IMMEDIATE) {
return new ImmediateAction(ActionFactory.regenerateCallback(options.callback));
}

throw new Error('Unknown action type ' + options.type + ' passed');
}

private static regenerateCallback(callback: Function): any {
// tslint:disable-next-line:no-eval
return eval(callback.toString());
}
}

export = ActionFactory;
Expand Up @@ -12,13 +12,18 @@
*/

import * as $ from 'jquery';
import {AbstractAction} from './ActionButton/AbstractAction';
import {SeverityEnum} from './Enum/Severity';
import ActionFactory = require('./ActionButton/ActionFactory');
import Severity = require('./Severity');

interface Action {
label: string;
action: AbstractAction;
action: ActionType;
}

interface ActionType {
type: string;
action: Function;
}

/**
Expand Down Expand Up @@ -172,11 +177,13 @@ class Notification {
// Remove potentially set timeout
$box.clearQueue();

$actionButton.siblings().addClass('disabled');

const target = <HTMLAnchorElement>e.currentTarget;
target.classList.add('executing');

$actionButtonContainer.find('a').not(target).addClass('disabled');
action.action.execute(target).then((): void => {
const actionInstance = ActionFactory.createAction(action.action);
actionInstance.execute(target).then((): void => {
$box.alert('close');
});
});
Expand Down
41 changes: 8 additions & 33 deletions Build/Sources/TypeScript/backend/Tests/NotificationTest.ts
Expand Up @@ -12,8 +12,6 @@
*/

import * as $ from 'jquery';
import DeferredAction = require('TYPO3/CMS/Backend/ActionButton/DeferredAction');
import ImmediateAction = require('TYPO3/CMS/Backend/ActionButton/ImmediateAction');
import Notification = require('TYPO3/CMS/Backend/Notification');

describe('TYPO3/CMS/Backend/Notification:', () => {
Expand Down Expand Up @@ -97,15 +95,18 @@ describe('TYPO3/CMS/Backend/Notification:', () => {
[
{
label: 'My action',
action: new ImmediateAction((promise: Promise<any>): Promise<any> => {
return promise;
}),
action: {
type: 'immediate',
callback: (promise: Promise<any>): Promise<any> => {
return promise;
},
},
},
{
label: 'My other action',
action: new DeferredAction((promise: Promise<any>): Promise<any> => {
action: (promise: Promise<any>): Promise<any> => {
return promise;
}),
},
},
],
);
Expand All @@ -116,30 +117,4 @@ describe('TYPO3/CMS/Backend/Notification:', () => {
expect(alertBox.querySelectorAll('.alert-actions a')[0].textContent).toEqual('My action');
expect(alertBox.querySelectorAll('.alert-actions a')[1].textContent).toEqual('My other action');
});

it('immediate action is called', () => {
const observer = {
callback: (): void => {
return;
},
};

spyOn(observer, 'callback').and.callThrough();

Notification.info(
'Info message',
'Some text',
1,
[
{
label: 'My immediate action',
action: new ImmediateAction(observer.callback),
},
],
);

const alertBox = document.querySelector('div.alert');
(<HTMLAnchorElement>alertBox.querySelector('.alert-actions a')).click();
expect(observer.callback).toHaveBeenCalled();
});
});
4 changes: 2 additions & 2 deletions typo3/sysext/backend/Classes/Notification/Action.php
Expand Up @@ -19,8 +19,8 @@

final class Action
{
public const TYPE_IMMEDIATE = 'ImmediateAction';
public const TYPE_DEFERRED = 'DeferredAction';
public const TYPE_IMMEDIATE = 'immediate';
public const TYPE_DEFERRED = 'deferred';

/**
* @var string
Expand Down
21 changes: 9 additions & 12 deletions typo3/sysext/backend/Classes/Notification/NotificationService.php
Expand Up @@ -97,22 +97,19 @@ public function error(string $title, string $message, int $duration = 0, array $
*/
private function createNotification(string $type, string $title, string $message, int $duration, array $actions = []): void
{
$actionFunctionTemplate = 'const %s = new %s(function() { %s });';
$actionDefinitionTemplate = '{label: %s, action: %s}';
$actionItemFunctions = [];
$actionDefinitionTemplate = '{label: %s, action: {type: %s, callback: () => {%s}}}';
$actionItemDefinitions = [];
$i = 1;
foreach ($actions as $action) {
$actionName = 'action' . $i++;
$actionItemFunctions[] = sprintf($actionFunctionTemplate, $actionName, $action->getType(), $action->getCallbackCode());
$actionItemDefinitions[] = sprintf($actionDefinitionTemplate, GeneralUtility::quoteJSvalue($action->getLabel()), $actionName);
$actionItemDefinitions[] = sprintf(
$actionDefinitionTemplate,
GeneralUtility::quoteJSvalue($action->getLabel()),
GeneralUtility::quoteJSvalue($action->getType()),
$action->getCallbackCode()
);
}
GeneralUtility::makeInstance(PageRenderer::class)
->loadRequireJsModule('TYPO3/CMS/Backend/Notification', sprintf('function(Notification) {
require([\'TYPO3/CMS/Backend/ActionButton/DeferredAction\', \'TYPO3/CMS/Backend/ActionButton/ImmediateAction\'], function(DeferredAction, ImmediateAction) {
%s
Notification.%s(%s, %s, %d, [%s]);
});
}', implode(LF, $actionItemFunctions), $type, GeneralUtility::quoteJSvalue($title), GeneralUtility::quoteJSvalue($message), $duration, implode(',', $actionItemDefinitions)));
Notification.%s(%s, %s, %d, [%s]);
}', $type, GeneralUtility::quoteJSvalue($title), GeneralUtility::quoteJSvalue($message), $duration, implode(',', $actionItemDefinitions)));
}
}
@@ -0,0 +1,13 @@
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
define(["require","exports"],function(e,r){"use strict";var i;return function(e){e.DEFERRED="deferred",e.IMMEDIATE="immediate"}(i||(i={})),i});
@@ -0,0 +1,13 @@
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
define(["require","exports","./ActionEnum","./DeferredAction","./ImmediateAction"],function(require,exports,ActionEnum,DeferredAction,ImmediateAction){"use strict";class ActionFactory{static createAction(e){if(e.type===ActionEnum.DEFERRED)return new DeferredAction(ActionFactory.regenerateCallback(e.callback));if(e.type===ActionEnum.IMMEDIATE)return new ImmediateAction(ActionFactory.regenerateCallback(e.callback));throw new Error("Unknown action type "+e.type+" passed")}static regenerateCallback(callback){return eval(callback.toString())}}return ActionFactory});

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion typo3/sysext/backend/Tests/JavaScript/NotificationTest.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 78955a6

Please sign in to comment.