Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4115231
kvStore.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
13 KB
Referenced Files
None
Subscribers
None
kvStore.js
View Options
/**
* Module for key-value storage in localStorage, used by CentralNotice campaign
* mixins and in-banner JS.
*
* This class is made available at mw.centralNotice.kvStore
*/
(
function
()
{
var
KVStorageContext
,
kvStore
,
error
=
null
,
campaignName
=
null
,
bannerName
=
null
,
category
=
null
,
cookiesEnabled
=
null
,
localStorageAvailable
=
null
,
now
=
Math
.
round
(
Date
.
now
()
/
1000
),
SEPARATOR
=
'|'
,
// | gets encoded in cookies, but is already in use in localStorage
SEPARATOR_IN_COOKIES
=
'!'
,
FIND_KEY_REGEX
=
/\|([^|]*)$/
,
// Prefix for all localStorage keys.
// Must correspond with PREFIX_REGEX from kvStoreMaintenance.
PREFIX
=
'CentralNoticeKV'
,
// In cookies, keep it short
PREFIX_IN_COOKIES
=
'CN'
,
// Default TTL of KV store items is 1/2 year, in seconds
DEFAULT_ITEM_TTL
=
(
365
/
2
)
*
60
*
60
*
24
;
/**
* A context for key-value storage.
*
* @class KVStorageContext
* @param {string} key A unique string to identify this context, when using
* LocalStorage. Must not contain SEPARATOR.
* @param {string} keyInCookies A unique string to identify this context,
* when using cookies. Must not contain SEPARATOR_IN_COOKIES. (Distinct
* keys for cookies help keep cookies small, improving performance.)
*/
KVStorageContext
=
function
(
key
,
keyInCookies
)
{
this
.
key
=
key
;
this
.
keyInCookies
=
keyInCookies
;
};
/**
* Are cookies enabled on this client?
* TODO Should this go in core?
*/
function
areCookiesEnabled
()
{
// On the first call, set a cookie and try to read it back
if
(
cookiesEnabled
===
null
)
{
mw
.
cookie
.
set
(
'cookieTest'
,
'testVal'
);
cookiesEnabled
=
(
mw
.
cookie
.
get
(
'cookieTest'
)
===
'testVal'
);
// Clear it out
mw
.
cookie
.
set
(
'cookieTest'
,
null
);
}
return
cookiesEnabled
;
}
/**
* Is LocalStorage available as a storage option? (Browser
* compatibility and certain user privacy options are required.)
*/
function
isLocalStorageAvailable
()
{
if
(
localStorageAvailable
===
null
)
{
// For the KV store to work, the browser has to support
// localStorage, and not throw an error if we try to access or use
// it. (An error can be thrown if the user completely disables
// offline website data/cookies, and in a few other circumstances)
try
{
if
(
!
window
.
localStorage
)
{
localStorageAvailable
=
false
;
}
else
{
localStorage
.
setItem
(
'localStorageTest'
,
'testVal'
);
localStorageAvailable
=
(
localStorage
.
getItem
(
'localStorageTest'
)
===
'testVal'
);
localStorage
.
removeItem
(
'localStorageTest'
);
}
}
catch
(
e
)
{
localStorageAvailable
=
false
;
}
}
return
localStorageAvailable
;
}
/**
* Flag that a problem with key-value storage occurred, and log via mw.log.
*
* @param {string} message A message about the error
* @param {string} key
* @param {*} value
* @param {KVStorageContext} context
*/
function
setError
(
message
,
key
,
value
,
context
)
{
error
=
{
message
:
message
,
key
:
key
,
value
:
value
,
context
:
context
?
context
.
key
:
null
,
time
:
new
Date
()
};
// If a campaign and/or a banner name have been set, include their names
// in the error
error
.
campaign
=
campaignName
;
error
.
banner
=
bannerName
;
mw
.
log
(
'CentralNotice KV storage error: '
+
JSON
.
stringify
(
error
)
);
}
/**
* Return the actual key to be used in localStorage, for the given key and
* context.
*
* The key returned should be unique among all localStorage keys used by
* this site. It includes unique strings for centralNotice and context, and
* may also include the campaign name or category.
*
* @param {string} key
* @param {KVStorageContext} context
* @return {string}
*/
function
makeKeyForLocalStorage
(
key
,
context
)
{
var
base
=
PREFIX
+
SEPARATOR
+
context
.
key
+
SEPARATOR
;
switch
(
context
.
key
)
{
case
kvStore
.
contexts
.
CAMPAIGN
.
key
:
return
base
+
campaignName
+
SEPARATOR
+
key
;
case
kvStore
.
contexts
.
CATEGORY
.
key
:
return
base
+
category
+
SEPARATOR
+
key
;
case
kvStore
.
contexts
.
GLOBAL
.
key
:
return
base
+
key
;
default
:
setError
(
'Invalid KV storage context'
,
key
,
null
,
context
);
return
base
+
'invalidContext'
+
SEPARATOR
+
key
;
}
}
/**
* Return the actual key to be used for a cookie (i.e., the cookie name)
* for the given key and context.
*
* Note: the key used in cookies contains the same information as the
* key used for localStorage, though the cookie key will be shorter.
*
* @param {string} key
* @param {KVStorageContext} context
* @return {string}
*/
function
makeKeyForCookie
(
key
,
context
)
{
var
base
=
PREFIX_IN_COOKIES
+
SEPARATOR_IN_COOKIES
+
context
.
keyInCookies
+
SEPARATOR_IN_COOKIES
;
switch
(
context
.
key
)
{
case
kvStore
.
contexts
.
CAMPAIGN
.
key
:
return
base
+
campaignName
+
SEPARATOR_IN_COOKIES
+
key
;
case
kvStore
.
contexts
.
CATEGORY
.
key
:
return
base
+
category
+
SEPARATOR_IN_COOKIES
+
key
;
case
kvStore
.
contexts
.
GLOBAL
.
key
:
return
base
+
key
;
default
:
setError
(
'Invalid KV storage context'
,
key
,
null
,
context
);
return
base
+
'invalidContext'
+
SEPARATOR_IN_COOKIES
+
key
;
}
}
function
setLocalStorageItem
(
key
,
value
,
context
,
ttl
)
{
var
lsKey
,
encodedWrappedValue
;
lsKey
=
makeKeyForLocalStorage
(
key
,
context
);
encodedWrappedValue
=
JSON
.
stringify
(
{
expiry
:
ttl
?
(
ttl
*
86400
)
+
now
:
DEFAULT_ITEM_TTL
+
now
,
val
:
value
}
);
// Write the value
try
{
localStorage
.
setItem
(
lsKey
,
encodedWrappedValue
);
// Check that it was written (it might not have been, if we're over
// the localStorage quota for this site, for example)
if
(
localStorage
.
getItem
(
lsKey
)
!==
encodedWrappedValue
)
{
setError
(
'Couldn\'t write value'
,
key
,
value
,
context
);
return
false
;
}
return
true
;
}
catch
(
e
)
{
setError
(
'Couldn\'t write value due to LocalStorage exception '
+
e
.
toString
(),
key
,
value
,
context
);
return
false
;
}
}
function
setCookieItem
(
key
,
value
,
context
,
ttl
)
{
return
Boolean
(
$
.
cookie
(
makeKeyForCookie
(
key
,
context
),
encodeURIComponent
(
JSON
.
stringify
(
value
)
),
{
expires
:
ttl
,
path
:
'/'
}
)
);
}
function
getLocalStorageItem
(
key
,
context
)
{
var
lsKey
=
makeKeyForLocalStorage
(
key
,
context
),
rawValue
,
wrappedValue
;
try
{
rawValue
=
localStorage
.
getItem
(
lsKey
);
}
catch
(
e
)
{
setError
(
'Couldn\'t read value due to LocalStorage exception '
+
e
.
toString
(),
key
,
null
,
context
);
return
null
;
}
if
(
rawValue
===
null
)
{
return
null
;
}
try
{
wrappedValue
=
JSON
.
parse
(
rawValue
);
}
catch
(
e
)
{
// FIXME: Consider detecting out-of-space errors and perform
// garbage-collection immediately, starting by removing the oldest
// expired (and unexpired) keys older than a certain threshold.
// If the JSON couldn't be parsed, log and return null (which is
// the same value we'd get if the key were not set).
if
(
e
instanceof
SyntaxError
)
{
setError
(
'Couldn\'t parse value, removing. '
+
e
.
message
,
key
,
rawValue
,
context
);
try
{
localStorage
.
removeItem
(
lsKey
);
}
catch
(
ex
)
{
setError
(
'Couldn\'t remove value due to LocalStorage exception '
+
ex
.
toString
(),
key
,
rawValue
,
context
);
}
return
null
;
// For any other errors, set and re-throw
}
else
{
setError
(
'Couldn\'t read value '
+
e
.
message
,
key
,
rawValue
,
context
);
throw
e
;
}
}
if
(
!
wrappedValue
.
expiry
||
wrappedValue
.
expiry
<
now
)
{
return
null
;
}
return
wrappedValue
.
val
;
}
function
getCookieItem
(
key
,
context
)
{
var
storageKey
=
makeKeyForCookie
(
key
,
context
),
rawCookie
=
$
.
cookie
(
storageKey
);
try
{
return
JSON
.
parse
(
decodeURIComponent
(
rawCookie
)
);
}
catch
(
e
)
{
// The cookie is probably corrupt. Remove.
$
.
removeCookie
(
storageKey
,
{
path
:
'/'
}
);
return
null
;
}
}
function
removeLocalStorageItem
(
key
,
context
)
{
try
{
localStorage
.
removeItem
(
makeKeyForLocalStorage
(
key
,
context
)
);
}
catch
(
e
)
{
setError
(
'Couldn\'t remove value due to LocalStorage exception '
+
e
.
toString
(),
key
,
null
,
context
);
}
}
function
removeCookieItem
(
key
,
context
)
{
$
.
removeCookie
(
makeKeyForCookie
(
key
,
context
),
{
path
:
'/'
}
);
}
/**
* Public API
*/
kvStore
=
{
/**
* Available key-value storage contexts
*
* @enum
* @readonly
*/
contexts
:
{
CAMPAIGN
:
new
KVStorageContext
(
'campaign'
,
'c'
),
CATEGORY
:
new
KVStorageContext
(
'category'
,
't'
),
GLOBAL
:
new
KVStorageContext
(
'global'
,
'g'
)
},
/**
* Options for storing data with a cookie or with the kvStore
* (LocalStorage).
*
* @enum
* @readonly
*/
multiStorageOptions
:
{
LOCAL_STORAGE
:
'kv_store'
,
COOKIE
:
'cookie'
,
NO_STORAGE
:
'no_storage'
},
/**
* Set the given value for the given key in the given context, using
* LocalStorage or a cookie. If the key already exists, its value will
* be overwritten.
*
* Value can be any type; will be json-encoded.
*
* Only when using LocalStorage: if the value was set, return true; if
* the value could not be set, we log the error via mw.log and return
* false. The error will be available via getError().
*
* Note: check isAvailable() before calling, or provide a
* multiStorageOption.
*
* Note: when using CAMPAIGN and CATEGORY contexts, ensure that you have
* set campaign and category, respectively. We don't check them here.
*
* @param {string} key
* @param {*} value
* @param {KVStorageContext} context
*
* @param {number} [ttl] Time to live for this item, in days; defaults
* to 1/2 a year. Null will trigger the default.
*
* @param {string} [multiStorageOption] A key from among
* kvStore.multiStorageOptions, to indicate how to store the item.
* Defaults to kvStore.multiStorageOptions.LOCAL_STORAGE.
*
* @return {boolean} true if the value could be set, false otherwise
*/
setItem
:
function
(
key
,
value
,
context
,
ttl
,
multiStorageOption
)
{
// Check validity of key
if
(
(
key
.
indexOf
(
SEPARATOR
)
!==
-
1
)
||
(
key
.
indexOf
(
SEPARATOR_IN_COOKIES
)
!==
-
1
)
)
{
setError
(
'Invalid key'
,
key
,
value
,
context
);
return
false
;
}
multiStorageOption
=
multiStorageOption
||
kvStore
.
multiStorageOptions
.
LOCAL_STORAGE
;
switch
(
multiStorageOption
)
{
case
kvStore
.
multiStorageOptions
.
LOCAL_STORAGE
:
return
setLocalStorageItem
(
key
,
value
,
context
,
ttl
);
case
kvStore
.
multiStorageOptions
.
COOKIE
:
return
setCookieItem
(
key
,
value
,
context
,
ttl
);
case
kvStore
.
multiStorageOptions
.
NO_STORAGE
:
return
false
;
default
:
throw
new
Error
(
'Unexpected multi-storage option'
);
}
},
/**
* Get the stored value for the given key in the given context.
*
* Note: check isAvailable() before calling.
*
* @param {string} key
* @param {KVStorageContext} context
* @param {string} [multiStorageOption] A key from among
* kvStore.multiStorageOptions, to indicate how to store the item.
* Defaults to kvStore.multiStorageOptions.LOCAL_STORAGE.
*/
getItem
:
function
(
key
,
context
,
multiStorageOption
)
{
multiStorageOption
=
multiStorageOption
||
kvStore
.
multiStorageOptions
.
LOCAL_STORAGE
;
switch
(
multiStorageOption
)
{
case
kvStore
.
multiStorageOptions
.
LOCAL_STORAGE
:
return
getLocalStorageItem
(
key
,
context
);
case
kvStore
.
multiStorageOptions
.
COOKIE
:
return
getCookieItem
(
key
,
context
);
case
kvStore
.
multiStorageOptions
.
NO_STORAGE
:
return
null
;
default
:
throw
new
Error
(
'Unexpected multi-storage option'
);
}
},
/**
* Remove the stored value for the given key in the given context
*
* Note: check isAvailable() before calling.
*
* @param {string} key
* @param {KVStorageContext} context
* @param {string} [multiStorageOption] A key from among
* kvStore.multiStorageOptions, to indicate how to store the item.
* Defaults to kvStore.multiStorageOptions.LOCAL_STORAGE.
*/
removeItem
:
function
(
key
,
context
,
multiStorageOption
)
{
multiStorageOption
=
multiStorageOption
||
kvStore
.
multiStorageOptions
.
LOCAL_STORAGE
;
switch
(
multiStorageOption
)
{
case
kvStore
.
multiStorageOptions
.
LOCAL_STORAGE
:
removeLocalStorageItem
(
key
,
context
);
return
;
case
kvStore
.
multiStorageOptions
.
COOKIE
:
removeCookieItem
(
key
,
context
);
return
;
case
kvStore
.
multiStorageOptions
.
NO_STORAGE
:
return
;
default
:
throw
new
Error
(
'Unexpected multi-storage option'
);
}
},
/**
* Convenience method to check for availability of storage without
* falling back to cookies.
*/
isAvailable
:
function
()
{
return
(
kvStore
.
getMultiStorageOption
(
false
)
!==
kvStore
.
multiStorageOptions
.
NO_STORAGE
);
},
/**
* Determine the appropriate multi-storage option
*
* @param {boolean} cookieAllowed
* @return {string} A string key
*/
getMultiStorageOption
:
function
(
cookieAllowed
)
{
if
(
isLocalStorageAvailable
()
)
{
return
kvStore
.
multiStorageOptions
.
LOCAL_STORAGE
;
}
if
(
cookieAllowed
&&
areCookiesEnabled
()
)
{
return
kvStore
.
multiStorageOptions
.
COOKIE
;
}
return
kvStore
.
multiStorageOptions
.
NO_STORAGE
;
},
/**
* If a KVStore error has occurred (during this page view), return an
* object with information about it. If no KVStore errors have occurred,
* return null.
*
* @return {?Object}
*/
getError
:
function
()
{
return
error
;
},
setNotAvailableError
:
function
()
{
setError
(
'LocalStorage not available.'
,
null
,
null
);
},
setMaintenanceError
:
function
(
lsKey
)
{
var
m
=
lsKey
.
match
(
FIND_KEY_REGEX
),
key
=
m
?
m
[
1
]
:
null
;
setError
(
'Error during KVStore maintenance.'
,
key
,
null
);
},
setCampaignName
:
function
(
cName
)
{
campaignName
=
cName
;
},
setBannerName
:
function
(
bName
)
{
bannerName
=
bName
;
},
setCategory
:
function
(
c
)
{
category
=
c
;
}
};
module
.
exports
=
kvStore
;
}()
);
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Wed, Aug 19, 00:45 (5 d, 8 h ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
79/8d/3fa1bbb5e885665cd24e33940222
Default Alt Text
kvStore.js (13 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment