Commit 6f9e25d9 authored by Andreas Fernandez's avatar Andreas Fernandez Committed by Frank Nägler
Browse files

[FEATURE] Store icons in localStorage

The icons that get requested by the Icon API on JavaScript side are not
stored in the client's localStorage.
To have a proper invalidation, a hash of the IconRegistry is built and
stored in the localStorage, too. If the hash changes, all icons in the
localStorage get flushed.

To achieve this, the Storage/Client module is extended to allow removing
values by a given prefix.

Resolves: #84780
Releases: master
Change-Id: Ic2137b05530201a8a94a7ea6c28ae1a012206221
Reviewed-on: https://review.typo3.org/56721


Reviewed-by: default avatarMathias Schreiber <mathias.schreiber@typo3.com>
Tested-by: default avatarMathias Schreiber <mathias.schreiber@typo3.com>
Tested-by: default avatarTYPO3com <no-reply@typo3.com>
Reviewed-by: Kay Strobach's avatarKay Strobach <typo3@kay-strobach.de>
Tested-by: Kay Strobach's avatarKay Strobach <typo3@kay-strobach.de>
Reviewed-by: Frank Nägler's avatarFrank Naegler <frank.naegler@typo3.org>
Tested-by: Frank Nägler's avatarFrank Naegler <frank.naegler@typo3.org>
parent 895ec997
......@@ -234,6 +234,12 @@ return [
'target' => \TYPO3\CMS\Core\Controller\IconController::class . '::getIcon'
],
// Get icon cache identifier
'icons_cache' => [
'path' => '/icons/cache',
'target' => \TYPO3\CMS\Core\Controller\IconController::class . '::getCacheIdentifier'
],
// Encode typolink parts on demand
'link_browser_encodetypolink' => [
'path' => '/link-browser/encode-typolink',
......
......@@ -12,6 +12,7 @@
*/
import * as $ from 'jquery';
import ClientStorage = require('./Storage/Client');
enum Sizes {
small = 'small',
......@@ -30,7 +31,7 @@ enum MarkupIdentifiers {
inline = 'inline'
}
interface Cache {
interface PromiseCache {
[key: string]: JQueryPromise<any>;
}
......@@ -42,7 +43,7 @@ class Icons {
public readonly sizes: any = Sizes;
public readonly states: any = States;
public readonly markupIdentifiers: any = MarkupIdentifiers;
private readonly cache: Cache = {};
private readonly promiseCache: PromiseCache = {};
/**
* Get the icon by its identifier
......@@ -59,24 +60,7 @@ class Icons {
overlayIdentifier?: string,
state?: string,
markupIdentifier?: MarkupIdentifiers): JQueryPromise<any> {
return $.when(this.fetch(identifier, size, overlayIdentifier, state, markupIdentifier));
}
/**
* Performs the AJAX request to fetch the icon
*
* @param {string} identifier
* @param {Sizes} size
* @param {string} overlayIdentifier
* @param {string} state
* @param {MarkupIdentifiers} markupIdentifier
* @returns {JQueryPromise<any>}
*/
public fetch(identifier: string,
size: Sizes,
overlayIdentifier: string,
state: string,
markupIdentifier: MarkupIdentifiers): JQueryPromise<any> {
/**
* Icon keys:
*
......@@ -90,22 +74,76 @@ class Icons {
state = state || States.default;
markupIdentifier = markupIdentifier || MarkupIdentifiers.default;
const icon = [identifier, size, overlayIdentifier, state, markupIdentifier];
const cacheIdentifier = icon.join('_');
const describedIcon = [identifier, size, overlayIdentifier, state, markupIdentifier];
const cacheIdentifier = describedIcon.join('_');
return $.when(this.getIconRegistryCache()).pipe((registryCacheIdentifier: string): any => {
if (!ClientStorage.isset('icon_registry_cache_identifier')
|| ClientStorage.get('icon_registry_cache_identifier') !== registryCacheIdentifier
) {
ClientStorage.unsetByPrefix('icon_');
ClientStorage.set('icon_registry_cache_identifier', registryCacheIdentifier);
}
return this.fetchFromLocal(cacheIdentifier).then(null, (): any => {
return this.fetchFromRemote(describedIcon, cacheIdentifier);
});
});
}
private getIconRegistryCache(): JQueryPromise<any> {
const promiseCacheIdentifier = 'icon_registry_cache_identifier';
if (!this.isPromiseCached(promiseCacheIdentifier)) {
this.putInPromiseCache(promiseCacheIdentifier, $.ajax({
url: TYPO3.settings.ajaxUrls.icons_cache,
success: (response: string): string => {
return response;
}
}));
}
return this.getFromPromiseCache(promiseCacheIdentifier);
}
if (!this.isCached(cacheIdentifier)) {
this.putInCache(cacheIdentifier, $.ajax({
/**
* Performs the AJAX request to fetch the icon
*
* @param {Array<string>} icon
* @param {string} cacheIdentifier
* @returns {JQueryPromise<any>}
*/
private fetchFromRemote(icon: Array<string>, cacheIdentifier: string): JQueryPromise<any> {
if (!this.isPromiseCached(cacheIdentifier)) {
this.putInPromiseCache(cacheIdentifier, $.ajax({
url: TYPO3.settings.ajaxUrls.icons,
dataType: 'html',
data: {
icon: JSON.stringify(icon)
},
success: (markup: string) => {
ClientStorage.set('icon_' + cacheIdentifier, markup);
return markup;
}
}).promise());
}));
}
return this.getFromCache(cacheIdentifier).done();
return this.getFromPromiseCache(cacheIdentifier);
}
/**
* Gets the icon from localStorage
* @param {string} cacheIdentifier
* @returns {JQueryPromise<any>}
*/
private fetchFromLocal(cacheIdentifier: string): JQueryPromise<any> {
const deferred = $.Deferred();
if (ClientStorage.isset('icon_' + cacheIdentifier)) {
deferred.resolve(ClientStorage.get('icon_' + cacheIdentifier));
} else {
deferred.reject();
}
return deferred.promise();
}
/**
......@@ -114,8 +152,8 @@ class Icons {
* @param {string} cacheIdentifier
* @returns {boolean}
*/
private isCached(cacheIdentifier: string): boolean {
return typeof this.cache[cacheIdentifier] !== 'undefined';
private isPromiseCached(cacheIdentifier: string): boolean {
return typeof this.promiseCache[cacheIdentifier] !== 'undefined';
}
/**
......@@ -124,8 +162,8 @@ class Icons {
* @param {string} cacheIdentifier
* @returns {JQueryPromise<any>}
*/
private getFromCache(cacheIdentifier: string): JQueryPromise<any> {
return this.cache[cacheIdentifier];
private getFromPromiseCache(cacheIdentifier: string): JQueryPromise<any> {
return this.promiseCache[cacheIdentifier];
}
/**
......@@ -134,34 +172,12 @@ class Icons {
* @param {string} cacheIdentifier
* @param {JQueryPromise<any>} markup
*/
private putInCache(cacheIdentifier: string, markup: JQueryPromise<any>): void {
this.cache[cacheIdentifier] = markup;
private putInPromiseCache(cacheIdentifier: string, markup: JQueryPromise<any>): void {
this.promiseCache[cacheIdentifier] = markup;
}
}
let iconsObject: Icons;
try {
// fetch from opening window
if (window.opener && window.opener.TYPO3 && window.opener.TYPO3.Icons) {
iconsObject = window.opener.TYPO3.Icons;
}
// fetch from parent
if (parent && parent.window.TYPO3 && parent.window.TYPO3.Icons) {
iconsObject = parent.window.TYPO3.Icons;
}
// fetch object from outer frame
if (top && top.TYPO3.Icons) {
iconsObject = top.TYPO3.Icons;
}
} catch (e) {
// This only happens if the opener, parent or top is some other url (eg a local file)
// which loaded the current window. Then the browser's cross domain policy jumps in
// and raises an exception.
// For this case we are safe and we can create our global object below.
}
if (!iconsObject) {
iconsObject = new Icons();
TYPO3.Icons = iconsObject;
......
......@@ -17,13 +17,15 @@
* @exports TYPO3/CMS/Backend/Storage/Client
*/
class Client {
private keyPrefix: string = 't3-';
/**
* Simple localStorage wrapper, to get value from localStorage
* @param {string} key
* @returns {string}
*/
public get = (key: string): string => {
return localStorage.getItem('t3-' + key);
return localStorage.getItem(this.keyPrefix + key);
}
/**
......@@ -34,7 +36,7 @@ class Client {
* @returns {string}
*/
public set = (key: string, value: string): void => {
localStorage.setItem('t3-' + key, value);
localStorage.setItem(this.keyPrefix + key, value);
}
/**
......@@ -43,7 +45,31 @@ class Client {
* @param {string} key
*/
public unset = (key: string): void => {
localStorage.removeItem('t3-' + key);
localStorage.removeItem(this.keyPrefix + key);
}
/**
* Removes values from localStorage by a specific prefix of the key
*
* @param {string} prefix
*/
public unsetByPrefix = (prefix: string): void => {
prefix = this.keyPrefix + prefix;
const keysToDelete: Array<string> = [];
for (let i = 0; i < localStorage.length; ++i) {
if (localStorage.key(i).substring(0, prefix.length) === prefix) {
// Remove the global key prefix, as it gets prepended in unset again
const key = localStorage.key(i).substr(this.keyPrefix.length);
// We can't delete the key here as this interferes with the size of the localStorage
keysToDelete.push(key);
}
}
for (let key of keysToDelete) {
this.unset(key);
}
}
/**
......
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
define(["require","exports","jquery"],function(e,t,n){"use strict";var o,i,r,a,s,c;(i=o||(o={})).small="small",i.default="default",i.large="large",i.overlay="overlay",(a=r||(r={})).default="default",a.disabled="disabled",(c=s||(s={})).default="default",c.inline="inline";var u,d=function(){function e(){this.sizes=o,this.states=r,this.markupIdentifiers=s,this.cache={}}return e.prototype.getIcon=function(e,t,o,i,r){return n.when(this.fetch(e,t,o,i,r))},e.prototype.fetch=function(e,t,i,a,c){var u=[e,t=t||o.default,i,a=a||r.default,c=c||s.default],d=u.join("_");return this.isCached(d)||this.putInCache(d,n.ajax({url:TYPO3.settings.ajaxUrls.icons,dataType:"html",data:{icon:JSON.stringify(u)},success:function(e){return e}}).promise()),this.getFromCache(d).done()},e.prototype.isCached=function(e){return void 0!==this.cache[e]},e.prototype.getFromCache=function(e){return this.cache[e]},e.prototype.putInCache=function(e,t){this.cache[e]=t},e}();try{window.opener&&window.opener.TYPO3&&window.opener.TYPO3.Icons&&(u=window.opener.TYPO3.Icons),parent&&parent.window.TYPO3&&parent.window.TYPO3.Icons&&(u=parent.window.TYPO3.Icons),top&&top.TYPO3.Icons&&(u=top.TYPO3.Icons)}catch(e){}return u||(u=new d,TYPO3.Icons=u),u});
\ No newline at end of file
define(["require","exports","jquery","./Storage/Client"],function(e,t,i,r){"use strict";var n,o,s,c,a,u;(o=n||(n={})).small="small",o.default="default",o.large="large",o.overlay="overlay",(c=s||(s={})).default="default",c.disabled="disabled",(u=a||(a={})).default="default",u.inline="inline";var h,f=function(){function e(){this.sizes=n,this.states=s,this.markupIdentifiers=a,this.promiseCache={}}return e.prototype.getIcon=function(e,t,o,c,u){var h=this,f=[e,t=t||n.default,o,c=c||s.default,u=u||a.default],l=f.join("_");return i.when(this.getIconRegistryCache()).pipe(function(e){return r.isset("icon_registry_cache_identifier")&&r.get("icon_registry_cache_identifier")===e||(r.unsetByPrefix("icon_"),r.set("icon_registry_cache_identifier",e)),h.fetchFromLocal(l).then(null,function(){return h.fetchFromRemote(f,l)})})},e.prototype.getIconRegistryCache=function(){var e="icon_registry_cache_identifier";return this.isPromiseCached(e)||this.putInPromiseCache(e,i.ajax({url:TYPO3.settings.ajaxUrls.icons_cache,success:function(e){return e}})),this.getFromPromiseCache(e)},e.prototype.fetchFromRemote=function(e,t){return this.isPromiseCached(t)||this.putInPromiseCache(t,i.ajax({url:TYPO3.settings.ajaxUrls.icons,dataType:"html",data:{icon:JSON.stringify(e)},success:function(e){return r.set("icon_"+t,e),e}})),this.getFromPromiseCache(t)},e.prototype.fetchFromLocal=function(e){var t=i.Deferred();return r.isset("icon_"+e)?t.resolve(r.get("icon_"+e)):t.reject(),t.promise()},e.prototype.isPromiseCached=function(e){return void 0!==this.promiseCache[e]},e.prototype.getFromPromiseCache=function(e){return this.promiseCache[e]},e.prototype.putInPromiseCache=function(e,t){this.promiseCache[e]=t},e}();return h||(h=new f,TYPO3.Icons=h),h});
\ No newline at end of file
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
define(["require","exports"],function(t,e){"use strict";return new function(){var t=this;this.get=function(t){return localStorage.getItem("t3-"+t)},this.set=function(t,e){localStorage.setItem("t3-"+t,e)},this.unset=function(t){localStorage.removeItem("t3-"+t)},this.clear=function(){localStorage.clear()},this.isset=function(e){var n=t.get(e);return null!=n}}});
\ No newline at end of file
var __values=this&&this.__values||function(e){var t="function"==typeof Symbol&&e[Symbol.iterator],r=0;return t?t.call(e):{next:function(){return e&&r>=e.length&&(e=void 0),{value:e&&e[r++],done:!e}}}};define(["require","exports"],function(e,t){"use strict";return new function(){var e=this;this.keyPrefix="t3-",this.get=function(t){return localStorage.getItem(e.keyPrefix+t)},this.set=function(t,r){localStorage.setItem(e.keyPrefix+t,r)},this.unset=function(t){localStorage.removeItem(e.keyPrefix+t)},this.unsetByPrefix=function(t){t=e.keyPrefix+t;for(var r,n,o=[],i=0;i<localStorage.length;++i)if(localStorage.key(i).substring(0,t.length)===t){var l=localStorage.key(i).substr(e.keyPrefix.length);o.push(l)}try{for(var a=__values(o),u=a.next();!u.done;u=a.next())l=u.value,e.unset(l)}catch(e){r={error:e}}finally{try{u&&!u.done&&(n=a.return)&&n.call(a)}finally{if(r)throw r.error}}},this.clear=function(){localStorage.clear()},this.isset=function(t){var r=e.get(t);return null!=r}}});
\ No newline at end of file
......@@ -45,29 +45,29 @@ define(['jquery', 'TYPO3/CMS/Backend/Icons'], function($, Icons) {
*/
describe('tests for Icons::putInCache', function() {
it('works for simply identifier and markup', function() {
Icons.putInCache('foo', 'bar');
expect(Icons.cache['foo']).toBe('bar');
Icons.putInPromiseCache('foo', 'bar');
expect(Icons.promiseCache['foo']).toBe('bar');
});
});
/**
* @test
*/
describe('tests for Icons::getFromCache', function() {
it('return undefined for uncached icon', function() {
expect(Icons.getFromCache('bar')).not.toBeDefined();
describe('tests for Icons::getFromPromiseCache', function() {
it('return undefined for uncached promise', function() {
expect(Icons.getFromPromiseCache('bar')).not.toBeDefined();
});
});
/**
* @test
*/
describe('tests for Icons::isCached', function() {
it('return true for cached icon', function() {
expect(Icons.isCached('foo')).toBe(true);
describe('tests for Icons::isPromiseCached', function() {
it('return true for cached promise', function() {
expect(Icons.isPromiseCached('foo')).toBe(true);
});
it('return false for uncached icon', function() {
expect(Icons.isCached('bar')).toBe(false);
it('return false for uncached promise', function() {
expect(Icons.isPromiseCached('bar')).toBe(false);
});
});
});
......
......@@ -19,14 +19,22 @@ use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconRegistry;
use TYPO3\CMS\Core\Type\Icon\IconState;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Controller for icon handling
*
* @internal
*/
class IconController
{
/**
* @var IconRegistry
*/
protected $iconRegistry;
/**
* @var IconFactory
*/
......@@ -37,9 +45,19 @@ class IconController
*/
public function __construct()
{
$this->iconRegistry = GeneralUtility::makeInstance(IconRegistry::class);
$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
}
/**
* @return ResponseInterface
* @internal
*/
public function getCacheIdentifier(): ResponseInterface
{
return new HtmlResponse($this->iconRegistry->getCacheIdentifier());
}
/**
* @param ServerRequestInterface $request
* @return ResponseInterface
......
......@@ -762,6 +762,21 @@ class IconRegistry implements SingletonInterface
return $this->mimeTypeMapping[$mimeType];
}
/**
* Calculates the cache identifier based on the current registry
*
* @return string
* @internal
*/
public function getCacheIdentifier(): string
{
if (!$this->fullInitialized) {
$this->initialize();
}
return sha1(json_encode($this->icons));
}
/**
* Load icons from TCA for each table and add them as "tcarecords-XX" to $this->icons
*/
......
.. include:: ../../Includes.txt
==============================================================
Feature: #84780 - Remove entries in localStorage by key prefix
==============================================================
See :issue:`84780`
Description
===========
The localStorage wrapper :js:`TYPO3/CMS/Backend/Storage/Client` is now capable of removing entries in the localStorage
by a specific key prefix.
Impact
======
The method :js:`Client.unsetByPrefix()` takes the prefix as argument. As all keys are internally namespaced with a `t3-`
prefix, this must be omitted in the requested prefix.
Example:
.. code-block:: javascript
function foo() {
// Removes any localStorage entry whose key starts with "t3-bar-"
Client.unsetByPrefix('bar-');
}
.. index:: Backend, JavaScript, ext:backend
.. include:: ../../Includes.txt
=====================================================================
Feature: #84780 - Store icons fetched by the Icon API in localStorage
=====================================================================
See :issue:`84780`
Description
===========
Icons that get fetched by the JavaScript-based Icon API are now stored in the localStorage of the client.
A hash is calculated based on the state of the IconRegistry and stored in the localStorage as well to determine whether
the icon markup need to get refetched. from the server
.. index:: Backend, JavaScript, ext:backend
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment