You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
547 lines
13 KiB
547 lines
13 KiB
/*
|
|
** Zabbix
|
|
** Copyright (C) 2001-2023 Zabbix SIA
|
|
**
|
|
** This program is free software; you can redistribute it and/or modify
|
|
** it under the terms of the GNU General Public License as published by
|
|
** the Free Software Foundation; either version 2 of the License, or
|
|
** (at your option) any later version.
|
|
**
|
|
** This program is distributed in the hope that it will be useful,
|
|
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
** GNU General Public License for more details.
|
|
**
|
|
** You should have received a copy of the GNU General Public License
|
|
** along with this program; if not, write to the Free Software
|
|
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
**/
|
|
|
|
|
|
ZBX_LocalStorage.defines = {
|
|
PREFIX_SEPARATOR: ':',
|
|
KEEP_ALIVE_INTERVAL: 30,
|
|
SYNC_INTERVAL_MS: 500,
|
|
KEY_SESSIONS: 'sessions',
|
|
KEY_LAST_WRITE: 'key_last_write'
|
|
};
|
|
|
|
/**
|
|
* Local storage wrapper. Implements singleton.
|
|
*
|
|
* @param {string} version Mandatory parameter.
|
|
* @param {string} prefix Used to distinct keys between sessions within same domain.
|
|
*/
|
|
function ZBX_LocalStorage(version, prefix) {
|
|
if (!version || !prefix) {
|
|
throw 'Local storage instantiation must be versioned, and prefixed.';
|
|
}
|
|
|
|
if (ZBX_LocalStorage.instance) {
|
|
return ZBX_LocalStorage.instance;
|
|
}
|
|
|
|
ZBX_LocalStorage.sessionid = prefix;
|
|
ZBX_LocalStorage.prefix = prefix + ZBX_LocalStorage.defines.PREFIX_SEPARATOR;
|
|
ZBX_LocalStorage.instance = this;
|
|
ZBX_LocalStorage.signature = (Math.random() % 9e6).toString(36).substr(2);
|
|
|
|
this.abs_to_rel_keymap = {};
|
|
this.key_last_write = {};
|
|
this.rel_keys = {};
|
|
this.abs_keys = {};
|
|
|
|
this.addKey('version');
|
|
this.addKey('tabs.lastseen');
|
|
this.addKey('notifications.list');
|
|
this.addKey('notifications.active_tabid');
|
|
this.addKey('notifications.user_settings');
|
|
this.addKey('notifications.alarm_state');
|
|
this.addKey('dashboard.copied_widget');
|
|
this.addKey('web.notifications.pos');
|
|
|
|
if (this.readKey('version') != version) {
|
|
this.truncate();
|
|
this.writeKey('version', version);
|
|
}
|
|
|
|
this.register();
|
|
}
|
|
|
|
/**
|
|
* @param {string} key Relative key.
|
|
* @param {callable} callback When out of sync is observed, then new value is read, and passed into callback.
|
|
*
|
|
* @return {string}
|
|
*/
|
|
ZBX_LocalStorage.prototype.onKeySync = function(key, callback) {
|
|
this.rel_keys[key].subscribeSync(callback);
|
|
};
|
|
|
|
/**
|
|
* @param {string} key Relative key.
|
|
* @param {callable} callback Executed on storage event for this key.
|
|
*
|
|
* @return {string}
|
|
*/
|
|
ZBX_LocalStorage.prototype.onKeyUpdate = function(key, callback) {
|
|
this.rel_keys[key].subscribe(callback);
|
|
};
|
|
|
|
/**
|
|
* Transform key into absolute key.
|
|
*
|
|
* @param {string} key
|
|
*
|
|
* @return {string}
|
|
*/
|
|
ZBX_LocalStorage.prototype.toAbsKey = function(key) {
|
|
return ZBX_LocalStorage.prefix + key;
|
|
};
|
|
|
|
/**
|
|
* @param {string} relative_key
|
|
*/
|
|
ZBX_LocalStorage.prototype.addKey = function(relative_key) {
|
|
var absolute_key = this.toAbsKey(relative_key);
|
|
|
|
this.rel_keys[relative_key] = new ZBX_LocalStorageKey(relative_key);
|
|
this.abs_keys[absolute_key] = this.rel_keys[relative_key];
|
|
this.abs_to_rel_keymap[absolute_key] = relative_key;
|
|
};
|
|
|
|
/**
|
|
* @param {Store} store
|
|
* @param {string} sessionid
|
|
*/
|
|
ZBX_LocalStorage.prototype.freeSession = function(store, sessionid) {
|
|
var len = store.length,
|
|
matches = [],
|
|
abs_key;
|
|
|
|
for (var i = 0; i < len; i++) {
|
|
abs_key = store.key(i);
|
|
if (abs_key.match('^' + sessionid)) {
|
|
matches.push(abs_key);
|
|
}
|
|
}
|
|
|
|
matches.forEach(function(abs_key) {
|
|
store.removeItem(abs_key);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Keeps alive local storage sessions. Removes inactive session.
|
|
*/
|
|
ZBX_LocalStorage.prototype.keepAlive = function() {
|
|
var store = localStorage,
|
|
lastseen = JSON.parse(store.getItem(ZBX_LocalStorage.defines.KEY_SESSIONS) || '{}'),
|
|
timestamp = (+new Date / 1000) >> 0,
|
|
expired_timestamp = timestamp - 2 * ZBX_LocalStorage.defines.KEEP_ALIVE_INTERVAL;
|
|
|
|
lastseen[ZBX_LocalStorage.sessionid] = (+new Date() / 1000) >> 0;
|
|
|
|
for (var sessionid in lastseen) {
|
|
if (lastseen[sessionid] < expired_timestamp) {
|
|
this.freeSession(store, sessionid);
|
|
delete lastseen[sessionid];
|
|
}
|
|
}
|
|
|
|
store.setItem(ZBX_LocalStorage.defines.KEY_SESSIONS, JSON.stringify(lastseen));
|
|
};
|
|
|
|
/**
|
|
* @param {RegEx|string} regex
|
|
* @param {callback} callback
|
|
*/
|
|
ZBX_LocalStorage.prototype.eachKeyRegex = function(regex, callback) {
|
|
this.eachKey(function(key) {
|
|
key.relative_key.match(regex) && callback(key);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {callback}
|
|
*/
|
|
ZBX_LocalStorage.prototype.eachKey = function(callback) {
|
|
for (var i in this.rel_keys) {
|
|
callback(this.rel_keys[i]);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Validates if key is used by this version of localStorage.
|
|
*
|
|
* @param {string} key Key to test.
|
|
*
|
|
* @return {bool}
|
|
*/
|
|
ZBX_LocalStorage.prototype.hasKey = function(key) {
|
|
return typeof this.rel_keys[key] !== 'undefined';
|
|
};
|
|
|
|
/**
|
|
* Alias to throw error on invalid key access.
|
|
*
|
|
* @param {string} key Key to test.
|
|
*/
|
|
ZBX_LocalStorage.prototype.ensureKey = function(key) {
|
|
if (typeof key !== 'string') {
|
|
throw 'Key must be a string, ' + (typeof key) + ' given instead.';
|
|
}
|
|
|
|
if (!this.hasKey(key)) {
|
|
throw 'Unknown localStorage key access at "' + key + '"';
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This whole design of signed payloads exists only because of IE11.
|
|
*
|
|
* @param {mixed} value
|
|
*
|
|
* @return {string}
|
|
*/
|
|
ZBX_LocalStorage.prototype.wrap = function(value) {
|
|
return JSON.stringify({
|
|
payload: value,
|
|
signature: ZBX_LocalStorage.signature
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Not only IE dispatches 'storage' event 'onwrite' instead of 'onchange', but this event is also dispatched onto
|
|
* window that is the modifier. So we need to sign all payloads.
|
|
*
|
|
* @param {string} value
|
|
*
|
|
* @return {mixed}
|
|
*/
|
|
ZBX_LocalStorage.prototype.unwrap = function(value) {
|
|
if (value === null) {
|
|
throw "Expected JSON string";
|
|
}
|
|
|
|
return JSON.parse(value);
|
|
};
|
|
|
|
/**
|
|
* After this method call local storage will not perform any further writes.
|
|
*/
|
|
ZBX_LocalStorage.prototype.destruct = function() {
|
|
this.writeKey = function() {};
|
|
this.truncate();
|
|
this.truncateBackup();
|
|
};
|
|
|
|
/**
|
|
* Backup keys are removed.
|
|
*/
|
|
ZBX_LocalStorage.prototype.truncateBackup = function() {
|
|
this.eachKey(function(key) {
|
|
key.truncateBackup();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Removes all local storage and creates default objects. Backup keys are not removed.
|
|
*/
|
|
ZBX_LocalStorage.prototype.truncate = function() {
|
|
this.eachKey(function(key) {
|
|
key.truncate();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Synchronization tick. It checks if any of keys have been written outside the window object where this instance runs.
|
|
* It compares last writes this instance knows about with last writes in storage. @see this.flushKeyWrite,
|
|
* When it is detected that some key is out of sync, then synthetic event is dispatched providing subscribers with
|
|
* new value, keep in mind that a key may hold native event subscription via `onKeyUpdate` and synthetic event is
|
|
* provided via `onKeySync` method.
|
|
*/
|
|
ZBX_LocalStorage.prototype.syncTick = function() {
|
|
var key_last_write = this.fetchKeyWrites();
|
|
|
|
for (var abs_key in this.key_last_write) {
|
|
if (this.key_last_write[abs_key] != key_last_write[abs_key]) {
|
|
this.key_last_write[abs_key] = key_last_write[abs_key];
|
|
this.abs_keys[abs_key].publishSync(this.readKey(this.abs_to_rel_keymap[abs_key]));
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Adds event handlers.
|
|
*/
|
|
ZBX_LocalStorage.prototype.register = function() {
|
|
window.addEventListener('storage', this.handleStorageEvent.bind(this));
|
|
this.keepAlive();
|
|
setInterval(this.keepAlive.bind(this), ZBX_LocalStorage.defines.KEEP_ALIVE_INTERVAL * 1000);
|
|
|
|
this.syncTick();
|
|
setInterval(this.syncTick.bind(this), ZBX_LocalStorage.defines.SYNC_INTERVAL_MS);
|
|
};
|
|
|
|
/**
|
|
* @param {StorageEvent} event
|
|
*/
|
|
ZBX_LocalStorage.prototype.handleStorageEvent = function(event) {
|
|
if (event.constructor != StorageEvent) {
|
|
throw 'Unmatched method signature!';
|
|
}
|
|
|
|
// Internal usage key.
|
|
if (event.key === ZBX_LocalStorage.defines.KEY_LAST_WRITE) {
|
|
return;
|
|
}
|
|
|
|
// Internal usage key.
|
|
if (event.key === ZBX_LocalStorage.defines.KEY_SESSIONS) {
|
|
return;
|
|
}
|
|
|
|
// This means, storage has been truncated.
|
|
if (event.key === null || event.key === '') {
|
|
return;
|
|
}
|
|
|
|
if (event.newValue === event.oldValue) {
|
|
// This is expensive, but internet expoler just dispatched storage change event at write.
|
|
return;
|
|
}
|
|
|
|
var value;
|
|
|
|
try {
|
|
value = this.unwrap(event.newValue);
|
|
}
|
|
catch(e) {
|
|
// If value could not be unwrapped, it has not originated from this class.
|
|
return;
|
|
}
|
|
|
|
if (value.signature === ZBX_LocalStorage.signature) {
|
|
// Internet explorer just dispatched storage event onto current instance.
|
|
return;
|
|
}
|
|
|
|
if (!this.abs_keys[event.key]) {
|
|
return;
|
|
}
|
|
|
|
this.abs_keys[event.key].publish(value.payload);
|
|
};
|
|
|
|
/**
|
|
* Writes an underlying value.
|
|
*
|
|
* @param {string} key
|
|
* @param {string} value
|
|
*/
|
|
ZBX_LocalStorage.prototype.writeKey = function(key, value) {
|
|
if (typeof value === 'undefined') {
|
|
throw 'Value may not be undefined, use null instead: ' + key;
|
|
}
|
|
|
|
this.ensureKey(key);
|
|
this.rel_keys[key].write(this.wrap(value));
|
|
this.flushKeyWrite(this.toAbsKey(key));
|
|
};
|
|
|
|
/**
|
|
* @return {object}
|
|
*/
|
|
ZBX_LocalStorage.prototype.fetchKeyWrites = function() {
|
|
var key_writes = JSON.parse(localStorage.getItem(ZBX_LocalStorage.defines.KEY_LAST_WRITE) || '{}'),
|
|
session_regex = '^' + ZBX_LocalStorage.prefix,
|
|
session_key_writes = {};
|
|
|
|
for (var abs_key in key_writes) {
|
|
if (abs_key.match(session_regex)) {
|
|
session_key_writes[abs_key] = key_writes[abs_key];
|
|
}
|
|
}
|
|
|
|
return session_key_writes;
|
|
};
|
|
|
|
/**
|
|
* Merges local sync object updates and writes them into store.
|
|
*
|
|
* @param {string} abs_key Absolute key.
|
|
*/
|
|
ZBX_LocalStorage.prototype.flushKeyWrite = function(abs_key) {
|
|
var key_last_write = this.fetchKeyWrites();
|
|
|
|
key_last_write[abs_key] = +new Date;
|
|
|
|
localStorage.setItem(ZBX_LocalStorage.defines.KEY_LAST_WRITE, JSON.stringify(key_last_write));
|
|
|
|
this.key_last_write = key_last_write;
|
|
};
|
|
|
|
/**
|
|
* Fetches underlying value. A copy of default value is returned if key has no data.
|
|
*
|
|
* @param {string} key
|
|
*
|
|
* @return {mixed}
|
|
*/
|
|
ZBX_LocalStorage.prototype.readKey = function(key, fallback) {
|
|
this.ensureKey(key);
|
|
|
|
try {
|
|
var item = this.rel_keys[key].fetch();
|
|
|
|
if (!item) {
|
|
return fallback;
|
|
}
|
|
|
|
return this.unwrap(item).payload;
|
|
}
|
|
catch(e) {
|
|
console.warn('failed to parse storage item "' + key + '"');
|
|
|
|
this.truncate();
|
|
this.truncateBackup();
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* @param {string} relative_key
|
|
*/
|
|
function ZBX_LocalStorageKey(relative_key) {
|
|
this.backup_store = sessionStorage;
|
|
this.primary_store = localStorage;
|
|
|
|
this.relative_key = relative_key;
|
|
this.absolute_key = ZBX_LocalStorage.prefix + this.relative_key;
|
|
|
|
this.on_update_cbs = [];
|
|
this.on_sync_cbs = [];
|
|
}
|
|
|
|
/**
|
|
* @throw
|
|
*
|
|
* @param {string} string
|
|
*/
|
|
ZBX_LocalStorageKey.prototype.writePrimary = function(string) {
|
|
if (string.constructor != String || string[0] != '{') {
|
|
throw 'Corrupted input.';
|
|
}
|
|
this.primary_store.setItem(this.absolute_key, string);
|
|
};
|
|
|
|
/**
|
|
* @throw
|
|
*
|
|
* @param {string} string
|
|
*/
|
|
ZBX_LocalStorageKey.prototype.writeBackup = function(string) {
|
|
if (string.constructor != String || string[0] != '{') {
|
|
throw 'Corrupted input.';
|
|
}
|
|
this.backup_store.setItem(this.absolute_key, string);
|
|
};
|
|
|
|
/**
|
|
* @param {string} string
|
|
*/
|
|
ZBX_LocalStorageKey.prototype.write = function(string) {
|
|
this.writeBackup(string);
|
|
this.writePrimary(string);
|
|
};
|
|
|
|
/**
|
|
* Fetch key value.
|
|
*
|
|
* @return {string|null} Null is returned if key is deleted.
|
|
*/
|
|
ZBX_LocalStorageKey.prototype.fetchPrimary = function() {
|
|
return this.primary_store.getItem(this.absolute_key);
|
|
};
|
|
|
|
/**
|
|
* Fetch key value.
|
|
*
|
|
* @return {string|null} Null is returned if key is deleted.
|
|
*/
|
|
ZBX_LocalStorageKey.prototype.fetchBackup = function() {
|
|
return this.backup_store.getItem(this.absolute_key);
|
|
};
|
|
|
|
/**
|
|
* Fetch key value.
|
|
*
|
|
* @return {string|null} Null is returned if key is deleted.
|
|
*/
|
|
ZBX_LocalStorageKey.prototype.fetch = function() {
|
|
return this.fetchPrimary() || this.fetchBackup();
|
|
};
|
|
|
|
/**
|
|
* Removes current key data from backup store.
|
|
*/
|
|
ZBX_LocalStorageKey.prototype.truncateBackup = function() {
|
|
this.backup_store.removeItem(this.absolute_key);
|
|
};
|
|
|
|
/**
|
|
* Removes current key data from primary store.
|
|
*/
|
|
ZBX_LocalStorageKey.prototype.truncatePrimary = function() {
|
|
this.primary_store.removeItem(this.absolute_key);
|
|
};
|
|
|
|
/**
|
|
* Removes current key data from stores.
|
|
*/
|
|
ZBX_LocalStorageKey.prototype.truncate = function() {
|
|
this.truncateBackup();
|
|
this.truncatePrimary();
|
|
};
|
|
|
|
/**
|
|
* Sunscribe a callback to key sync request.
|
|
*
|
|
* @param {callable} callback
|
|
*/
|
|
ZBX_LocalStorageKey.prototype.subscribeSync = function(callback) {
|
|
this.on_sync_cbs.push(callback);
|
|
};
|
|
|
|
/**
|
|
* @param {object} payload
|
|
*/
|
|
ZBX_LocalStorageKey.prototype.publishSync = function(payload) {
|
|
this.on_sync_cbs.forEach(function(callback) {
|
|
callback(payload);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {callable} callback
|
|
*/
|
|
ZBX_LocalStorageKey.prototype.subscribe = function(callback) {
|
|
this.on_update_cbs.push(callback);
|
|
};
|
|
|
|
/**
|
|
* @param {object} payload
|
|
*/
|
|
ZBX_LocalStorageKey.prototype.publish = function(payload) {
|
|
this.on_update_cbs.forEach(function(callback) {
|
|
callback(payload);
|
|
});
|
|
};
|
|
|
|
ZABBIX.namespace(
|
|
'instances.localStorage',
|
|
new ZBX_LocalStorage('1', window.ZBX_SESSION_NAME)
|
|
);
|