[TASK] Migrate EXT:backend LoginRefresh to TypeScript
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Resources / Private / TypeScript / LoginRefresh.ts
1 /*
2  * This file is part of the TYPO3 CMS project.
3  *
4  * It is free software; you can redistribute it and/or modify it under
5  * the terms of the GNU General Public License, either version 2
6  * of the License, or any later version.
7  *
8  * For the full copyright and license information, please read the
9  * LICENSE.txt file that was distributed with this source code.
10  *
11  * The TYPO3 project - inspiring people to share!
12  */
13
14 import * as $ from 'jquery';
15 import Typo3Notification = require('TYPO3/CMS/Backend/Notification');
16
17 enum MarkupIdentifiers {
18   loginrefresh = 't3js-modal-loginrefresh',
19   lockedModal = 't3js-modal-backendlocked',
20   loginFormModal = 't3js-modal-backendloginform'
21 }
22
23 // hack is required, because the Notification definition is wrong
24 declare var Notification: any;
25
26 /**
27  * Module: TYPO3/CMS/Backend/LoginRefresh
28  * @exports TYPO3/CMS/Backend/LoginRefresh
29  */
30 class LoginRefresh {
31   private options: any = {
32     modalConfig: {
33       backdrop: 'static'
34     }
35   };
36   private webNotification: Notification = null;
37   private intervalTime: number = 60;
38   private intervalId: number = null;
39   private backendIsLocked: boolean = false;
40   private isTimingOut: boolean = false;
41   private $timeoutModal: JQuery = null;
42   private $backendLockedModal: JQuery = null;
43   private $loginForm: JQuery = null;
44   private loginFramesetUrl: string = '';
45   private logoutUrl: string = '';
46
47   /**
48    * Initialize login refresh
49    */
50   public initialize(): void {
51     this.initializeTimeoutModal();
52     this.initializeBackendLockedModal();
53     this.initializeLoginForm();
54
55     this.startTask();
56
57     if (document.location.protocol === 'https:' && typeof Notification !== 'undefined' && Notification.permission !== 'granted') {
58       Notification.requestPermission();
59     }
60   }
61
62   /**
63    * Start the task
64    */
65   public startTask(): void {
66     if (this.intervalId !== null) {
67       return;
68     }
69     // set interval to 60 seconds
70     let interval: Number = this.intervalTime * 1000;
71     this.intervalId = setInterval(this.checkActiveSession, interval);
72   }
73
74   /**
75    * Stop the task
76    */
77   public stopTask(): void {
78     clearInterval(this.intervalId);
79     this.intervalId = null;
80   }
81
82   /**
83    * Set interval time
84    *
85    * @param {number} intervalTime
86    */
87   public setIntervalTime(intervalTime: number): void {
88     // To avoid the integer overflow in setInterval, we limit the interval time to be one request per day
89     this.intervalTime = Math.min(intervalTime, 86400);
90   }
91
92   /**
93    * Set the logout URL
94    *
95    * @param {string} logoutUrl
96    */
97   public setLogoutUrl(logoutUrl: string): void {
98     this.logoutUrl = logoutUrl;
99   }
100
101   /**
102    * Set login frameset url
103    */
104   public setLoginFramesetUrl(loginFramesetUrl: string): void {
105     this.loginFramesetUrl = loginFramesetUrl;
106   }
107
108   /**
109    * Shows the timeout dialog. If the backend is not focused, a Web Notification
110    * is displayed, too.
111    */
112   public showTimeoutModal(): void {
113     this.isTimingOut = true;
114     this.$timeoutModal.modal(this.options.modalConfig);
115     this.fillProgressbar(this.$timeoutModal);
116
117     if (document.location.protocol === 'https:' && typeof Notification !== 'undefined'
118       && Notification.permission === 'granted' && document.hidden) {
119       this.webNotification = new Notification(TYPO3.lang['mess.login_about_to_expire_title'], {
120         body: TYPO3.lang['mess.login_about_to_expire'],
121         icon: '/typo3/sysext/backend/Resources/Public/Images/Logo.png'
122       });
123       this.webNotification.onclick = () => {
124         window.focus();
125       };
126     }
127   }
128
129   /**
130    * Hides the timeout dialog. If a Web Notification is displayed, close it too.
131    */
132   public hideTimeoutModal(): void {
133     this.isTimingOut = false;
134     this.$timeoutModal.modal('hide');
135
136     if (typeof Notification !== 'undefined' && this.webNotification !== null) {
137       this.webNotification.close();
138     }
139   }
140
141   /**
142    * Shows the "backend locked" dialog.
143    */
144   public showBackendLockedModal(): void {
145     this.$backendLockedModal.modal(this.options.modalConfig);
146   }
147
148   /**
149    * Hides the "backend locked" dialog.
150    */
151   public hideBackendLockedModal(): void {
152     this.$backendLockedModal.modal('hide');
153   }
154
155   /**
156    * Shows the login form.
157    */
158   public showLoginForm(): void {
159     // log off for sure
160     $.ajax({
161       url: TYPO3.settings.ajaxUrls.logout,
162       method: 'GET',
163       success: () => {
164         TYPO3.configuration.showRefreshLoginPopup
165           ? this.showLoginPopup()
166           : this.$loginForm.modal(this.options.modalConfig);
167       }
168     });
169   }
170
171   /**
172    * Opens the login form in a new window.
173    */
174   public showLoginPopup(): void {
175     const vHWin = window.open(
176       this.loginFramesetUrl,
177       'relogin_' + Math.random().toString(16).slice(2),
178       'height=450,width=700,status=0,menubar=0,location=1'
179     );
180     if (vHWin) {
181       vHWin.focus();
182     }
183   }
184
185   /**
186    * Hides the login form.
187    */
188   public hideLoginForm(): void {
189     this.$loginForm.modal('hide');
190   }
191
192   /**
193    * Generates the modal displayed if the backend is locked.
194    */
195   protected initializeBackendLockedModal(): void {
196     this.$backendLockedModal = this.generateModal(MarkupIdentifiers.lockedModal);
197     this.$backendLockedModal.find('.modal-header h4').text(TYPO3.lang['mess.please_wait']);
198     this.$backendLockedModal.find('.modal-body').append(
199       $('<p />').text(TYPO3.lang['mess.be_locked'])
200     );
201     this.$backendLockedModal.find('.modal-footer').remove();
202
203     $('body').append(this.$backendLockedModal);
204   }
205
206   /**
207    * Generates the modal displayed on near session time outs
208    */
209   protected initializeTimeoutModal(): void {
210     this.$timeoutModal = this.generateModal(MarkupIdentifiers.loginrefresh);
211     this.$timeoutModal.addClass('modal-severity-notice');
212     this.$timeoutModal.find('.modal-header h4').text(TYPO3.lang['mess.login_about_to_expire_title']);
213     this.$timeoutModal.find('.modal-body').append(
214       $('<p />').text(TYPO3.lang['mess.login_about_to_expire']),
215       $('<div />', {class: 'progress'}).append(
216         $('<div />', {
217           class: 'progress-bar progress-bar-warning progress-bar-striped active',
218           role: 'progressbar',
219           'aria-valuemin': '0',
220           'aria-valuemax': '100'
221         }).append(
222           $('<span />', {class: 'sr-only'})
223         )
224       )
225     );
226     this.$timeoutModal.find('.modal-footer').append(
227       $('<button />', {
228         class: 'btn btn-default',
229         'data-action': 'logout'
230       }).text(TYPO3.lang['mess.refresh_login_logout_button']).on('click', () => {
231         top.location.href = this.logoutUrl;
232       }),
233       $('<button />', {
234         class: 'btn btn-primary t3js-active',
235         'data-action': 'refreshSession'
236       }).text(TYPO3.lang['mess.refresh_login_refresh_button']).on('click', () => {
237         $.ajax({
238           url: TYPO3.settings.ajaxUrls.login_timedout,
239           method: 'GET',
240           success: () => {
241             this.hideTimeoutModal();
242           }
243         });
244       })
245     );
246     this.registerDefaultModalEvents(this.$timeoutModal);
247
248     $('body').append(this.$timeoutModal);
249   }
250
251   /**
252    * Generates the login form displayed if the session has timed out.
253    */
254   protected initializeLoginForm(): void {
255     if (TYPO3.configuration.showRefreshLoginPopup) {
256       // dialog is not required if "showRefreshLoginPopup" is enabled
257       return;
258     }
259
260     this.$loginForm = this.generateModal(MarkupIdentifiers.loginFormModal);
261     this.$loginForm.addClass('modal-notice');
262     let refresh_login_title = String(TYPO3.lang['mess.refresh_login_title']).replace('%s', TYPO3.configuration.username);
263     this.$loginForm.find('.modal-header h4').text(refresh_login_title);
264     this.$loginForm.find('.modal-body').append(
265       $('<p />').text(TYPO3.lang['mess.login_expired']),
266       $('<form />', {
267         id: 'beLoginRefresh',
268         method: 'POST',
269         action: TYPO3.settings.ajaxUrls.login
270       }).append(
271         $('<div />', {class: 'form-group'}).append(
272           $('<input />', {
273             type: 'password',
274             name: 'p_field',
275             autofocus: 'autofocus',
276             class: 'form-control',
277             placeholder: TYPO3.lang['mess.refresh_login_password'],
278             'data-rsa-encryption': 't3-loginrefres-userident'
279           })
280         ),
281         $('<input />', {type: 'hidden', name: 'username', value: TYPO3.configuration.username}),
282         $('<input />', {type: 'hidden', name: 'userident', id: 't3-loginrefres-userident'})
283       )
284     );
285     this.$loginForm.find('.modal-footer').append(
286       $('<a />', {
287         href: this.logoutUrl,
288         class: 'btn btn-default'
289       }).text(TYPO3.lang['mess.refresh_exit_button']),
290       $('<button />', {type: 'button', class: 'btn btn-primary', 'data-action': 'refreshSession'})
291         .text(TYPO3.lang['mess.refresh_login_button'])
292         .on('click', () => {
293           this.$loginForm.find('form').submit();
294         })
295     );
296     this.registerDefaultModalEvents(this.$loginForm).on('submit', this.submitForm);
297     $('body').append(this.$loginForm);
298     if (require.specified('TYPO3/CMS/Rsaauth/RsaEncryptionModule')) {
299       require(['TYPO3/CMS/Rsaauth/RsaEncryptionModule'], function(RsaEncryption: any): void {
300         RsaEncryption.registerForm($('#beLoginRefresh').get(0));
301       });
302     }
303   }
304
305   /**
306    * Generates a modal dialog as template.
307    *
308    * @param {string} identifier
309    * @returns {JQuery}
310    */
311   protected generateModal(identifier: string): JQuery {
312     return $('<div />', {
313       id: identifier,
314       class: 't3js-modal ' + identifier + ' modal modal-type-default modal-severity-notice modal-style-light modal-size-small fade'
315     }).append(
316       $('<div />', {class: 'modal-dialog'}).append(
317         $('<div />', {class: 'modal-content'}).append(
318           $('<div />', {class: 'modal-header'}).append(
319             $('<h4 />', {class: 'modal-title'})
320           ),
321           $('<div />', {class: 'modal-body'}),
322           $('<div />', {class: 'modal-footer'})
323         )
324       )
325     );
326   }
327
328   /**
329    * Fills the progressbar attached to the given modal.
330    */
331   protected fillProgressbar($activeModal: JQuery): void {
332     if (!this.isTimingOut) {
333       return;
334     }
335
336     const max = 100;
337     let current = 0;
338     const $progressBar = $activeModal.find('.progress-bar');
339     const $srText = $progressBar.children('.sr-only');
340
341     const progress = setInterval(() => {
342       const isOverdue = (current >= max);
343       if (!this.isTimingOut || isOverdue) {
344         clearInterval(progress);
345
346         if (isOverdue) {
347           // show login form
348           this.hideTimeoutModal();
349           this.showLoginForm();
350         }
351
352         // reset current
353         current = 0;
354       } else {
355         current += 1;
356       }
357
358       const percentText = (current) + '%';
359       $progressBar.css('width', percentText);
360       $srText.text(percentText);
361     },                           300);
362   }
363
364   /**
365    * Creates additional data based on the security level and "submits" the form
366    * via an AJAX request.
367    *
368    * @param {JQueryEventObject} event
369    */
370   protected submitForm(event: JQueryEventObject): void {
371     event.preventDefault();
372
373     const $form = this.$loginForm.find('form');
374     const $passwordField = $form.find('input[name=p_field]');
375     const $useridentField = $form.find('input[name=userident]');
376     const passwordFieldValue = $passwordField.val();
377
378     if (passwordFieldValue === '' && $useridentField.val() === '') {
379       Typo3Notification.error(TYPO3.lang['mess.refresh_login_failed'], TYPO3.lang['mess.refresh_login_emptyPassword']);
380       $passwordField.focus();
381       return;
382     }
383
384     if (passwordFieldValue) {
385       $useridentField.val(passwordFieldValue);
386       $passwordField.val('');
387     }
388
389     const postData: any = {
390       login_status: 'login'
391     };
392     $.each($form.serializeArray(), function(i: number, field: any): void {
393       postData[field.name] = field.value;
394     });
395     $.ajax({
396       url: $form.attr('action'),
397       method: 'POST',
398       data: postData,
399       success: (response) => {
400         if (response.login.success) {
401           // User is logged in
402           this.hideLoginForm();
403         } else {
404           Typo3Notification.error(TYPO3.lang['mess.refresh_login_failed'], TYPO3.lang['mess.refresh_login_failed_message']);
405           $passwordField.focus();
406         }
407       }
408     });
409   }
410
411   /**
412    * Registers the (shown|hidden).bs.modal events.
413    * If a modal is shown, the interval check is stopped. If the modal hides,
414    * the interval check starts again.
415    * This method is not invoked for the backend locked modal, because we still
416    * need to check if the backend gets unlocked again.
417    *
418    * @param {JQuery} $modal
419    * @returns {JQuery}
420    */
421   protected registerDefaultModalEvents($modal: JQuery): JQuery {
422     $modal.on('hidden.bs.modal', () => {
423       this.startTask();
424     }).on('shown.bs.modal', () => {
425       this.stopTask();
426       // focus the button which was configured as active button
427       this.$timeoutModal.find('.modal-footer .t3js-active').first().focus();
428     });
429     return $modal;
430   }
431
432   /**
433    * Periodically called task that checks if
434    *
435    * - the user's backend session is about to expire
436    * - the user's backend session has expired
437    * - the backend got locked
438    *
439    * and opens a dialog.
440    */
441   protected checkActiveSession = (): void => {
442     $.ajax({
443       url: TYPO3.settings.ajaxUrls.login_timedout,
444       success: (response) => {
445         if (response.login.locked) {
446           if (!this.backendIsLocked) {
447             this.backendIsLocked = true;
448             this.showBackendLockedModal();
449           }
450         } else {
451           if (this.backendIsLocked) {
452             this.backendIsLocked = false;
453             this.hideBackendLockedModal();
454           }
455         }
456
457         if (!this.backendIsLocked) {
458           if (response.login.timed_out || response.login.will_time_out) {
459             response.login.timed_out
460               ? this.showLoginForm()
461               : this.showTimeoutModal();
462           }
463         }
464       }
465     });
466   }
467 }
468
469 let loginRefreshObject;
470 try {
471   // fetch from opening window
472   if (window.opener && window.opener.TYPO3 && window.opener.TYPO3.LoginRefresh) {
473     loginRefreshObject = window.opener.TYPO3.LoginRefresh;
474   }
475
476   // fetch from parent
477   if (parent && parent.window.TYPO3 && parent.window.TYPO3.LoginRefresh) {
478     loginRefreshObject = parent.window.TYPO3.LoginRefresh;
479   }
480
481   // fetch object from outer frame
482   if (top && top.TYPO3 && top.TYPO3.LoginRefresh) {
483     loginRefreshObject = top.TYPO3.LoginRefresh;
484   }
485 } catch (e) {
486   // This only happens if the opener, parent or top is some other url (eg a local file)
487   // which loaded the current window. Then the browser's cross domain policy jumps in
488   // and raises an exception.
489   // For this case we are safe and we can create our global object below.
490 }
491
492 if (!loginRefreshObject) {
493   loginRefreshObject = new LoginRefresh();
494
495   // attach to global frame
496   if (typeof TYPO3 !== 'undefined') {
497     TYPO3.LoginRefresh = loginRefreshObject;
498   }
499 }
500
501 export = loginRefreshObject;