deps,lib,src: add experimental web storage

This commit introduces an experimental implementation of the Web
Storage API using SQLite as the backing data store.

PR-URL: https://github.com/nodejs/node/pull/52435
Reviewed-By: Yagiz Nizipli <yagiz.nizipli@sentry.io>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ethan Arrowood <ethan@arrowood.dev>
This commit is contained in:
cjihrig 2023-09-15 10:32:14 -04:00 committed by Node.js GitHub Bot
parent 340cb6887d
commit 3d09e579d3
109 changed files with 275542 additions and 0 deletions

View File

@ -38,6 +38,7 @@ on:
- root-certificates
- simdjson
- simdutf
- sqlite
- undici
- uvwasi
- zlib
@ -272,6 +273,14 @@ jobs:
cat temp-output
tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true
rm temp-output
- id: sqlite
subsystem: deps
label: dependencies
run: |
./tools/dep_updaters/update-sqlite.sh > temp-output
cat temp-output
tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true
rm temp-output
- id: undici
subsystem: deps
label: dependencies

14
deps/sqlite/BUILD.gn vendored Normal file
View File

@ -0,0 +1,14 @@
##############################################################################
# #
# DO NOT EDIT THIS FILE! #
# #
##############################################################################
# This file is used by GN for building, which is NOT the build system used for
# building official binaries.
# Please modify the gyp files if you are making changes to build system.
import("unofficial.gni")
sqlite_gn_build("sqlite") {
}

24
deps/sqlite/sqlite.gyp vendored Normal file
View File

@ -0,0 +1,24 @@
{
'variables': {
'sqlite_sources': [
'sqlite3.c',
],
},
'targets': [
{
'target_name': 'sqlite',
'type': 'static_library',
'cflags': ['-fvisibility=hidden'],
'xcode_settings': {
'GCC_SYMBOLS_PRIVATE_EXTERN': 'YES', # -fvisibility=hidden
},
'include_dirs': ['.'],
'sources': [
'<@(sqlite_sources)',
],
'direct_dependent_settings': {
'include_dirs': ['.'],
},
},
],
}

257673
deps/sqlite/sqlite3.c vendored Normal file

File diff suppressed because it is too large Load Diff

13425
deps/sqlite/sqlite3.h vendored Normal file

File diff suppressed because it is too large Load Diff

22
deps/sqlite/unofficial.gni vendored Normal file
View File

@ -0,0 +1,22 @@
# This file is used by GN for building, which is NOT the build system used for
# building official binaries.
# Please edit the gyp files if you are making changes to build system.
# The actual configurations are put inside a template in unofficial.gni to
# prevent accidental edits from contributors.
template("sqlite_gn_build") {
config("sqlite_config") {
include_dirs = [ "." ]
}
gypi_values = exec_script("../../tools/gypi_to_gn.py",
[ rebase_path("sqlite.gyp") ],
"scope",
[ "sqlite.gyp" ])
source_set(target_name) {
forward_variables_from(invoker, "*")
public_configs = [ ":sqlite_config" ]
sources = gypi_values.sqlite_sources
}
}

View File

@ -1123,6 +1123,14 @@ added: v12.3.0
Enable experimental WebAssembly module support.
### `--experimental-webstorage`
<!-- YAML
added: REPLACEME
-->
Enable experimental [`Web Storage`][] support.
### `--force-context-aware`
<!-- YAML
@ -1491,6 +1499,17 @@ Disable [runtime allocation of executable memory][jitless]. This may be
required on some platforms for security reasons. It can also reduce attack
surface on other platforms, but the performance impact may be severe.
### `--localstorage-file=file`
<!-- YAML
added: REPLACEME
-->
The file used to store `localStorage` data. If the file does not exist, it is
created the first time `localStorage` is accessed. The same file may be shared
between multiple Node.js processes concurrently. This flag is a no-op unless
Node.js is started with the `--experimental-webstorage` flag.
### `--max-http-header-size=size`
<!-- YAML
@ -2833,6 +2852,7 @@ one is included in the list below.
* `--experimental-vm-modules`
* `--experimental-wasi-unstable-preview1`
* `--experimental-wasm-modules`
* `--experimental-webstorage`
* `--force-context-aware`
* `--force-fips`
* `--force-node-api-uncaught-exceptions-policy`
@ -2849,6 +2869,7 @@ one is included in the list below.
* `--inspect-publish-uid`
* `--inspect-wait`
* `--inspect`
* `--localstorage-file`
* `--max-http-header-size`
* `--napi-modules`
* `--network-family-autoselection-attempt-timeout`
@ -3376,6 +3397,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`NODE_OPTIONS`]: #node_optionsoptions
[`NO_COLOR`]: https://no-color.org
[`SlowBuffer`]: buffer.md#class-slowbuffer
[`Web Storage`]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API
[`WebSocket`]: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
[`YoungGenerationSizeFromSemiSpaceSize`]: https://chromium.googlesource.com/v8/v8.git/+/refs/tags/10.3.129/src/heap/heap.cc#328
[`dns.lookup()`]: dns.md#dnslookuphostname-options-callback

View File

@ -582,6 +582,19 @@ changes:
A browser-compatible implementation of {Headers}.
## `localStorage`
<!-- YAML
added: REPLACEME
-->
> Stability: 1.0 - Early development.
A browser-compatible implementation of [`localStorage`][]. Data is stored
unencrypted in the file specified by the [`--localstorage-file`][] CLI flag.
Any modification of this data outside of the Web Storage API is not supported.
Enable this API with the [`--experimental-webstorage`][] CLI flag.
## `MessageChannel`
<!-- YAML
@ -950,6 +963,19 @@ changes:
A browser-compatible implementation of {Request}.
## `sessionStorage`
<!-- YAML
added: REPLACEME
-->
> Stability: 1.0 - Early development.
A browser-compatible implementation of [`sessionStorage`][]. Data is stored in
memory, with a storage quota of 10 MB. Any modification of this data outside of
the Web Storage API is not supported. Enable this API with the
[`--experimental-webstorage`][] CLI flag.
## `setImmediate(callback[, ...args])`
<!-- YAML
@ -980,6 +1006,17 @@ added: v0.0.1
[`setTimeout`][] is described in the [timers][] section.
## Class: `Storage`
<!-- YAML
added: REPLACEME
-->
> Stability: 1.0 - Early development.
A browser-compatible implementation of [`Storage`][]. Enable this API with the
[`--experimental-webstorage`][] CLI flag.
## `structuredClone(value[, options])`
<!-- YAML
@ -1168,6 +1205,8 @@ A browser-compatible implementation of [`WritableStreamDefaultWriter`][].
[Navigator API]: https://html.spec.whatwg.org/multipage/system-state.html#the-navigator-object
[RFC 5646]: https://www.rfc-editor.org/rfc/rfc5646.txt
[Web Crypto API]: webcrypto.md
[`--experimental-webstorage`]: cli.md#--experimental-webstorage
[`--localstorage-file`]: cli.md#--localstorage-filefile
[`--no-experimental-global-navigator`]: cli.md#--no-experimental-global-navigator
[`--no-experimental-websocket`]: cli.md#--no-experimental-websocket
[`AbortController`]: https://developer.mozilla.org/en-US/docs/Web/API/AbortController
@ -1194,6 +1233,7 @@ A browser-compatible implementation of [`WritableStreamDefaultWriter`][].
[`ReadableStreamDefaultController`]: webstreams.md#class-readablestreamdefaultcontroller
[`ReadableStreamDefaultReader`]: webstreams.md#class-readablestreamdefaultreader
[`ReadableStream`]: webstreams.md#class-readablestream
[`Storage`]: https://developer.mozilla.org/en-US/docs/Web/API/Storage
[`TextDecoderStream`]: webstreams.md#class-textdecoderstream
[`TextDecoder`]: util.md#class-utiltextdecoder
[`TextEncoderStream`]: webstreams.md#class-textencoderstream
@ -1218,11 +1258,13 @@ A browser-compatible implementation of [`WritableStreamDefaultWriter`][].
[`exports`]: modules.md#exports
[`fetch()`]: https://developer.mozilla.org/en-US/docs/Web/API/fetch
[`globalThis`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis
[`localStorage`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
[`module`]: modules.md#module
[`perf_hooks.performance`]: perf_hooks.md#perf_hooksperformance
[`process.nextTick()`]: process.md#processnexttickcallback-args
[`process` object]: process.md#process
[`require()`]: modules.md#requireid
[`sessionStorage`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage
[`setImmediate`]: timers.md#setimmediatecallback-args
[`setInterval`]: timers.md#setintervalcallback-delay-args
[`setTimeout`]: timers.md#settimeoutcallback-delay-args

View File

@ -3970,6 +3970,7 @@ Will generate an object similar to:
openssl: '3.0.13+quic',
simdjson: '3.8.0',
simdutf: '5.2.4',
sqlite: '3.46.0',
tz: '2024a',
undici: '6.13.0',
unicode: '15.1',

View File

@ -29,6 +29,7 @@ This a list of all the dependencies:
* [postject][]
* [simdjson][]
* [simdutf][]
* [sqlite][]
* [undici][]
* [uvwasi][]
* [V8][]
@ -290,6 +291,11 @@ a C++ library for fast JSON parsing.
The [simdutf](https://github.com/simdutf/simdutf) dependency is
a C++ library for fast UTF-8 decoding and encoding.
### sqlite
The [sqlite](https://github.com/sqlite/sqlite) dependency is
an embedded SQL database engine written in C.
### undici
The [undici](https://github.com/nodejs/undici) dependency is an HTTP/1.1 client,
@ -345,6 +351,7 @@ performance improvements not currently available in standard zlib.
[postject]: #postject
[simdjson]: #simdjson
[simdutf]: #simdutf
[sqlite]: #sqlite
[undici]: #undici
[update-openssl-action]: ../../../.github/workflows/update-openssl.yml
[uvwasi]: #uvwasi

View File

@ -200,6 +200,9 @@ Enable experimental support for the EventSource Web API.
.It Fl -no-experimental-websocket
Disable experimental support for the WebSocket API.
.
.It Fl -experimental-webstorage
Enable experimental support for the Web Storage API.
.
.It Fl -no-experimental-repl-await
Disable top-level await keyword support in REPL.
.
@ -313,6 +316,9 @@ other platforms, but the performance impact may be severe.
This flag is inherited from V8 and is subject to change upstream. It may
disappear in a non-semver-major release.
.
.It Fl -localstorage-file Ns = Ns Ar file
The file used to store localStorage data.
.
.It Fl -max-http-header-size Ns = Ns Ar size
Specify the maximum size of HTTP headers in bytes. Defaults to 16 KiB.
.

View File

@ -100,6 +100,7 @@ function prepareExecution(options) {
setupInspectorHooks();
setupNavigator();
setupWarningHandler();
setupWebStorage();
setupWebsocket();
setupEventsource();
setupCodeCoverage();
@ -328,6 +329,19 @@ function setupNavigator() {
defineReplaceableLazyAttribute(globalThis, 'internal/navigator', ['navigator'], false);
}
function setupWebStorage() {
if (getEmbedderOptions().noBrowserGlobals ||
!getOptionValue('--experimental-webstorage')) {
return;
}
// https://html.spec.whatwg.org/multipage/webstorage.html#webstorage
exposeLazyInterfaces(globalThis, 'internal/webstorage', ['Storage']);
defineReplaceableLazyAttribute(globalThis, 'internal/webstorage', [
'localStorage', 'sessionStorage',
]);
}
function setupCodeCoverage() {
// Resolve the coverage directory to an absolute path, and
// overwrite process.env so that the original path gets passed

View File

@ -0,0 +1,55 @@
'use strict';
const {
ObjectDefineProperties,
} = primordials;
const { ERR_INVALID_ARG_VALUE } = require('internal/errors').codes;
const { getOptionValue } = require('internal/options');
const { emitExperimentalWarning } = require('internal/util');
const { kConstructorKey, Storage } = internalBinding('webstorage');
const { resolve, toNamespacedPath } = require('path');
const { getValidatedPath } = require('internal/fs/utils');
const kInMemoryPath = ':memory:';
emitExperimentalWarning('Web Storage');
module.exports = { Storage };
let lazyLocalStorage;
let lazySessionStorage;
ObjectDefineProperties(module.exports, {
__proto__: null,
localStorage: {
__proto__: null,
configurable: true,
enumerable: true,
get() {
if (lazyLocalStorage === undefined) {
let location = getOptionValue('--localstorage-file');
if (location === '') {
throw new ERR_INVALID_ARG_VALUE('--localstorage-file',
location,
'is an invalid localStorage location');
}
location = toNamespacedPath(resolve(getValidatedPath(location)));
lazyLocalStorage = new Storage(kConstructorKey, location);
}
return lazyLocalStorage;
},
},
sessionStorage: {
__proto__: null,
configurable: true,
enumerable: true,
get() {
if (lazySessionStorage === undefined) {
lazySessionStorage = new Storage(kConstructorKey, kInMemoryPath);
}
return lazySessionStorage;
},
},
});

View File

@ -146,6 +146,7 @@
'src/node_wasi.cc',
'src/node_wasm_web_api.cc',
'src/node_watchdog.cc',
'src/node_webstorage.cc',
'src/node_worker.cc',
'src/node_zlib.cc',
'src/path.cc',
@ -272,6 +273,7 @@
'src/node_v8_platform-inl.h',
'src/node_wasi.h',
'src/node_watchdog.h',
'src/node_webstorage.h',
'src/node_worker.h',
'src/path.h',
'src/permission/child_process_permission.h',
@ -544,6 +546,7 @@
'dependencies': [
'deps/histogram/histogram.gyp:histogram',
'deps/sqlite/sqlite.gyp:sqlite',
],
'msvs_settings': {
@ -838,6 +841,7 @@
'dependencies': [
'deps/googletest/googletest.gyp:gtest_prod',
'deps/histogram/histogram.gyp:histogram',
'deps/sqlite/sqlite.gyp:sqlite',
'deps/simdjson/simdjson.gyp:simdjson',
'deps/simdutf/simdutf.gyp:simdutf',
'deps/ada/ada.gyp:ada',
@ -1022,6 +1026,7 @@
'dependencies': [
'<(node_lib_target_name)',
'deps/histogram/histogram.gyp:histogram',
'deps/sqlite/sqlite.gyp:sqlite',
],
'includes': [
@ -1033,6 +1038,7 @@
'deps/v8/include',
'deps/cares/include',
'deps/uv/include',
'deps/sqlite',
'test/cctest',
],
@ -1065,6 +1071,7 @@
'dependencies': [
'<(node_lib_target_name)',
'deps/histogram/histogram.gyp:histogram',
'deps/sqlite/sqlite.gyp:sqlite',
'deps/uvwasi/uvwasi.gyp:uvwasi',
],
'includes': [
@ -1075,6 +1082,7 @@
'tools/msvs/genfiles',
'deps/v8/include',
'deps/cares/include',
'deps/sqlite',
'deps/uv/include',
'deps/uvwasi/include',
'test/cctest',
@ -1109,6 +1117,7 @@
'<(node_lib_target_name)',
'deps/googletest/googletest.gyp:gtest_prod',
'deps/histogram/histogram.gyp:histogram',
'deps/sqlite/sqlite.gyp:sqlite',
'deps/uvwasi/uvwasi.gyp:uvwasi',
'deps/ada/ada.gyp:ada',
],
@ -1120,6 +1129,7 @@
'tools/msvs/genfiles',
'deps/v8/include',
'deps/cares/include',
'deps/sqlite',
'deps/uv/include',
'deps/uvwasi/include',
'test/cctest',
@ -1156,6 +1166,7 @@
'deps/googletest/googletest.gyp:gtest',
'deps/googletest/googletest.gyp:gtest_main',
'deps/histogram/histogram.gyp:histogram',
'deps/sqlite/sqlite.gyp:sqlite',
'deps/simdjson/simdjson.gyp:simdjson',
'deps/simdutf/simdutf.gyp:simdutf',
'deps/ada/ada.gyp:ada',
@ -1171,6 +1182,7 @@
'deps/v8/include',
'deps/cares/include',
'deps/uv/include',
'deps/sqlite',
'test/cctest',
],
@ -1232,6 +1244,7 @@
'dependencies': [
'<(node_lib_target_name)',
'deps/histogram/histogram.gyp:histogram',
'deps/sqlite/sqlite.gyp:sqlite',
'deps/ada/ada.gyp:ada',
],
@ -1246,6 +1259,7 @@
'deps/v8/include',
'deps/cares/include',
'deps/uv/include',
'deps/sqlite',
'test/embedding',
],
@ -1345,6 +1359,7 @@
'dependencies': [
'<(node_lib_target_name)',
'deps/histogram/histogram.gyp:histogram',
'deps/sqlite/sqlite.gyp:sqlite',
'deps/ada/ada.gyp:ada',
'deps/simdjson/simdjson.gyp:simdjson',
'deps/simdutf/simdutf.gyp:simdutf',
@ -1360,6 +1375,7 @@
'deps/v8/include',
'deps/cares/include',
'deps/uv/include',
'deps/sqlite',
],
'defines': [ 'NODE_WANT_INTERNALS=1' ],

View File

@ -43,6 +43,7 @@
#define PER_ISOLATE_SYMBOL_PROPERTIES(V) \
V(fs_use_promises_symbol, "fs_use_promises_symbol") \
V(async_id_symbol, "async_id_symbol") \
V(constructor_key_symbol, "constructor_key_symbol") \
V(handle_onclose_symbol, "handle_onclose") \
V(no_message_symbol, "no_message_symbol") \
V(messaging_deserialize_symbol, "messaging_deserialize_symbol") \

View File

@ -85,6 +85,7 @@
V(wasi) \
V(wasm_web_api) \
V(watchdog) \
V(webstorage) \
V(worker) \
V(zlib)

View File

@ -10,6 +10,7 @@
#include "node.h"
#include "simdjson.h"
#include "simdutf.h"
#include "sqlite3.h"
#include "undici_version.h"
#include "util.h"
#include "uv.h"
@ -131,6 +132,7 @@ Metadata::Versions::Versions() {
simdjson = SIMDJSON_VERSION;
simdutf = SIMDUTF_VERSION;
sqlite = SQLITE_VERSION;
ada = ADA_VERSION;
}

View File

@ -48,6 +48,7 @@ namespace node {
V(acorn) \
V(simdjson) \
V(simdutf) \
V(sqlite) \
V(ada) \
NODE_VERSIONS_KEY_UNDICI(V) \
V(cjs_module_lexer) \

View File

@ -410,6 +410,14 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
kAllowedInEnvvar,
true);
AddOption("--experimental-global-customevent", "", NoOp{}, kAllowedInEnvvar);
AddOption("--experimental-webstorage",
"experimental Web Storage API",
&EnvironmentOptions::experimental_webstorage,
kAllowedInEnvvar);
AddOption("--localstorage-file",
"file used to persist localStorage data",
&EnvironmentOptions::localstorage_file,
kAllowedInEnvvar);
AddOption("--experimental-global-navigator",
"expose experimental Navigator API on the global scope",
&EnvironmentOptions::experimental_global_navigator,

View File

@ -118,6 +118,8 @@ class EnvironmentOptions : public Options {
bool experimental_eventsource = false;
bool experimental_fetch = true;
bool experimental_websocket = true;
bool experimental_webstorage = false;
std::string localstorage_file;
bool experimental_global_navigator = true;
bool experimental_global_web_crypto = true;
bool experimental_https_modules = false;

706
src/node_webstorage.cc Normal file
View File

@ -0,0 +1,706 @@
#include "node_webstorage.h"
#include "base_object-inl.h"
#include "debug_utils-inl.h"
#include "env-inl.h"
#include "memory_tracker-inl.h"
#include "node.h"
#include "node_errors.h"
#include "node_mem-inl.h"
#include "sqlite3.h"
#include "util-inl.h"
namespace node {
namespace webstorage {
using v8::Array;
using v8::Boolean;
using v8::Context;
using v8::DontDelete;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::IndexedPropertyHandlerConfiguration;
using v8::Integer;
using v8::Intercepted;
using v8::Isolate;
using v8::Local;
using v8::Map;
using v8::Maybe;
using v8::MaybeLocal;
using v8::Name;
using v8::NamedPropertyHandlerConfiguration;
using v8::Null;
using v8::Object;
using v8::PropertyAttribute;
using v8::PropertyCallbackInfo;
using v8::PropertyDescriptor;
using v8::PropertyHandlerFlags;
using v8::String;
using v8::Uint32;
using v8::Value;
#define THROW_SQLITE_ERROR(env, r) \
node::THROW_ERR_INVALID_STATE((env), sqlite3_errstr((r)))
#define CHECK_ERROR_OR_THROW(env, expr, expected, ret) \
do { \
int r_ = (expr); \
if (r_ != (expected)) { \
THROW_SQLITE_ERROR((env), r_); \
return (ret); \
} \
} while (0)
static void ThrowQuotaExceededException(Local<Context> context) {
Isolate* isolate = context->GetIsolate();
auto dom_exception_str = FIXED_ONE_BYTE_STRING(isolate, "DOMException");
auto err_name = FIXED_ONE_BYTE_STRING(isolate, "QuotaExceededError");
auto err_message =
FIXED_ONE_BYTE_STRING(isolate, "Setting the value exceeded the quota");
Local<Object> per_context_bindings;
Local<Value> domexception_ctor_val;
if (!GetPerContextExports(context).ToLocal(&per_context_bindings) ||
!per_context_bindings->Get(context, dom_exception_str)
.ToLocal(&domexception_ctor_val)) {
return;
}
CHECK(domexception_ctor_val->IsFunction());
Local<Function> domexception_ctor = domexception_ctor_val.As<Function>();
Local<Value> argv[] = {err_message, err_name};
Local<Value> exception;
if (!domexception_ctor->NewInstance(context, arraysize(argv), argv)
.ToLocal(&exception)) {
return;
}
isolate->ThrowException(exception);
}
Storage::Storage(Environment* env, Local<Object> object, Local<String> location)
: BaseObject(env, object) {
MakeWeak();
node::Utf8Value utf8_location(env->isolate(), location);
symbols_.Reset(env->isolate(), Map::New(env->isolate()));
db_ = nullptr;
location_ = utf8_location.ToString();
}
Storage::~Storage() {
db_ = nullptr;
}
void Storage::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("symbols", symbols_);
tracker->TrackField("location", location_);
}
bool Storage::Open() {
static const int kCurrentSchemaVersion = 1;
static const char get_schema_version_sql[] =
"SELECT schema_version FROM nodejs_webstorage_state";
static const char init_sql_v0[] =
"PRAGMA encoding = 'UTF-16le';"
"PRAGMA busy_timeout = 3000;"
"PRAGMA journal_mode = WAL;"
"PRAGMA synchronous = NORMAL;"
"PRAGMA temp_store = memory;"
"PRAGMA optimize;"
""
"CREATE TABLE IF NOT EXISTS nodejs_webstorage("
" key BLOB NOT NULL,"
" value BLOB NOT NULL,"
" PRIMARY KEY(key)"
") STRICT;"
""
"CREATE TABLE IF NOT EXISTS nodejs_webstorage_state("
// max_size is 10MB. This can be made configurable in the future.
" max_size INTEGER NOT NULL DEFAULT 10485760,"
" total_size INTEGER NOT NULL,"
" schema_version INTEGER NOT NULL DEFAULT 0,"
" single_row_ INTEGER NOT NULL DEFAULT 1 CHECK(single_row_ = 1),"
" PRIMARY KEY(single_row_)"
") STRICT;"
""
"CREATE TRIGGER IF NOT EXISTS nodejs_quota_insert "
"AFTER INSERT ON nodejs_webstorage "
"FOR EACH ROW "
"BEGIN "
" UPDATE nodejs_webstorage_state"
" SET total_size = total_size + OCTET_LENGTH(NEW.key) +"
" OCTET_LENGTH(NEW.value);"
" SELECT RAISE(ABORT, 'QuotaExceeded') WHERE EXISTS ("
" SELECT 1 FROM nodejs_webstorage_state WHERE total_size > max_size"
" );"
"END;"
""
"CREATE TRIGGER IF NOT EXISTS nodejs_quota_update "
"AFTER UPDATE ON nodejs_webstorage "
"FOR EACH ROW "
"BEGIN "
" UPDATE nodejs_webstorage_state"
" SET total_size = total_size + "
" ((OCTET_LENGTH(NEW.key) + OCTET_LENGTH(NEW.value)) -"
" (OCTET_LENGTH(OLD.key) + OCTET_LENGTH(OLD.value)));"
" SELECT RAISE(ABORT, 'QuotaExceeded') WHERE EXISTS ("
" SELECT 1 FROM nodejs_webstorage_state WHERE total_size > max_size"
" );"
"END;"
""
"CREATE TRIGGER IF NOT EXISTS nodejs_quota_delete "
"AFTER DELETE ON nodejs_webstorage "
"FOR EACH ROW "
"BEGIN "
" UPDATE nodejs_webstorage_state"
" SET total_size = total_size - (OCTET_LENGTH(OLD.key) +"
" OCTET_LENGTH(OLD.value));"
"END;"
""
"INSERT OR IGNORE INTO nodejs_webstorage_state (total_size) VALUES (0);";
sqlite3* db = db_.get();
if (db != nullptr) {
return true;
}
int r = sqlite3_open(location_.c_str(), &db);
CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, false);
r = sqlite3_exec(db, init_sql_v0, 0, 0, nullptr);
CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, false);
// Get the current schema version, used to determine schema migrations.
sqlite3_stmt* s = nullptr;
r = sqlite3_prepare_v2(db, get_schema_version_sql, -1, &s, 0);
r = sqlite3_exec(db, init_sql_v0, 0, 0, nullptr);
CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, false);
auto stmt = stmt_unique_ptr(s);
CHECK_ERROR_OR_THROW(env(), sqlite3_step(stmt.get()), SQLITE_ROW, false);
CHECK(sqlite3_column_type(stmt.get(), 0) == SQLITE_INTEGER);
int schema_version = sqlite3_column_int(stmt.get(), 0);
stmt = nullptr; // Force finalization.
if (schema_version > kCurrentSchemaVersion) {
node::THROW_ERR_INVALID_STATE(
env(), "localStorage was created with a newer version of Node.js");
return false;
}
if (schema_version < kCurrentSchemaVersion) {
// Run any migrations and update the schema version.
std::string set_user_version_sql =
"UPDATE nodejs_webstorage_state SET schema_version = " +
std::to_string(kCurrentSchemaVersion) + ";";
r = sqlite3_exec(db, set_user_version_sql.c_str(), 0, 0, nullptr);
CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, false);
}
db_ = conn_unique_ptr(db);
return true;
}
void Storage::New(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Realm* realm = Realm::GetCurrent(args);
if (!args[0]->StrictEquals(realm->isolate_data()->constructor_key_symbol())) {
THROW_ERR_ILLEGAL_CONSTRUCTOR(env);
return;
}
CHECK(args.IsConstructCall());
CHECK(args[1]->IsString());
new Storage(env, args.This(), args[1].As<String>());
}
void Storage::Clear() {
if (!Open()) {
return;
}
static const char sql[] = "DELETE FROM nodejs_webstorage";
sqlite3_stmt* s = nullptr;
CHECK_ERROR_OR_THROW(
env(), sqlite3_prepare_v2(db_.get(), sql, -1, &s, 0), SQLITE_OK, void());
auto stmt = stmt_unique_ptr(s);
CHECK_ERROR_OR_THROW(env(), sqlite3_step(stmt.get()), SQLITE_DONE, void());
}
Local<Array> Storage::Enumerate() {
if (!Open()) {
return Local<Array>();
}
static const char sql[] = "SELECT key FROM nodejs_webstorage";
sqlite3_stmt* s = nullptr;
int r = sqlite3_prepare_v2(db_.get(), sql, -1, &s, 0);
CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, Local<Array>());
auto stmt = stmt_unique_ptr(s);
std::vector<Local<Value>> values;
while ((r = sqlite3_step(stmt.get())) == SQLITE_ROW) {
CHECK(sqlite3_column_type(stmt.get(), 0) == SQLITE_BLOB);
auto size = sqlite3_column_bytes(stmt.get(), 0) / sizeof(uint16_t);
values.emplace_back(
String::NewFromTwoByte(env()->isolate(),
reinterpret_cast<const uint16_t*>(
sqlite3_column_blob(stmt.get(), 0)),
v8::NewStringType::kNormal,
size)
.ToLocalChecked());
}
CHECK_ERROR_OR_THROW(env(), r, SQLITE_DONE, Local<Array>());
return Array::New(env()->isolate(), values.data(), values.size());
}
Local<Value> Storage::Length() {
if (!Open()) {
return Local<Value>();
}
static const char sql[] = "SELECT count(*) FROM nodejs_webstorage";
sqlite3_stmt* s = nullptr;
int r = sqlite3_prepare_v2(db_.get(), sql, -1, &s, 0);
CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, Local<Value>());
auto stmt = stmt_unique_ptr(s);
CHECK_ERROR_OR_THROW(
env(), sqlite3_step(stmt.get()), SQLITE_ROW, Local<Value>());
CHECK(sqlite3_column_type(stmt.get(), 0) == SQLITE_INTEGER);
int result = sqlite3_column_int(stmt.get(), 0);
return Integer::New(env()->isolate(), result);
}
Local<Value> Storage::Load(Local<Name> key) {
if (key->IsSymbol()) {
auto symbol_map = symbols_.Get(env()->isolate());
MaybeLocal<Value> result = symbol_map->Get(env()->context(), key);
return result.FromMaybe(Local<Value>());
}
if (!Open()) {
return Local<Value>();
}
static const char sql[] =
"SELECT value FROM nodejs_webstorage WHERE key = ? LIMIT 1";
sqlite3_stmt* s = nullptr;
int r = sqlite3_prepare_v2(db_.get(), sql, -1, &s, 0);
CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, Local<Value>());
auto stmt = stmt_unique_ptr(s);
node::TwoByteValue utf16key(env()->isolate(), key);
auto key_size = utf16key.length() * sizeof(uint16_t);
r = sqlite3_bind_blob(stmt.get(), 1, utf16key.out(), key_size, SQLITE_STATIC);
CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, Local<Value>());
auto value = Local<Value>();
r = sqlite3_step(stmt.get());
if (r == SQLITE_ROW) {
CHECK(sqlite3_column_type(stmt.get(), 0) == SQLITE_BLOB);
auto size = sqlite3_column_bytes(stmt.get(), 0) / sizeof(uint16_t);
value = String::NewFromTwoByte(env()->isolate(),
reinterpret_cast<const uint16_t*>(
sqlite3_column_blob(stmt.get(), 0)),
v8::NewStringType::kNormal,
size)
.ToLocalChecked();
} else if (r != SQLITE_DONE) {
THROW_SQLITE_ERROR(env(), r);
}
return value;
}
Local<Value> Storage::LoadKey(const int index) {
if (!Open()) {
return Local<Value>();
}
static const char sql[] =
"SELECT key FROM nodejs_webstorage LIMIT 1 OFFSET ?";
sqlite3_stmt* s = nullptr;
int r = sqlite3_prepare_v2(db_.get(), sql, -1, &s, 0);
CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, Local<Value>());
auto stmt = stmt_unique_ptr(s);
r = sqlite3_bind_int(stmt.get(), 1, index);
CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, Local<Value>());
auto value = Local<Value>();
r = sqlite3_step(stmt.get());
if (r == SQLITE_ROW) {
CHECK(sqlite3_column_type(stmt.get(), 0) == SQLITE_BLOB);
auto size = sqlite3_column_bytes(stmt.get(), 0) / sizeof(uint16_t);
value = String::NewFromTwoByte(env()->isolate(),
reinterpret_cast<const uint16_t*>(
sqlite3_column_blob(stmt.get(), 0)),
v8::NewStringType::kNormal,
size)
.ToLocalChecked();
} else if (r != SQLITE_DONE) {
THROW_SQLITE_ERROR(env(), r);
}
return value;
}
bool Storage::Remove(Local<Name> key) {
if (key->IsSymbol()) {
auto symbol_map = symbols_.Get(env()->isolate());
Maybe<bool> result = symbol_map->Delete(env()->context(), key);
return !result.IsNothing();
}
if (!Open()) {
return false;
}
static const char sql[] = "DELETE FROM nodejs_webstorage WHERE key = ?";
sqlite3_stmt* s = nullptr;
int r = sqlite3_prepare_v2(db_.get(), sql, -1, &s, 0);
CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, false);
auto stmt = stmt_unique_ptr(s);
node::TwoByteValue utf16key(env()->isolate(), key);
auto key_size = utf16key.length() * sizeof(uint16_t);
r = sqlite3_bind_blob(stmt.get(), 1, utf16key.out(), key_size, SQLITE_STATIC);
CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, false);
CHECK_ERROR_OR_THROW(env(), sqlite3_step(stmt.get()), SQLITE_DONE, false);
return true;
}
bool Storage::Store(Local<Name> key, Local<Value> value) {
if (key->IsSymbol()) {
auto symbol_map = symbols_.Get(env()->isolate());
MaybeLocal<Map> result = symbol_map->Set(env()->context(), key, value);
return !result.IsEmpty();
}
Local<String> val;
if (!value->ToString(env()->context()).ToLocal(&val)) {
return false;
}
if (!Open()) {
return false;
}
static const char sql[] =
"INSERT INTO nodejs_webstorage (key, value) VALUES (?, ?)"
" ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value"
" WHERE EXCLUDED.key = key";
sqlite3_stmt* s = nullptr;
node::TwoByteValue utf16key(env()->isolate(), key);
node::TwoByteValue utf16val(env()->isolate(), val);
int r = sqlite3_prepare_v2(db_.get(), sql, -1, &s, 0);
CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, false);
auto stmt = stmt_unique_ptr(s);
auto key_size = utf16key.length() * sizeof(uint16_t);
r = sqlite3_bind_blob(stmt.get(), 1, utf16key.out(), key_size, SQLITE_STATIC);
CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, false);
auto val_size = utf16val.length() * sizeof(uint16_t);
r = sqlite3_bind_blob(stmt.get(), 2, utf16val.out(), val_size, SQLITE_STATIC);
CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, false);
r = sqlite3_step(stmt.get());
if (r == SQLITE_CONSTRAINT) {
ThrowQuotaExceededException(env()->context());
return false;
}
CHECK_ERROR_OR_THROW(env(), r, SQLITE_DONE, false);
return true;
}
static Local<Name> Uint32ToName(Local<Context> context, uint32_t index) {
return Uint32::New(context->GetIsolate(), index)
->ToString(context)
.ToLocalChecked();
}
static void Clear(const FunctionCallbackInfo<Value>& info) {
Storage* storage;
ASSIGN_OR_RETURN_UNWRAP(&storage, info.Holder());
storage->Clear();
}
static void GetItem(const FunctionCallbackInfo<Value>& info) {
Storage* storage;
ASSIGN_OR_RETURN_UNWRAP(&storage, info.Holder());
Environment* env = Environment::GetCurrent(info);
if (info.Length() < 1) {
return THROW_ERR_MISSING_ARGS(
env, "Failed to execute 'getItem' on 'Storage': 1 argument required");
}
Local<String> prop;
if (!info[0]->ToString(env->context()).ToLocal(&prop)) {
return;
}
Local<Value> result = storage->Load(prop);
if (result.IsEmpty()) {
info.GetReturnValue().Set(Null(env->isolate()));
} else {
info.GetReturnValue().Set(result);
}
}
static void Key(const FunctionCallbackInfo<Value>& info) {
Storage* storage;
ASSIGN_OR_RETURN_UNWRAP(&storage, info.Holder());
Environment* env = Environment::GetCurrent(info);
int index;
if (info.Length() < 1) {
return THROW_ERR_MISSING_ARGS(
env, "Failed to execute 'key' on 'Storage': 1 argument required");
}
if (!info[0]->Int32Value(env->context()).To(&index)) {
return;
}
if (index < 0) {
info.GetReturnValue().Set(Null(env->isolate()));
return;
}
Local<Value> result = storage->LoadKey(index);
if (result.IsEmpty()) {
info.GetReturnValue().Set(Null(env->isolate()));
} else {
info.GetReturnValue().Set(result);
}
}
static void RemoveItem(const FunctionCallbackInfo<Value>& info) {
Storage* storage;
ASSIGN_OR_RETURN_UNWRAP(&storage, info.Holder());
Environment* env = Environment::GetCurrent(info);
Local<String> prop;
if (info.Length() < 1) {
return THROW_ERR_MISSING_ARGS(
env,
"Failed to execute 'removeItem' on 'Storage': 1 argument required");
}
if (!info[0]->ToString(env->context()).ToLocal(&prop)) {
return;
}
storage->Remove(prop);
}
static void SetItem(const FunctionCallbackInfo<Value>& info) {
Storage* storage;
ASSIGN_OR_RETURN_UNWRAP(&storage, info.Holder());
Environment* env = Environment::GetCurrent(info);
if (info.Length() < 2) {
return THROW_ERR_MISSING_ARGS(
env, "Failed to execute 'setItem' on 'Storage': 2 arguments required");
}
Local<String> prop;
if (!info[0]->ToString(env->context()).ToLocal(&prop)) {
return;
}
storage->Store(prop, info[1]);
}
template <typename T>
static bool ShouldIntercept(Local<Name> property,
const PropertyCallbackInfo<T>& info) {
Environment* env = Environment::GetCurrent(info);
Local<Value> proto = info.Holder()->GetPrototype();
if (proto->IsObject()) {
bool has_prop;
if (!proto.As<Object>()->Has(env->context(), property).To(&has_prop)) {
return false;
}
if (has_prop) {
return false;
}
}
return true;
}
static Intercepted StorageGetter(Local<Name> property,
const PropertyCallbackInfo<Value>& info) {
if (!ShouldIntercept(property, info)) {
return Intercepted::kNo;
}
Storage* storage;
ASSIGN_OR_RETURN_UNWRAP(&storage, info.Holder(), Intercepted::kNo);
Local<Value> result = storage->Load(property);
if (result.IsEmpty()) {
info.GetReturnValue().SetUndefined();
} else {
info.GetReturnValue().Set(result);
}
return Intercepted::kYes;
}
static Intercepted StorageSetter(Local<Name> property,
Local<Value> value,
const PropertyCallbackInfo<void>& info) {
Storage* storage;
ASSIGN_OR_RETURN_UNWRAP(&storage, info.Holder(), Intercepted::kNo);
if (storage->Store(property, value)) {
info.GetReturnValue().Set(value);
}
return Intercepted::kYes;
}
static Intercepted StorageQuery(Local<Name> property,
const PropertyCallbackInfo<Integer>& info) {
if (!ShouldIntercept(property, info)) {
return Intercepted::kNo;
}
Storage* storage;
ASSIGN_OR_RETURN_UNWRAP(&storage, info.Holder(), Intercepted::kNo);
Local<Value> result = storage->Load(property);
if (result.IsEmpty()) {
return Intercepted::kNo;
}
info.GetReturnValue().Set(0);
return Intercepted::kYes;
}
static Intercepted StorageDeleter(Local<Name> property,
const PropertyCallbackInfo<Boolean>& info) {
Storage* storage;
ASSIGN_OR_RETURN_UNWRAP(&storage, info.Holder(), Intercepted::kNo);
if (storage->Remove(property)) {
info.GetReturnValue().Set(true);
}
return Intercepted::kYes;
}
static void StorageEnumerator(const PropertyCallbackInfo<Array>& info) {
Storage* storage;
ASSIGN_OR_RETURN_UNWRAP(&storage, info.Holder());
info.GetReturnValue().Set(storage->Enumerate());
}
static Intercepted StorageDefiner(Local<Name> property,
const PropertyDescriptor& desc,
const PropertyCallbackInfo<void>& info) {
Storage* storage;
ASSIGN_OR_RETURN_UNWRAP(&storage, info.Holder(), Intercepted::kNo);
if (desc.has_value()) {
return StorageSetter(property, desc.value(), info);
}
return Intercepted::kYes;
}
static Intercepted IndexedGetter(uint32_t index,
const PropertyCallbackInfo<Value>& info) {
Environment* env = Environment::GetCurrent(info);
return StorageGetter(Uint32ToName(env->context(), index), info);
}
static Intercepted IndexedSetter(uint32_t index,
Local<Value> value,
const PropertyCallbackInfo<void>& info) {
Environment* env = Environment::GetCurrent(info);
return StorageSetter(Uint32ToName(env->context(), index), value, info);
}
static Intercepted IndexedQuery(uint32_t index,
const PropertyCallbackInfo<Integer>& info) {
Environment* env = Environment::GetCurrent(info);
return StorageQuery(Uint32ToName(env->context(), index), info);
}
static Intercepted IndexedDeleter(uint32_t index,
const PropertyCallbackInfo<Boolean>& info) {
Environment* env = Environment::GetCurrent(info);
return StorageDeleter(Uint32ToName(env->context(), index), info);
}
static Intercepted IndexedDefiner(uint32_t index,
const PropertyDescriptor& desc,
const PropertyCallbackInfo<void>& info) {
Environment* env = Environment::GetCurrent(info);
return StorageDefiner(Uint32ToName(env->context(), index), desc, info);
}
static void StorageLengthGetter(const FunctionCallbackInfo<Value>& info) {
Storage* storage;
ASSIGN_OR_RETURN_UNWRAP(&storage, info.This());
info.GetReturnValue().Set(storage->Length());
}
static void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
Environment* env = Environment::GetCurrent(context);
Isolate* isolate = env->isolate();
auto ctor_tmpl = NewFunctionTemplate(isolate, Storage::New);
auto inst_tmpl = ctor_tmpl->InstanceTemplate();
inst_tmpl->SetInternalFieldCount(Storage::kInternalFieldCount);
inst_tmpl->SetHandler(NamedPropertyHandlerConfiguration(
StorageGetter,
StorageSetter,
StorageQuery,
StorageDeleter,
StorageEnumerator,
StorageDefiner,
nullptr,
Local<Value>(),
PropertyHandlerFlags::kHasNoSideEffect));
inst_tmpl->SetHandler(IndexedPropertyHandlerConfiguration(
IndexedGetter,
IndexedSetter,
IndexedQuery,
IndexedDeleter,
nullptr,
IndexedDefiner,
nullptr,
Local<Value>(),
PropertyHandlerFlags::kHasNoSideEffect));
Local<FunctionTemplate> length_getter =
FunctionTemplate::New(isolate, StorageLengthGetter);
ctor_tmpl->PrototypeTemplate()->SetAccessorProperty(
FIXED_ONE_BYTE_STRING(isolate, "length"),
length_getter,
Local<FunctionTemplate>(),
DontDelete);
SetProtoMethod(isolate, ctor_tmpl, "clear", Clear);
SetProtoMethodNoSideEffect(isolate, ctor_tmpl, "getItem", GetItem);
SetProtoMethodNoSideEffect(isolate, ctor_tmpl, "key", Key);
SetProtoMethod(isolate, ctor_tmpl, "removeItem", RemoveItem);
SetProtoMethod(isolate, ctor_tmpl, "setItem", SetItem);
SetConstructorFunction(context, target, "Storage", ctor_tmpl);
auto symbol = env->isolate_data()->constructor_key_symbol();
target
->DefineOwnProperty(context,
FIXED_ONE_BYTE_STRING(isolate, "kConstructorKey"),
symbol,
PropertyAttribute::ReadOnly)
.Check();
}
} // namespace webstorage
} // namespace node
NODE_BINDING_CONTEXT_AWARE_INTERNAL(webstorage, node::webstorage::Initialize)

58
src/node_webstorage.h Normal file
View File

@ -0,0 +1,58 @@
#ifndef SRC_NODE_WEBSTORAGE_H_
#define SRC_NODE_WEBSTORAGE_H_
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#include "base_object.h"
#include "node_mem.h"
#include "sqlite3.h"
#include "util.h"
namespace node {
namespace webstorage {
struct conn_deleter {
void operator()(sqlite3* conn) const noexcept {
CHECK_EQ(sqlite3_close(conn), SQLITE_OK);
}
};
using conn_unique_ptr = std::unique_ptr<sqlite3, conn_deleter>;
struct stmt_deleter {
void operator()(sqlite3_stmt* stmt) const noexcept { sqlite3_finalize(stmt); }
};
using stmt_unique_ptr = std::unique_ptr<sqlite3_stmt, stmt_deleter>;
class Storage : public BaseObject {
public:
Storage(Environment* env,
v8::Local<v8::Object> object,
v8::Local<v8::String> location);
void MemoryInfo(MemoryTracker* tracker) const override;
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
void Clear();
v8::Local<v8::Array> Enumerate();
v8::Local<v8::Value> Length();
v8::Local<v8::Value> Load(v8::Local<v8::Name> key);
v8::Local<v8::Value> LoadKey(const int index);
bool Remove(v8::Local<v8::Name> key);
bool Store(v8::Local<v8::Name> key, v8::Local<v8::Value> value);
SET_MEMORY_INFO_NAME(Storage)
SET_SELF_SIZE(Storage)
private:
bool Open();
~Storage() override;
std::string location_;
conn_unique_ptr db_;
v8::Global<v8::Map> symbols_;
};
} // namespace webstorage
} // namespace node
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#endif // SRC_NODE_WEBSTORAGE_H_

View File

@ -366,6 +366,14 @@ if (global.ReadableStream) {
);
}
if (global.Storage) {
knownGlobals.push(
global.localStorage,
global.sessionStorage,
global.Storage,
);
}
function allowGlobals(...allowlist) {
knownGlobals = knownGlobals.concat(allowlist);
}

View File

@ -35,6 +35,7 @@ Last update:
- WebCryptoAPI: https://github.com/web-platform-tests/wpt/tree/5e042cbc4e/WebCryptoAPI
- webidl/ecmascript-binding/es-exceptions: https://github.com/web-platform-tests/wpt/tree/a370aad338/webidl/ecmascript-binding/es-exceptions
- webmessaging/broadcastchannel: https://github.com/web-platform-tests/wpt/tree/e97fac4791/webmessaging/broadcastchannel
- webstorage: https://github.com/web-platform-tests/wpt/tree/9dafa89214/webstorage
[Web Platform Tests]: https://github.com/web-platform-tests/wpt
[`git node wpt`]: https://github.com/nodejs/node-core-utils/blob/main/docs/git-node.md#git-node-wpt

View File

@ -0,0 +1,55 @@
// Create a credentialless iframe. The new document will execute any scripts
// sent toward the token it returns.
const newIframeCredentialless = (child_origin, opt_headers) => {
opt_headers ||= "";
const sub_document_token = token();
let iframe = document.createElement('iframe');
iframe.src = child_origin + executor_path + opt_headers +
`&uuid=${sub_document_token}`;
iframe.credentialless = true;
document.body.appendChild(iframe);
return sub_document_token;
};
// Create a normal iframe. The new document will execute any scripts sent
// toward the token it returns.
const newIframe = (child_origin) => {
const sub_document_token = token();
let iframe = document.createElement('iframe');
iframe.src = child_origin + executor_path + `&uuid=${sub_document_token}`;
iframe.credentialless = false
document.body.appendChild(iframe);
return sub_document_token;
};
// Create a popup. The new document will execute any scripts sent toward the
// token it returns.
const newPopup = (test, origin) => {
const popup_token = token();
const popup = window.open(origin + executor_path + `&uuid=${popup_token}`);
test.add_cleanup(() => popup.close());
return popup_token;
}
// Create a fenced frame. The new document will execute any scripts sent
// toward the token it returns.
const newFencedFrame = async (child_origin) => {
const support_loading_mode_fenced_frame =
"|header(Supports-Loading-Mode,fenced-frame)";
const sub_document_token = token();
const url = child_origin + executor_path +
support_loading_mode_fenced_frame +
`&uuid=${sub_document_token}`;
const urn = await generateURNFromFledge(url, []);
attachFencedFrame(urn);
return sub_document_token;
};
const importScript = (url) => {
const script = document.createElement("script");
script.type = "text/javascript";
script.src = url;
const loaded = new Promise(resolve => script.onload = resolve);
document.body.appendChild(script);
return loaded;
}

View File

@ -0,0 +1,134 @@
const executor_path = '/common/dispatcher/executor.html?pipe=';
const executor_worker_path = '/common/dispatcher/executor-worker.js?pipe=';
const executor_service_worker_path = '/common/dispatcher/executor-service-worker.js?pipe=';
// COEP
const coep_none =
'|header(Cross-Origin-Embedder-Policy,none)';
const coep_credentialless =
'|header(Cross-Origin-Embedder-Policy,credentialless)';
const coep_require_corp =
'|header(Cross-Origin-Embedder-Policy,require-corp)';
// COEP-Report-Only
const coep_report_only_credentialless =
'|header(Cross-Origin-Embedder-Policy-Report-Only,credentialless)';
// COOP
const coop_same_origin =
'|header(Cross-Origin-Opener-Policy,same-origin)';
// CORP
const corp_cross_origin =
'|header(Cross-Origin-Resource-Policy,cross-origin)';
const cookie_same_site_none = ';SameSite=None;Secure';
// Test using the modern async/await primitives are easier to read/write.
// However they run sequentially, contrary to async_test. This is the parallel
// version, to avoid timing out.
let promise_test_parallel = (promise, description) => {
async_test(test => {
promise(test)
.then(() => test.done())
.catch(test.step_func(error => { throw error; }));
}, description);
};
// Add a cookie |cookie_key|=|cookie_value| on an |origin|.
// Note: cookies visibility depends on the path of the document. Those are set
// from a document from: /html/cross-origin-embedder-policy/credentialless/. So
// the cookie is visible to every path underneath.
const setCookie = async (origin, cookie_key, cookie_value) => {
const popup_token = token();
const popup_url = origin + executor_path + `&uuid=${popup_token}`;
const popup = window.open(popup_url);
const reply_token = token();
send(popup_token, `
document.cookie = "${cookie_key}=${cookie_value}";
send("${reply_token}", "done");
`);
assert_equals(await receive(reply_token), "done");
popup.close();
}
let parseCookies = function(headers_json) {
if (!headers_json["cookie"])
return {};
return headers_json["cookie"]
.split(';')
.map(v => v.split('='))
.reduce((acc, v) => {
acc[v[0].trim()] = v[1].trim();
return acc;
}, {});
}
// Open a new window with a given |origin|, loaded with COEP:credentialless. The
// new document will execute any scripts sent toward the token it returns.
const newCredentiallessWindow = (origin) => {
const main_document_token = token();
const url = origin + executor_path + coep_credentialless +
`&uuid=${main_document_token}`;
const context = window.open(url);
add_completion_callback(() => w.close());
return main_document_token;
};
// Create a new iframe, loaded with COEP:credentialless.
// The new document will execute any scripts sent toward the token it returns.
const newCredentiallessIframe = (parent_token, child_origin) => {
const sub_document_token = token();
const iframe_url = child_origin + executor_path + coep_credentialless +
`&uuid=${sub_document_token}`;
send(parent_token, `
let iframe = document.createElement("iframe");
iframe.src = "${iframe_url}";
document.body.appendChild(iframe);
`)
return sub_document_token;
};
// A common interface for building the 4 type of execution contexts:
// It outputs: [
// - The token to communicate with the environment.
// - A promise resolved when the environment encounters an error.
// ]
const environments = {
document: headers => {
const tok = token();
const url = window.origin + executor_path + headers + `&uuid=${tok}`;
const context = window.open(url);
add_completion_callback(() => context.close());
return [tok, new Promise(resolve => {})];
},
dedicated_worker: headers => {
const tok = token();
const url = window.origin + executor_worker_path + headers + `&uuid=${tok}`;
const context = new Worker(url);
return [tok, new Promise(resolve => context.onerror = resolve)];
},
shared_worker: headers => {
const tok = token();
const url = window.origin + executor_worker_path + headers + `&uuid=${tok}`;
const context = new SharedWorker(url);
return [tok, new Promise(resolve => context.onerror = resolve)];
},
service_worker: headers => {
const tok = token();
const url = window.origin + executor_worker_path + headers + `&uuid=${tok}`;
const scope = url; // Generate a one-time scope for service worker.
const error = new Promise(resolve => {
navigator.serviceWorker.register(url, {scope: scope})
.then(registration => {
add_completion_callback(() => registration.unregister());
}, /* catch */ resolve);
});
return [tok, error];
},
};

View File

@ -98,5 +98,9 @@
"webmessaging/broadcastchannel": {
"commit": "e97fac4791931fb7455ba3fad759d362c7108b09",
"path": "webmessaging/broadcastchannel"
},
"webstorage": {
"commit": "9dafa892146c4b5b1f604a39b3cf8677f8f70d44",
"path": "webstorage"
}
}

5
test/fixtures/wpt/webstorage/META.yml vendored Normal file
View File

@ -0,0 +1,5 @@
spec: https://html.spec.whatwg.org/multipage/webstorage.html
suggested_reviewers:
- siusin
- inexorabletash
- jdm

View File

@ -0,0 +1,4 @@
These are the storage (`localStorage`, `sessionStorage`) tests for the
[Web storage chapter of the HTML Standard](https://html.spec.whatwg.org/multipage/webstorage.html).
IDL is covered by `/html/dom/idlharness.https.html`.

View File

@ -0,0 +1,57 @@
["localStorage", "sessionStorage"].forEach(function(name) {
[9, "x"].forEach(function(key) {
test(function() {
var desc = {
value: "value",
};
var storage = window[name];
storage.clear();
assert_equals(storage[key], undefined);
assert_equals(storage.getItem(key), null);
assert_equals(Object.defineProperty(storage, key, desc), storage);
assert_equals(storage[key], "value");
assert_equals(storage.getItem(key), "value");
}, "Defining data property for key " + key + " on " + name);
test(function() {
var desc1 = {
value: "value",
};
var desc2 = {
value: "new value",
};
var storage = window[name];
storage.clear();
assert_equals(storage[key], undefined);
assert_equals(storage.getItem(key), null);
assert_equals(Object.defineProperty(storage, key, desc1), storage);
assert_equals(storage[key], "value");
assert_equals(storage.getItem(key), "value");
assert_equals(Object.defineProperty(storage, key, desc2), storage);
assert_equals(storage[key], "new value");
assert_equals(storage.getItem(key), "new value");
}, "Defining data property for key " + key + " on " + name + " twice");
test(function() {
var desc = {
value: {
toString: function() { return "value"; }
},
};
var storage = window[name];
storage.clear();
assert_equals(storage[key], undefined);
assert_equals(storage.getItem(key), null);
assert_equals(Object.defineProperty(storage, key, desc), storage);
assert_equals(storage[key], "value");
assert_equals(storage.getItem(key), "value");
}, "Defining data property with toString for key " + key + " on " + name);
});
});

View File

@ -0,0 +1,20 @@
<!doctype html>
<title>localStorage and document.domain</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
</head>
<body>
<div id="log"></div>
<iframe></iframe>
<script>
async_test(function(t) {
frames[0].addEventListener("storage", function(e) {
t.step(function() {
localStorage.clear()
t.done()
})
})
frames[0].document.domain = document.domain
localStorage.setItem("test", "test")
})
</script>

View File

@ -0,0 +1,54 @@
storageEventList = [];
iframe = document.createElement("IFRAME");
document.body.appendChild(iframe);
function runAfterNStorageEvents(callback, expectedNumEvents)
{
countStorageEvents(callback, expectedNumEvents, 0)
}
function countStorageEvents(callback, expectedNumEvents, times)
{
function onTimeout()
{
var currentCount = storageEventList.length;
if (currentCount == expectedNumEvents) {
callback();
} else if (currentCount > expectedNumEvents) {
msg = "got at least " + currentCount + ", expected only " + expectedNumEvents + " events";
callback(msg);
} else if (times > 50) {
msg = "Timeout: only got " + currentCount + ", expected " + expectedNumEvents + " events";
callback(msg);
} else {
countStorageEvents(callback, expectedNumEvents, times+1);
}
}
setTimeout(onTimeout, 20);
}
function clearStorage(storageName, callback)
{
if (window[storageName].length === 0) {
storageEventList = [];
setTimeout(callback, 0);
} else {
window[storageName].clear();
runAfterNStorageEvents(function() {
storageEventList = [];
callback();
}, 1);
}
}
function testStorages(testCallback)
{
testCallback("sessionStorage");
var hit = false;
add_result_callback(function() {
if (!hit) {
hit = true;
testCallback("localStorage");
}
});
}

View File

@ -0,0 +1,15 @@
<!DOCTYPE HTML>
<html>
<head>
<meta name="timeout" content="long">
<title>WebStorage Test: StorageEvent - window.onstorage</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
</head>
<body>
<div id="log"></div>
<script src="eventTestHarness.js"></script>
<script src="event_basic.js"></script>
</body>
</html>

View File

@ -0,0 +1,128 @@
testStorages(function(storageString) {
async_test(function(t) {
assert_true(storageString in window, storageString + " exist");
var storage = window[storageString];
t.add_cleanup(function() { storage.clear() });
clearStorage(storageString, t.step_func(loadiframe));
assert_equals(storage.length, 0, "storage.length");
function loadiframe(msg)
{
iframe.onload = t.step_func(step1);
iframe.src = "resources/event_basic.html";
}
function step1(msg)
{
storage.setItem('FOO', 'BAR');
runAfterNStorageEvents(t.step_func(step2), 1);
}
function step2(msg)
{
if(msg != undefined) {
assert_unreached(msg);
}
assert_equals(storageEventList.length, 1);
assert_equals(storageEventList[0].storageAreaString, storageString,
"Storage event came from wrong storage type.");
assert_equals(storageEventList[0].key, "FOO");
assert_equals(storageEventList[0].oldValue, null);
assert_equals(storageEventList[0].newValue, "BAR");
storage.setItem('FU', 'BAR');
storage.setItem('a', '1');
storage.setItem('b', '2');
storage.setItem('b', '3');
runAfterNStorageEvents(t.step_func(step3), 5);
}
function step3(msg)
{
if(msg != undefined) {
assert_unreached(msg);
}
assert_equals(storageEventList.length, 5);
assert_equals(storageEventList[1].storageAreaString, storageString,
"Storage event came from wrong storage type.");
assert_equals(storageEventList[1].key, "FU");
assert_equals(storageEventList[1].oldValue, null);
assert_equals(storageEventList[1].newValue, "BAR");
assert_equals(storageEventList[2].storageAreaString, storageString,
"Storage event came from wrong storage type.");
assert_equals(storageEventList[2].key, "a");
assert_equals(storageEventList[2].oldValue, null);
assert_equals(storageEventList[2].newValue, "1");
assert_equals(storageEventList[3].storageAreaString, storageString,
"Storage event came from wrong storage type.");
assert_equals(storageEventList[3].key, "b");
assert_equals(storageEventList[3].oldValue, null);
assert_equals(storageEventList[3].newValue, "2");
assert_equals(storageEventList[4].storageAreaString, storageString,
"Storage event came from wrong storage type.");
assert_equals(storageEventList[4].key, "b");
assert_equals(storageEventList[4].oldValue, "2");
assert_equals(storageEventList[4].newValue, "3");
storage.removeItem('FOO');
runAfterNStorageEvents(t.step_func(step4), 6);
}
function step4(msg)
{
if(msg != undefined) {
assert_unreached(msg);
}
assert_equals(storageEventList.length, 6);
assert_equals(storageEventList[5].storageAreaString, storageString,
"Storage event came from wrong storage type.");
assert_equals(storageEventList[5].key, "FOO");
assert_equals(storageEventList[5].oldValue, "BAR");
assert_equals(storageEventList[5].newValue, null);
storage.removeItem('FU');
runAfterNStorageEvents(t.step_func(step5), 7);
}
function step5(msg)
{
if(msg != undefined) {
assert_unreached(msg);
}
assert_equals(storageEventList.length, 7);
assert_equals(storageEventList[6].storageAreaString, storageString,
"Storage event came from wrong storage type.");
assert_equals(storageEventList[6].key, "FU");
assert_equals(storageEventList[6].oldValue, "BAR");
assert_equals(storageEventList[6].newValue, null);
storage.clear();
runAfterNStorageEvents(t.step_func(step6), 8);
}
function step6(msg)
{
if(msg != undefined) {
assert_unreached(msg);
}
assert_equals(storageEventList.length, 8);
assert_equals(storageEventList[7].storageAreaString, storageString,
"Storage event came from wrong storage type.");
assert_equals(storageEventList[7].key, null);
assert_equals(storageEventList[7].oldValue, null);
assert_equals(storageEventList[7].newValue, null);
t.done();
}
}, storageString + " mutations fire StorageEvents that are caught by the event listener set via window.onstorage.");
});

View File

@ -0,0 +1,15 @@
<!DOCTYPE HTML>
<html>
<head>
<meta name="timeout" content="long">
<title>WebStorage Test: StorageEvent - set onstorage as body attribute</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
</head>
<body>
<div id="log"></div>
<script src="eventTestHarness.js"></script>
<script src="event_body_attribute.js"></script>
</body>
</html>

View File

@ -0,0 +1,116 @@
testStorages(function(storageString) {
async_test(function(t) {
assert_true(storageString in window, storageString + " exist");
var storage = window[storageString];
t.add_cleanup(function() { storage.clear() });
clearStorage(storageString, t.step_func(step0));
assert_equals(storage.length, 0, "storage.length");
function step0(msg)
{
iframe.onload = t.step_func(step1);
// Null out the existing handler eventTestHarness.js set up;
// otherwise this test won't be testing much of anything useful.
iframe.contentWindow.onstorage = null;
iframe.src = "resources/event_body_handler.html";
}
function step1(msg)
{
storageEventList = new Array();
storage.setItem('FOO', 'BAR');
runAfterNStorageEvents(t.step_func(step2), 1);
}
function step2(msg)
{
if (msg != undefined) {
assert_unreached(msg);
}
assert_equals(storageEventList.length, 1);
assert_equals(storageEventList[0].key, "FOO");
assert_equals(storageEventList[0].oldValue, null);
assert_equals(storageEventList[0].newValue, "BAR");
storage.setItem('FU', 'BAR');
storage.setItem('a', '1');
storage.setItem('b', '2');
storage.setItem('b', '3');
runAfterNStorageEvents(t.step_func(step3), 5);
}
function step3(msg)
{
if (msg != undefined) {
assert_unreached(msg);
}
assert_equals(storageEventList.length, 5);
assert_equals(storageEventList[1].key, "FU");
assert_equals(storageEventList[1].oldValue, null);
assert_equals(storageEventList[1].newValue, "BAR");
assert_equals(storageEventList[2].key, "a");
assert_equals(storageEventList[2].oldValue, null);
assert_equals(storageEventList[2].newValue, "1");
assert_equals(storageEventList[3].key, "b");
assert_equals(storageEventList[3].oldValue, null);
assert_equals(storageEventList[3].newValue, "2");
assert_equals(storageEventList[4].key, "b");
assert_equals(storageEventList[4].oldValue, "2");
assert_equals(storageEventList[4].newValue, "3");
storage.removeItem('FOO');
runAfterNStorageEvents(t.step_func(step4), 6);
}
function step4(msg)
{
if(msg != undefined) {
assert_unreached(msg);
}
assert_equals(storageEventList.length, 6);
assert_equals(storageEventList[5].key, "FOO");
assert_equals(storageEventList[5].oldValue, "BAR");
assert_equals(storageEventList[5].newValue, null);
storage.removeItem('FU');
runAfterNStorageEvents(t.step_func(step5), 7);
}
function step5(msg)
{
if(msg != undefined) {
assert_unreached(msg);
}
assert_equals(storageEventList.length, 7);
assert_equals(storageEventList[6].key, "FU");
assert_equals(storageEventList[6].oldValue, "BAR");
assert_equals(storageEventList[6].newValue, null);
storage.clear();
runAfterNStorageEvents(t.step_func(step6), 8);
}
function step6(msg)
{
if(msg != undefined) {
assert_unreached(msg);
}
assert_equals(storageEventList.length, 8);
assert_equals(storageEventList[7].key, null);
assert_equals(storageEventList[7].oldValue, null);
assert_equals(storageEventList[7].newValue, null);
t.done();
}
}, storageString + " mutations fire StorageEvents that are caught by the event listener specified as an attribute on the body.");
});

View File

@ -0,0 +1,15 @@
<!DOCTYPE HTML>
<html>
<head>
<meta name="timeout" content="long">
<title>WebStorage Test: StorageEvent - the case of value changed</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
</head>
<body>
<div id="log"></div>
<script src="eventTestHarness.js"></script>
<script src="event_case_sensitive.js"></script>
</body>
</html>

View File

@ -0,0 +1,52 @@
testStorages(function(storageString) {
async_test(function(t) {
assert_true(storageString in window, storageString + " exist");
var storage = window[storageString];
t.add_cleanup(function() { storage.clear() });
clearStorage(storageString, t.step_func(loadiframe));
assert_equals(storage.length, 0, "storage.length");
function loadiframe(msg)
{
iframe.onload = t.step_func(step0);
iframe.src = "resources/event_basic.html";
}
function step0(msg)
{
storage.foo = "test";
runAfterNStorageEvents(t.step_func(step1), 1);
}
function step1(msg)
{
storageEventList = new Array();
storage.foo = "test";
runAfterNStorageEvents(t.step_func(step2), 0);
}
function step2(msg)
{
if(msg != undefined) {
assert_unreached(msg);
}
assert_equals(storageEventList.length, 0);
storage.foo = "TEST";
runAfterNStorageEvents(t.step_func(step3), 1);
}
function step3(msg)
{
if(msg != undefined) {
assert_unreached(msg);
}
assert_equals(storageEventList.length, 1);
t.done();
}
}, storageString + " storage events fire even when only the case of the value changes.");
});

View File

@ -0,0 +1,77 @@
test(function() {
assert_throws_js(
TypeError,
() => StorageEvent(""),
"Calling StorageEvent constructor without 'new' must throw"
);
}, "StorageEvent constructor called as normal function");
test(function() {
assert_throws_js(TypeError, () => new StorageEvent());
// should be redundant, but .length can be wrong with custom bindings
assert_equals(StorageEvent.length, 1, 'StorageEvent.length');
}, 'constructor with no arguments');
test(function() {
var event = new StorageEvent('type');
assert_equals(event.type, 'type', 'type');
assert_equals(event.key, null, 'key');
assert_equals(event.oldValue, null, 'oldValue');
assert_equals(event.newValue, null, 'newValue');
assert_equals(event.url, '', 'url');
assert_equals(event.storageArea, null, 'storageArea');
}, 'constructor with just type argument');
test(function() {
assert_not_equals(localStorage, null, 'localStorage'); // precondition
var event = new StorageEvent('storage', {
bubbles: true,
cancelable: true,
key: 'key',
oldValue: 'oldValue',
newValue: 'newValue',
url: 'url', // not an absolute URL to ensure it isn't resolved
storageArea: localStorage
});
assert_equals(event.type, 'storage', 'type');
assert_equals(event.bubbles, true, 'bubbles');
assert_equals(event.cancelable, true, 'cancelable');
assert_equals(event.key, 'key', 'key');
assert_equals(event.oldValue, 'oldValue', 'oldValue');
assert_equals(event.newValue, 'newValue', 'newValue');
assert_equals(event.url, 'url', 'url');
assert_equals(event.storageArea, localStorage, 'storageArea');
}, 'constructor with sensible type argument and members');
test(function() {
var event = new StorageEvent(null, {
key: null,
oldValue: null,
newValue: null,
url: null,
storageArea: null
});
assert_equals(event.type, 'null', 'type');
assert_equals(event.key, null, 'key');
assert_equals(event.oldValue, null, 'oldValue');
assert_equals(event.newValue, null, 'newValue');
assert_equals(event.url, 'null', 'url');
assert_equals(event.storageArea, null, 'storageArea');
}, 'constructor with null type argument and members');
test(function() {
var event = new StorageEvent(undefined, {
key: undefined,
oldValue: undefined,
newValue: undefined,
url: undefined,
storageArea: undefined
});
assert_equals(event.type, 'undefined', 'type');
assert_equals(event.key, null, 'key');
assert_equals(event.oldValue, null, 'oldValue');
assert_equals(event.newValue, null, 'newValue');
assert_equals(event.url, '', 'url'); // not 'undefined'!
assert_equals(event.storageArea, null, 'storageArea');
}, 'constructor with undefined type argument and members');

View File

@ -0,0 +1,60 @@
test(() => {
const event = new StorageEvent('storage');
assert_throws_js(TypeError, () => event.initStorageEvent());
// should be redundant, but .length can be wrong with custom bindings
assert_equals(event.initStorageEvent.length, 1, 'event.initStorageEvent.length');
}, 'initStorageEvent with 0 arguments');
test(() => {
const event = new StorageEvent('storage');
event.initStorageEvent('type');
assert_equals(event.type, 'type', 'event.type');
assert_equals(event.bubbles, false, 'event.bubbles');
assert_equals(event.cancelable, false, 'event.cancelable');
assert_equals(event.key, null, 'event.key');
assert_equals(event.oldValue, null, 'event.oldValue');
assert_equals(event.newValue, null, 'event.newValue');
assert_equals(event.url, '', 'event.url');
assert_equals(event.storageArea, null, 'event.storageArea');
}, 'initStorageEvent with 1 argument');
test(() => {
assert_not_equals(localStorage, null, 'localStorage'); // precondition
const event = new StorageEvent('storage');
event.initStorageEvent('type', true, true, 'key', 'oldValue', 'newValue', 'url', localStorage);
assert_equals(event.type, 'type', 'event.type');
assert_equals(event.bubbles, true, 'event.bubbles');
assert_equals(event.cancelable, true, 'event.cancelable');
assert_equals(event.key, 'key', 'event.key');
assert_equals(event.oldValue, 'oldValue', 'event.oldValue');
assert_equals(event.newValue, 'newValue', 'event.newValue');
assert_equals(event.url, 'url', 'event.url');
assert_equals(event.storageArea, localStorage, 'event.storageArea');
}, 'initStorageEvent with 8 sensible arguments');
test(() => {
const event = new StorageEvent('storage');
event.initStorageEvent(null, null, null, null, null, null, null, null);
assert_equals(event.type, 'null', 'event.type');
assert_equals(event.bubbles, false, 'event.bubbles');
assert_equals(event.cancelable, false, 'event.cancelable');
assert_equals(event.key, null, 'event.key');
assert_equals(event.oldValue, null, 'event.oldValue');
assert_equals(event.newValue, null, 'event.newValue');
assert_equals(event.url, 'null', 'event.url');
assert_equals(event.storageArea, null, 'event.storageArea');
}, 'initStorageEvent with 8 null arguments');
test(() => {
const event = new StorageEvent('storage');
event.initStorageEvent(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
assert_equals(event.type, 'undefined', 'event.type');
assert_equals(event.bubbles, false, 'event.bubbles');
assert_equals(event.cancelable, false, 'event.cancelable');
assert_equals(event.key, null, 'event.key');
assert_equals(event.oldValue, null, 'event.oldValue');
assert_equals(event.newValue, null, 'event.newValue');
assert_equals(event.url, '', 'event.url');
assert_equals(event.storageArea, null, 'event.storageArea');
}, 'initStorageEvent with 8 undefined arguments');

View File

@ -0,0 +1,38 @@
<!DOCTYPE HTML>
<html>
<head>
<title>WebStorage Test: localStorage event - key</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
</head>
<body>
<h1>event_local_key</h1>
<div id="log"></div>
<script>
async_test(function(t) {
localStorage.clear();
t.add_cleanup(function() { localStorage.clear() });
self.fail = t.step_func(function(msg) {
assert_unreached(msg);
t.done();
});
var expected = ['name', null]
function onStorageEvent(event) {
assert_equals(event.key, expected.shift());
if (!expected.length) {
t.done();
}
}
window.addEventListener('storage', t.step_func(onStorageEvent), false);
var el = document.createElement("iframe");
el.setAttribute('id', 'ifrm');
el.setAttribute('src', 'resources/local_set_item_clear_iframe.html');
document.body.appendChild(el);
}, "key property test of local event - Local event is fired due to an invocation of the setItem(), clear() methods.");
</script>
</body>
</html>

View File

@ -0,0 +1,38 @@
<!DOCTYPE HTML>
<html>
<head>
<title>WebStorage Test: localStorage event - newValue</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
</head>
<body>
<h1>event_local_newValue</h1>
<div id="log"></div>
<script>
async_test(function(t) {
localStorage.clear();
t.add_cleanup(function() { localStorage.clear() });
self.fail = t.step_func(function(msg) {
assert_unreached(msg);
t.done();
});
var expected = ['user1', 'user2', null]
function onStorageEvent(event) {
assert_equals(event.newValue, expected.shift());
if (!expected.length) {
t.done();
}
}
window.addEventListener('storage', t.step_func(onStorageEvent), false);
var el = document.createElement("iframe");
el.setAttribute('id', 'ifrm');
el.setAttribute('src', 'resources/local_change_item_iframe.html');
document.body.appendChild(el);
}, "newValue property test of local event - Local event is fired due to an invocation of the setItem(), clear() methods.");
</script>
</body>
</html>

View File

@ -0,0 +1,38 @@
<!DOCTYPE HTML>
<html>
<head>
<title>WebStorage Test: localStorage event - oldValue</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
</head>
<body>
<h1>event_local_oldValue</h1>
<div id="log"></div>
<script>
async_test(function(t) {
localStorage.clear();
t.add_cleanup(function() { localStorage.clear() });
self.fail = t.step_func(function(msg) {
assert_unreached(msg);
t.done();
});
var expected = [null, 'user1', null]
function onStorageEvent(event) {
assert_equals(event.oldValue, expected.shift());
if (!expected.length) {
t.done();
}
}
window.addEventListener('storage', t.step_func(onStorageEvent), false);
var el = document.createElement("iframe");
el.setAttribute('id', 'ifrm');
el.setAttribute('src', 'resources/local_change_item_iframe.html');
document.body.appendChild(el);
}, "oldValue property test of local event - Local event is fired due to an invocation of the setItem(), clear() methods.");
</script>
</body>
</html>

View File

@ -0,0 +1,45 @@
<!DOCTYPE HTML>
<meta charset="utf-8">
<title>Web Storage Test: event - localStorage removeItem</title>
<link rel="author" title="Intel" href="http://www.intel.com">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
async_test(function(t) {
localStorage.clear();
t.add_cleanup(function() { localStorage.clear() });
self.step = function(f) { t.step(f); };
var event_index = 0;
window.addEventListener('storage', t.step_func(function(event) {
switch(++event_index) {
case 1:
assert_equals(event.key, "name", "set key");
assert_equals(event.oldValue, null, "set oldValue");
assert_equals(event.newValue, "user1", "set newValue");
assert_equals(event.url, el.contentDocument.documentURI, "set url");
assert_equals(event.storageArea, localStorage, "set storageArea");
break;
case 2:
assert_equals(event.key, "name", "remove key");
assert_equals(event.oldValue, "user1", "remove oldValue");
assert_equals(event.newValue, null, "remove newValue");
assert_equals(event.url, el.contentDocument.documentURI, "remove url");
assert_equals(event.storageArea, localStorage, "remove storageArea");
t.done();
break;
}
}), false);
var el = document.createElement("iframe");
el.setAttribute('id', 'ifrm');
el.setAttribute('src', 'resources/local_set_item_remove_iframe.html');
document.body.appendChild(el);
}, "key property test of local event");
</script>

View File

@ -0,0 +1,39 @@
<!DOCTYPE HTML>
<html>
<head>
<title>WebStorage Test: localStorage event - storageArea</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
</head>
<body>
<h1>event_local_storageArea</h1>
<div id="log"></div>
<script>
async_test(function(t) {
localStorage.clear();
t.add_cleanup(function() { localStorage.clear() });
self.fail = t.step_func(function(msg) {
assert_unreached(msg);
t.done();
});
function onStorageEvent(event) {
assert_equals(event.storageArea.length, 1);
var key = event.storageArea.key(0);
var value = event.storageArea.getItem(key);
assert_equals(key, "name");
assert_equals(value, "user1");
t.done();
}
window.addEventListener('storage', t.step_func(onStorageEvent), false);
var el = document.createElement("iframe");
el.setAttribute('id', 'ifrm');
el.setAttribute('src', 'resources/local_set_item_iframe.html');
document.body.appendChild(el);
}, "storageArea property test of local event - Local event is fired due to an invocation of the setItem() method.");
</script>
</body>
</html>

View File

@ -0,0 +1,43 @@
<!DOCTYPE HTML>
<html>
<head>
<title>WebStorage Test: localStorage event - url</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
</head>
<body>
<h1>event_local_url</h1>
<div id="log"></div>
<script>
async_test(function(t) {
localStorage.clear();
t.add_cleanup(function() { localStorage.clear() });
self.fail = t.step_func(function(msg) {
assert_unreached(msg);
t.done();
});
function onStorageEvent(event) {
var url = window.location.href;
var pos = url.lastIndexOf("/");
if (pos != -1) {
url = url.substr(0, pos + 1);
url = url + "resources/local_set_item_iframe.html";
}
assert_equals(event.url, url);
t.done();
}
window.addEventListener('storage', t.step_func(onStorageEvent), false);
var el = document.createElement("iframe");
el.setAttribute('id', 'ifrm');
el.setAttribute('src', 'resources/local_set_item_iframe.html');
document.body.appendChild(el);
}, "url property test of local event - Local event is fired due to an invocation of the setItem() method.");
</script>
</body>
</html>

View File

@ -0,0 +1,111 @@
<!DOCTYPE HTML>
<html>
<title>WebStorage Test: StorageEvent - only if something changes</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<body>
<script>
const iframe = document.createElement('iframe');
function tests(storageName) {
test(t => assert_true(storageName in window), storageName + ' exists');
const storage = window[storageName];
const iframeStorage = iframe.contentWindow[storageName];
add_completion_callback(() => {
storage.clear();
});
promise_test(t => {
const w = new EventWatcher(t, iframe.contentWindow, 'storage');
// Random key to make sure we don't conflict with any cruft leftover from
// earlier runs. Any synchronization would be really hard with localStorage
// limited guarantees.
const testKey = Math.random().toString(36).slice(2);
storage.setItem(testKey, 'foo');
storage.setItem(testKey, 'foo');
storage.setItem(testKey, 'bar');
return w.wait_for('storage')
.then(e => {
assert_equals(e.storageArea, iframeStorage);
assert_equals(e.key, testKey);
assert_equals(e.newValue, 'foo');
return w.wait_for('storage');
})
.then(e => {
assert_equals(e.storageArea, iframeStorage);
assert_equals(e.key, testKey);
assert_equals(e.oldValue, 'foo');
assert_equals(e.newValue, 'bar');
});
}, 'Setting to same value does not trigger event for ' + storageName);
promise_test(t => {
const w = new EventWatcher(t, iframe.contentWindow, 'storage');
// Random key to make sure we don't conflict with any cruft leftover from
// earlier runs. Any synchronization would be really hard with localStorage
// limited guarantees.
const testKey1 = Math.random().toString(36).slice(2);
const testKey2 = Math.random().toString(36).slice(2);
storage.removeItem(testKey1);
storage.setItem(testKey2, 'foo');
return w.wait_for('storage')
.then(e => {
assert_equals(e.storageArea, iframeStorage);
assert_equals(e.key, testKey2);
assert_equals(e.newValue, 'foo');
});
}, 'Deleting non-existent key does not trigger event for ' + storageName);
promise_test(t => {
const w = new EventWatcher(t, iframe.contentWindow, 'storage');
// Random key to make sure we don't conflict with any cruft leftover from
// earlier runs. Any synchronization would be really hard with localStorage
// limited guarantees.
const testKey = Math.random().toString(36).slice(2);
storage.setItem(testKey, 'foo');
storage.clear();
storage.clear();
storage.setItem(testKey, 'bar');
return w.wait_for('storage')
.then(e => {
assert_equals(e.storageArea, iframeStorage);
assert_equals(e.key, testKey);
assert_equals(e.newValue, 'foo');
return w.wait_for('storage');
})
.then(e => {
assert_equals(e.storageArea, iframeStorage);
assert_equals(e.key, null);
assert_equals(e.oldValue, null);
assert_equals(e.newValue, null);
return w.wait_for('storage');
})
.then(e => {
assert_equals(e.storageArea, iframeStorage);
assert_equals(e.key, testKey);
assert_equals(e.oldValue, null);
assert_equals(e.newValue, 'bar');
});
}, 'Clearing empty storage does not trigger event for ' + storageName);
}
iframe.src = "resources/event_basic.html";
iframe.onload = () => {
tests('sessionStorage');
tests('localStorage');
};
document.body.appendChild(iframe);
</script>
</body>
</html>

View File

@ -0,0 +1,38 @@
<!DOCTYPE HTML>
<html>
<head>
<title>WebStorage Test: sessionStorage event - key</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
</head>
<body>
<h1>event_session_key</h1>
<div id="log"></div>
<script>
async_test(function(t) {
sessionStorage.clear();
t.add_cleanup(function() { sessionStorage.clear() });
self.fail = t.step_func(function(msg) {
assert_unreached(msg);
t.done();
});
var expected = ['name', null]
function onStorageEvent(event) {
assert_equals(event.key, expected.shift());
if (!expected.length) {
t.done();
}
}
window.addEventListener('storage', t.step_func(onStorageEvent), false);
var el = document.createElement("iframe");
el.setAttribute('id', 'ifrm');
el.setAttribute('src', 'resources/session_set_item_clear_iframe.html');
document.body.appendChild(el);
}, "key property test of session event - Session event is fired due to an invocation of the setItem(), clear() methods.");
</script>
</body>
</html>

View File

@ -0,0 +1,40 @@
<!DOCTYPE HTML>
<html>
<head>
<title>WebStorage Test: sessionStorage event - newValue</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
</head>
<body>
<h1>event_session_newValue</h1>
<div id="log"></div>
<script>
async_test(function(t) {
sessionStorage.clear();
t.add_cleanup(function() { sessionStorage.clear() });
self.fail = t.step_func(function(msg) {
assert_unreached(msg);
t.done();
});
var expected = ['user1', 'user2', null]
function onStorageEvent(event) {
t.step(function() {
assert_equals(event.newValue, expected.shift());
});
if (!expected.length) {
t.done();
}
}
window.addEventListener('storage', t.step_func(onStorageEvent), false);
var el = document.createElement("iframe");
el.setAttribute('id', 'ifrm');
el.setAttribute('src', 'resources/session_change_item_iframe.html');
document.body.appendChild(el);
}, "newvalue property test of session event - Session event is fired due to an invocation of the setItem(), clear() methods.");
</script>
</body>
</html>

View File

@ -0,0 +1,38 @@
<!DOCTYPE HTML>
<html>
<head>
<title>WebStorage Test: sessionStorage event - oldValue</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
</head>
<body>
<h1>event_session_oldValue</h1>
<div id="log"></div>
<script>
async_test(function(t) {
sessionStorage.clear();
t.add_cleanup(function() { sessionStorage.clear() });
self.fail = t.step_func(function(msg) {
assert_unreached(msg);
t.done();
});
var expected = [null, 'user1', null]
function onStorageEvent(event) {
assert_equals(event.oldValue, expected.shift());
if (!expected.length) {
t.done();
}
}
window.addEventListener('storage', t.step_func(onStorageEvent), false);
var el = document.createElement("iframe");
el.setAttribute('id', 'ifrm');
el.setAttribute('src', 'resources/session_change_item_iframe.html');
document.body.appendChild(el);
}, "oldvalue property test of session event - Session event is fired due to an invocation of the setItem(), clear() methods.");
</script>
</body>
</html>

View File

@ -0,0 +1,44 @@
<!DOCTYPE HTML>
<meta charset="utf-8">
<title>Web Storage Test: event - sessionStorage removeItem</title>
<link rel="author" title="Intel" href="http://www.intel.com">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
async_test(function(t) {
sessionStorage.clear();
t.add_cleanup(function() { sessionStorage.clear() });
self.step = function(f) { t.step(f); };
var event_index = 0;
window.addEventListener('storage', t.step_func(function(event) {
switch(++event_index) {
case 1:
assert_equals(event.key, "name", "set key");
assert_equals(event.oldValue, null, "set oldValue");
assert_equals(event.newValue, "user1", "set newValue");
assert_equals(event.url, el.contentDocument.documentURI, "set url");
assert_equals(event.storageArea, sessionStorage, "set storageArea");
break;
case 2:
assert_equals(event.key, "name", "remove key");
assert_equals(event.oldValue, "user1", "remove oldValue");
assert_equals(event.newValue, null, "remove newValue");
assert_equals(event.url, el.contentDocument.documentURI, "remove url");
assert_equals(event.storageArea, sessionStorage, "remove storageArea");
t.done();
break;
}
}), false);
var el = document.createElement("iframe");
el.setAttribute('id', 'ifrm');
el.setAttribute('src', 'resources/session_set_item_remove_iframe.html');
document.body.appendChild(el);
}, "key property test of session event");
</script>

View File

@ -0,0 +1,40 @@
<!DOCTYPE HTML>
<html>
<head>
<title>WebStorage Test: sessionStorage event - storageArea</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
</head>
<body>
<h1>event_session_storageArea</h1>
<div id="log"></div>
<script>
async_test(function(t) {
sessionStorage.clear();
t.add_cleanup(function() { sessionStorage.clear() });
self.fail = t.step_func(function(msg) {
assert_unreached(msg);
t.done();
});
function onStorageEvent(event) {
assert_equals(event.storageArea.length, 1);
var key = event.storageArea.key(0);
var value = event.storageArea.getItem(key);
assert_equals(key, "name");
assert_equals(value, "user1");
t.done();
}
window.addEventListener('storage', t.step_func(onStorageEvent), false);
var el = document.createElement("iframe");
el.setAttribute('id', 'ifrm');
el.setAttribute('src', 'resources/session_set_item_iframe.html');
document.body.appendChild(el);
}, "storageArea property test of session event - session event is fired due to an invocation of the setItem() method.");
</script>
</body>
</html>

View File

@ -0,0 +1,43 @@
<!DOCTYPE HTML>
<html>
<head>
<title>WebStorage Test: sessionStorage event - url</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
</head>
<body>
<h1>event_session_url</h1>
<div id="log"></div>
<script>
async_test(function(t) {
sessionStorage.clear();
t.add_cleanup(function() { sessionStorage.clear() });
self.fail = t.step_func(function(msg) {
assert_unreached(msg);
t.done();
});
function onStorageEvent(event) {
var url = window.location.href;
var pos = url.lastIndexOf("/");
if (pos != -1) {
url = url.substr(0, pos + 1);
url = url + "resources/session_set_item_iframe.html";
}
assert_equals(event.url, url);
t.done();
}
window.addEventListener('storage', t.step_func(onStorageEvent), false);
var el = document.createElement("iframe");
el.setAttribute('id', 'ifrm');
el.setAttribute('src', 'resources/session_set_item_iframe.html');
document.body.appendChild(el);
}, "url property test of session event - Session event is fired due to an invocation of the setItem() method.");
</script>
</body>
</html>

View File

@ -0,0 +1,15 @@
<!DOCTYPE HTML>
<html>
<head>
<meta name="timeout" content="long">
<title>WebStorage Test: StorageEvent - attached setAttribute</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
</head>
<body>
<div id="log"></div>
<script src="eventTestHarness.js"></script>
<script src="event_setattribute.js"></script>
</body>
</html>

View File

@ -0,0 +1,115 @@
testStorages(function(storageString) {
async_test(function(t) {
assert_true(storageString in window, storageString + " exist");
var storage = window[storageString];
t.add_cleanup(function() { storage.clear() });
clearStorage(storageString, t.step_func(step0));
assert_equals(storage.length, 0, "storage.length");
function step0(msg)
{
iframe.onload = t.step_func(step1);
// Null out the existing handler eventTestHarness.js set up;
// otherwise this test won't be testing much of anything useful.
iframe.contentWindow.onstorage = null;
iframe.src = "resources/event_setattribute_handler.html";
}
function step1(msg)
{
storage.setItem('FOO', 'BAR');
runAfterNStorageEvents(t.step_func(step2), 1);
}
function step2(msg)
{
if(msg != undefined) {
assert_unreached(msg);
}
assert_equals(storageEventList.length, 1);
assert_equals(storageEventList[0].key, "FOO");
assert_equals(storageEventList[0].oldValue, null);
assert_equals(storageEventList[0].newValue, "BAR");
storage.setItem('FU', 'BAR');
storage.setItem('a', '1');
storage.setItem('b', '2');
storage.setItem('b', '3');
runAfterNStorageEvents(t.step_func(step3), 5);
}
function step3(msg)
{
if(msg != undefined) {
assert_unreached(msg);
}
assert_equals(storageEventList.length, 5);
assert_equals(storageEventList[1].key, "FU");
assert_equals(storageEventList[1].oldValue, null);
assert_equals(storageEventList[1].newValue, "BAR");
assert_equals(storageEventList[2].key, "a");
assert_equals(storageEventList[2].oldValue, null);
assert_equals(storageEventList[2].newValue, "1");
assert_equals(storageEventList[3].key, "b");
assert_equals(storageEventList[3].oldValue, null);
assert_equals(storageEventList[3].newValue, "2");
assert_equals(storageEventList[4].key, "b");
assert_equals(storageEventList[4].oldValue, "2");
assert_equals(storageEventList[4].newValue, "3");
storage.removeItem('FOO');
runAfterNStorageEvents(t.step_func(step4), 6);
}
function step4(msg)
{
if(msg != undefined) {
assert_unreached(msg);
}
assert_equals(storageEventList.length, 6);
assert_equals(storageEventList[5].key, "FOO");
assert_equals(storageEventList[5].oldValue, "BAR");
assert_equals(storageEventList[5].newValue, null);
storage.removeItem('FU');
runAfterNStorageEvents(t.step_func(step5), 7);
}
function step5(msg)
{
if(msg != undefined) {
assert_unreached(msg);
}
assert_equals(storageEventList.length, 7);
assert_equals(storageEventList[6].key, "FU");
assert_equals(storageEventList[6].oldValue, "BAR");
assert_equals(storageEventList[6].newValue, null);
storage.clear();
runAfterNStorageEvents(t.step_func(step6), 8);
}
function step6(msg)
{
if(msg != undefined) {
assert_unreached(msg);
}
assert_equals(storageEventList.length, 8);
assert_equals(storageEventList[7].key, null);
assert_equals(storageEventList[7].oldValue, null);
assert_equals(storageEventList[7].newValue, null);
t.done();
}
}, storageString + " mutations fire StorageEvents that are caught by the event listener attached via setattribute.");
});

View File

@ -0,0 +1,75 @@
<!doctype html>
<meta charset=utf-8>
<title>localStorage: about:blank partitioning</title>
<meta name=help href="https://privacycg.github.io/storage-partitioning/">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/get-host-info.sub.js"></script>
<script src="/webstorage/resources/partitioning-utils.js"></script>
<body>
<script>
const path =
"webstorage/resources/localstorage-about-blank-partitioned-win-open.html";
const crossSiteURL = `${get_host_info().HTTP_NOTSAMESITE_ORIGIN}/${path}`;
const sameSiteURL = `${get_host_info().HTTP_ORIGIN}/${path}`;
let firstPartyID = getOrCreateID("userID3");
let crossSiteIframeID;
let sameSiteIframeID;
let crossSiteIframe;
let crossSiteIframeAboutBlankID;
let frameMessageCount = 0;
promise_test(async t => {
localStorage.clear();
// Step 1. Add a cross-site iframe
return addIframePromise(crossSiteURL).then(async crossSiteIframe => {
return new Promise(resolve => {
window.addEventListener("message", async e => {
const payload = {
command: "open about:blank window"
}
if (e.data.message === "window loaded") {
// Step 2. cross-site iframe is loaded, capture reference to its ID
crossSiteIframeID = e.data.userID;
// Step 3. Ask the cross-site iframe to create an about:blank window
crossSiteIframe.contentWindow.postMessage(payload, e.origin);
}
if (e.data.message === "about:blank frame ID") {
// Step 4. capture reference to 3P iframe's about:blank window ID
crossSiteIframeAboutBlankID = e.data.userID;
crossSiteIframe.contentWindow.postMessage(
{command: "close about:blank window"}, "*");
}
if (e.data.message === "about:blank window closed") {
resolve({crossSiteIframeID, crossSiteIframeAboutBlankID});
}
});
}).then(ids => {
const {
crossSiteIframeID,
crossSiteIframeAboutBlankID
} = ids;
// Step 5. Assert some things
for (let id in ids) {
assert_true(id !== undefined, "id is not undefined");
}
// Note: we use assert_true, rather than assert_equals becuase we're
// setting random numbers as IDs - this would mean expectations
// files wouldn't work as intended.
assert_true(crossSiteIframeAboutBlankID !== crossSiteIframeID,
"about:blank window opened by 3P iframe does not inherit 3P iframe's StorageKey");
assert_true(firstPartyID !== crossSiteIframeAboutBlankID,
"about:blank window open by 3P iframe does not inherit 1P StorageKey");
localStorage.clear();
})
});
}, "StorageKey: test 3P about:blank window opened from a 3P iframe");
</script>
</body>

View File

@ -0,0 +1,61 @@
<!doctype html>
<meta charset=utf-8>
<title>localStorage: partitioned storage test</title>
<meta name=help href="https://privacycg.github.io/storage-partitioning/">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<iframe id="shared-iframe" src="http://{{host}}:{{ports[http][0]}}/webstorage/resources/localstorage-about-blank-partitioned-iframe.html"></iframe>
<body>
<script>
// Here's the set-up for this test:
// Step 1. (window) set up listeners for main window.
// Step 2. (window) set up load listener for same-site iframe.
// Step 3. (same-site iframe) loads, send it a message to createOrGet a "userID".
// Step 4. (same-site iframe) receives the message, creates the "userID".
// Step 5. (window) receives "storage got set" message from same-site iframe.
// Step 6. (window) opens cross-site window w/ shared (same-site to us currently) iframe.
// Step 7. (cross-site iframe) loads, sends back the userID key from the iframe.
// Step 8. (window) asserts that the IDs should be different, as they should have a different StorageKey.
const altOrigin = "http://{{hosts[alt][]}}:{{ports[http][0]}}";
async_test(t => {
let crossSiteWindow;
let crossSiteID;
let sameSiteID;
const iframe = document.getElementById("shared-iframe");
iframe.addEventListener("load", t.step_func(e => {
const payload = {
command: "create ID",
key: "userID",
};
iframe.contentWindow.postMessage(payload, iframe.origin);
}), {once: true});
window.addEventListener("message", t.step_func(e => {
if (e.data.message === "ID created") {
sameSiteID = e.data.userID;
assert_true(typeof sameSiteID === "string");
if (location.origin !== altOrigin) {
crossSiteWindow = window.open(`${altOrigin}/webstorage/localstorage-basic-partitioned.tentative.sub.html`, "", "noopener=false");
t.add_cleanup(() => crossSiteWindow.close());
}
}
if (e.data.message === "cross-site window iframe loaded") {
crossSiteID = e.data.userID;
t.step(() => {
assert_true(typeof crossSiteID === "string");
assert_true(sameSiteID !== crossSiteID, "IDs pulled from two partitioned iframes are different.")
});
// clean up after ourselves.
iframe.contentWindow.localStorage.clear();
crossSiteWindow.postMessage({command: "clearStorage"}, altOrigin);
t.done();
};
}));
}, "Simple test for partitioned localStorage");
</script>
</body>

View File

@ -0,0 +1,27 @@
// META: script=/common/get-host-info.sub.js
// META: script=/common/utils.js
// META: script=/common/dispatcher/dispatcher.js
// META: script=/html/cross-origin-embedder-policy/credentialless/resources/common.js
// META: script=/html/anonymous-iframe/resources/common.js
promise_test(async test => {
const same_origin= get_host_info().HTTPS_ORIGIN;
const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN;
const reply_token = token();
for(iframe of [
newIframe(same_origin),
newIframe(cross_origin),
]) {
send(iframe, `
try {
let c = window.localStorage;
send("${reply_token}","OK");
} catch (exception) {
send("${reply_token}","ERROR");
}
`);
}
assert_equals(await receive(reply_token), "OK");
assert_equals(await receive(reply_token), "OK");
}, "LocalStorage should be accessible on both same_origin and cross_origin iframes");

View File

@ -0,0 +1,17 @@
var tests = [
function() { localStorage.key(); },
function() { localStorage.getItem(); },
function() { localStorage.setItem(); },
function() { localStorage.setItem("a"); },
function() { localStorage.removeItem(); },
function() { sessionStorage.key(); },
function() { sessionStorage.getItem(); },
function() { sessionStorage.setItem(); },
function() { sessionStorage.setItem("a"); },
function() { sessionStorage.removeItem(); },
];
tests.forEach(function(fun) {
test(function() {
assert_throws_js(TypeError, fun);
}, "Should throw TypeError for " + format_value(fun) + ".");
});

View File

@ -0,0 +1,16 @@
<!DOCTYPE HTML>
<html>
<head>
<script>
function handleStorageEvent(e) {
if (window.sessionStorage === e.storageArea)
e.storageAreaString = "sessionStorage";
else if (window.localStorage === e.storageArea)
e.storageAreaString = "localStorage";
window.parent.storageEventList.push(e);
}
</script>
</head>
<body onstorage="handleStorageEvent(event);">
</body>
</html>

View File

@ -0,0 +1,14 @@
<!DOCTYPE HTML>
<html>
<head>
<script>
function handleStorageEvent(e) {
window.parent.storageEventList.push(e);
}
</script>
</head>
<body onstorage="handleStorageEvent(event);">
</body>
</html>

View File

@ -0,0 +1,15 @@
<!DOCTYPE HTML>
<html>
<head></head>
<body>
<script>
function handleStorageEvent(e) {
window.parent.storageEventList.push(e);
}
document.body.setAttribute("onstorage", "handleStorageEvent(event);");
</script>
</body>
</html>

View File

@ -0,0 +1,18 @@
<!DOCTYPE HTML>
<html>
<body>
<script>
if (('localStorage' in window) && window.localStorage !== null){
try {
localStorage.setItem("name", "user1");
localStorage.setItem("name", "user2");
} catch (e) {
parent.fail("setItem method is failed.");
}
localStorage.clear();
} else {
parent.fail("localStorage is not supported.");
}
</script>
</body>
</html>

View File

@ -0,0 +1,17 @@
<!DOCTYPE HTML>
<html>
<body>
<script>
if (('localStorage' in window) && window.localStorage !== null){
try {
localStorage.setItem("name", "user1");
} catch (e) {
parent.fail("setItem method is failed.");
}
localStorage.clear();
} else {
parent.fail("localStorage is not supported.");
}
</script>
</body>
</html>

View File

@ -0,0 +1,16 @@
<!DOCTYPE HTML>
<html>
<body>
<script>
if (('localStorage' in window) && window.localStorage !== null){
try {
localStorage.setItem("name", "user1");
} catch (e) {
parent.fail("setItem method is failed.");
}
} else {
parent.fail("localStorage is not supported.");
}
</script>
</body>
</html>

View File

@ -0,0 +1,11 @@
<!DOCTYPE HTML>
<html>
<body>
<script>
parent.step(function() {
localStorage.setItem("name", "user1");
localStorage.removeItem('name');
});
</script>
</body>
</html>

View File

@ -0,0 +1,41 @@
<!doctype html>
<meta charset="utf-8">
<script>
function getOrCreateID(key) {
if (!localStorage.getItem(key)) {
const newID = +new Date() + "-" + Math.random();
localStorage.setItem(key, newID);
}
return localStorage.getItem(key);
}
window.addEventListener("load", () => {
// if we have an opener, we know that we are loaded inside a cross-site
// iframe (because we opened it ourselves).
if (parent.opener) {
const payload = {
message: "cross-site window iframe loaded",
userID: getOrCreateID("userID"),
}
parent.opener.postMessage(payload, parent.opener.origin);
}
});
window.addEventListener("message", (e) => {
if (e.data.command == "create ID") {
getOrCreateID(e.data.key);
// storage is set, call back to window.
const payload = {
message: "ID created",
userID: localStorage.getItem("userID"),
}
e.source.postMessage(payload, e.source.origin);
}
if (e.data.command == "clearStorage") {
localStorage.clear();
}
});
</script>

View File

@ -0,0 +1,37 @@
<!doctype html>
<meta charset="utf-8">
<script src="./partitioning-utils.js"></script>
<script>
window.addEventListener("load", () => {
localStorage.clear();
const userID = getOrCreateID("userID4");
const payload = {
message: "window loaded",
userID,
}
let win = window.opener ? window.opener : window.parent;
win.postMessage(payload, "*");
});
window.addEventListener("message", e => {
let win = window.opener ? parent.window.opener : window.parent;
if (e.data.command == "open about:blank window") {
window.blankWindow = window.open("about:blank");
const payload = {
message: "about:blank frame ID",
userID: window.blankWindow?.localStorage["userID4"],
}
let win = window.opener ? parent.window.opener : window.parent;
win.postMessage(payload, "*");
}
if (e.data.command == "close about:blank window") {
window.blankWindow.close();
win.postMessage({message: "about:blank window closed"}, "*");
}
});
</script>

View File

@ -0,0 +1,20 @@
function getOrCreateID(key) {
if (!localStorage.getItem(key)) {
const newID = +new Date() + "-" + Math.random();
localStorage.setItem(key, newID);
}
return localStorage.getItem(key);
}
function addIframePromise(url) {
return new Promise(resolve => {
const iframe = document.createElement("iframe");
iframe.style.display = "none";
iframe.src = url;
iframe.addEventListener("load", (e) => {
resolve(iframe);
}, {once: true});
document.body.appendChild(iframe);
});
}

View File

@ -0,0 +1,44 @@
<!doctype html>
<meta charset="utf-8">
<script>
function getOrCreateID(key) {
if (!sessionStorage.getItem(key)) {
const newID = new Date() + "-" + Math.random();
sessionStorage.setItem(key, newID);
}
return sessionStorage.getItem(key);
}
window.addEventListener("load", () => {
// In this testing set-up, only cross-site iframes will have an opener.
if (parent.opener) {
const payload = {
message: "cross-site window iframe loaded",
userID: getOrCreateID("userID"),
}
// Once the cross-site iframe has loaded, we send a message back to
// the main window with the ID from sessionStorage.
parent.opener.postMessage(payload, parent.opener.origin);
}
});
window.addEventListener("message", (e) => {
if (e.data.command == "create ID") {
// e.data.key is equivalent to "userID"
getOrCreateID(e.data.key);
const payload = {
message: "ID created",
userID: sessionStorage.getItem("userID"),
}
// Return the ID from sessionStorage to the main window.
e.source.postMessage(payload, e.source.origin);
}
// Additional functionality for clean-up at the end of the test.
if (e.data.command == "clearStorage") {
sessionStorage.clear();
}
});
</script>

View File

@ -0,0 +1,18 @@
<!DOCTYPE HTML>
<html>
<body>
<script>
if (('sessionStorage' in window) && window.sessionStorage !== null){
try {
sessionStorage.setItem("name", "user1");
sessionStorage.setItem("name", "user2");
} catch (e) {
parent.fail("setItem method is failed.");
}
sessionStorage.clear();
} else {
parent.fail("sessionStorage is not supported.");
}
</script>
</body>
</html>

View File

@ -0,0 +1,17 @@
<!DOCTYPE HTML>
<html>
<body>
<script>
if (('sessionStorage' in window) && window.sessionStorage !== null){
try {
sessionStorage.setItem('name', 'user1');
} catch (e) {
parent.fail('setItem method is failed.');
}
sessionStorage.clear();
} else {
parent.fail('sessionStorage is not supported.');
}
</script>
</body>
</html>

View File

@ -0,0 +1,16 @@
<!DOCTYPE HTML>
<html>
<body>
<script>
if (('sessionStorage' in window) && window.sessionStorage !== null){
try {
sessionStorage.setItem('name', 'user1');
} catch (e) {
parent.fail('setItem method is failed.');
}
} else {
parent.fail('sessionStorage is not supported.');
}
</script>
</body>
</html>

View File

@ -0,0 +1,11 @@
<!DOCTYPE HTML>
<html>
<body>
<script>
parent.step(function() {
sessionStorage.setItem("name", "user1");
sessionStorage.removeItem('name');
});
</script>
</body>
</html>

View File

@ -0,0 +1,36 @@
<!DOCTYPE HTML>
<html>
<head>
<title>WebStorage Test: localStorage - second page</title>
</head>
<body>
<script>
var storage = window.localStorage;
var assertions = [];
assertions.push({
actual: storage.getItem("FOO"),
expected: "BAR",
message: "storage.getItem('FOO')"
});
storage.setItem("FOO", "BAR-NEWWINDOW");
assertions.push({
actual: storage.getItem("FOO"),
expected: "BAR-NEWWINDOW",
message: "value for FOO after changing"
});
assertions.push({
actual: window.opener.localStorage.getItem("FOO"),
expected: "BAR-NEWWINDOW",
message: "value for FOO in my opening window"
});
window.opener.postMessage(assertions, '*');
</script>
</body>
</html>

View File

@ -0,0 +1,34 @@
<!DOCTYPE HTML>
<html>
<head>
<title>WebStorage Test: sessionStorage - second page</title>
</head>
<body>
<script>
var storage = window.sessionStorage;
var assertions = [];
assertions.push({
actual: storage.getItem("FOO"),
expected: null,
message: "storage.getItem('FOO')"
});
storage.setItem("FOO", "BAR-NEWWINDOW");
assertions.push({
actual: storage.getItem("FOO"),
expected: "BAR-NEWWINDOW",
message: "value for FOO after changing"
});
let channel = new BroadcastChannel('storage_session_window_noopener');
channel.postMessage(assertions, '*');
window.close();
</script>
</body>
</html>

View File

@ -0,0 +1,41 @@
<!DOCTYPE HTML>
<html>
<head>
<title>WebStorage Test: sessionStorage - second page</title>
</head>
<body>
<script>
var storage = window.sessionStorage;
var assertions = [];
assertions.push({
actual: storage.getItem("FOO"),
expected: "BAR",
message: "storage.getItem('FOO')"
});
storage.setItem("FOO", "BAR-NEWWINDOW");
assertions.push({
actual: storage.getItem("FOO"),
expected: "BAR-NEWWINDOW",
message: "value for FOO after changing"
});
assertions.push({
actual: window.opener.sessionStorage.getItem("FOO"),
expected: "BAR",
message: "value for FOO in my opening window"
});
assertions.push({
actual: storage.getItem("BAZ"),
expected: null,
message: "value for BAZ set after window.open(), is not set in new window"
});
window.opener.postMessage(assertions, '*');
</script>
</body>
</html>

View File

@ -0,0 +1,73 @@
<!doctype html>
<meta charset=utf-8>
<title>sessionStorage: partitioned storage test</title>
<meta name=help href="https://privacycg.github.io/storage-partitioning/">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<iframe id="shared-iframe" src="http://{{host}}:{{ports[http][0]}}/webstorage/resources/sessionStorage-about-blank-partitioned-iframe.html"></iframe>
<body>
<script>
// Here's the set-up for this test:
// Step 1. (main window) set up messaging and same-site iframe load listeners.
// Step 2. (same-site iframe) loads, requests sessionStorage for "userID".
// Step 3. (same-site iframe) receives the message, gets or allocates sessionStorage,
// and returns the generated ID to the main frame.
// Step 4. (main window) receives "storage got set" message from same-site iframe.
// Step 5. (main window) opens a new cross-site window with the shared-iframe inside.
// Step 6. (cross-site iframe) loads, requests sessionStorage for "userID", gets or
// allocates that sessionStorage, and returns the generated ID to the main frame.
// Step 7. (main window) asserts that the generated IDs should be different, as
// they should have a different StorageKey.
const altOrigin = "http://{{hosts[alt][]}}:{{ports[http][0]}}";
async_test(t => {
let crossSiteWindow;
let crossSiteID;
let sameSiteID;
// Retrieve the iframe we created in the HTML above.
const iframe = document.getElementById("shared-iframe");
// Once the iframe loads, we request sessionStorage.
iframe.addEventListener("load", t.step_func(e => {
const payload = {
command: "create ID",
key: "userID",
};
iframe.contentWindow.postMessage(payload, iframe.origin);
}), {once: true});
window.addEventListener("message", t.step_func(e => {
// Once we get or allocate the sessionStorage, we expect the iframe
// to message us back with the generated ID.
if (e.data.message === "ID created") {
sameSiteID = e.data.userID;
assert_true(typeof sameSiteID === "string");
// Now that same-site storage has been secured, we need to open a
// new cross-site window that contains our shared-iframe to repeat
// the process in a cross-site environment.
if (location.origin !== altOrigin) {
crossSiteWindow = window.open(`${altOrigin}/webstorage/sessionStorage-basic-partitioned.tentative.sub.html`, "", "noopener=false");
t.add_cleanup(() => crossSiteWindow.close());
}
}
// We expect that once the cross-site iframe requests sessionStorage,
// it will message us back with the generated ID.
if (e.data.message === "cross-site window iframe loaded") {
crossSiteID = e.data.userID;
t.step(() => {
// Same and cross-site iframes should have different generated IDs.
assert_true(typeof crossSiteID === "string");
assert_true(sameSiteID !== crossSiteID, "IDs pulled from two partitioned iframes are different.")
});
// Clear storage state to clean up after the test.
iframe.contentWindow.sessionStorage.clear();
crossSiteWindow.postMessage({command: "clearStorage"}, altOrigin);
t.done();
};
}));
}, "Simple test for partitioned sessionStorage");
</script>
</body>

View File

@ -0,0 +1,102 @@
["localStorage", "sessionStorage"].forEach(function(name) {
[9, "x"].forEach(function(key) {
test(function() {
var expected = "value for " + this.name;
var value = expected;
var storage = window[name];
storage.clear();
assert_equals(storage[key], undefined);
assert_equals(storage.getItem(key), null);
assert_equals(storage[key] = value, value);
assert_equals(storage[key], expected);
assert_equals(storage.getItem(key), expected);
}, "Setting property for key " + key + " on " + name);
test(function() {
var expected = "value for " + this.name;
var value = {
toString: function() { return expected; }
};
var storage = window[name];
storage.clear();
assert_equals(storage[key], undefined);
assert_equals(storage.getItem(key), null);
assert_equals(storage[key] = value, value);
assert_equals(storage[key], expected);
assert_equals(storage.getItem(key), expected);
}, "Setting property with toString for key " + key + " on " + name);
test(function() {
var proto = "proto for " + this.name;
Storage.prototype[key] = proto;
this.add_cleanup(function() { delete Storage.prototype[key]; });
var value = "value for " + this.name;
var storage = window[name];
storage.clear();
assert_equals(storage[key], proto);
assert_equals(storage.getItem(key), null);
assert_equals(storage[key] = value, value);
// Hidden because no [LegacyOverrideBuiltIns].
assert_equals(storage[key], proto);
assert_equals(Object.getOwnPropertyDescriptor(storage, key), undefined);
assert_equals(storage.getItem(key), value);
}, "Setting property for key " + key + " on " + name + " with data property on prototype");
test(function() {
var proto = "proto for " + this.name;
Storage.prototype[key] = proto;
this.add_cleanup(function() { delete Storage.prototype[key]; });
var value = "value for " + this.name;
var existing = "existing for " + this.name;
var storage = window[name];
storage.clear();
storage.setItem(key, existing);
// Hidden because no [LegacyOverrideBuiltIns].
assert_equals(storage[key], proto);
assert_equals(Object.getOwnPropertyDescriptor(storage, key), undefined);
assert_equals(storage.getItem(key), existing);
assert_equals(storage[key] = value, value);
assert_equals(storage[key], proto);
assert_equals(Object.getOwnPropertyDescriptor(storage, key), undefined);
assert_equals(storage.getItem(key), value);
}, "Setting property for key " + key + " on " + name + " with data property on prototype and existing item");
test(function() {
var storage = window[name];
storage.clear();
var proto = "proto getter for " + this.name;
Object.defineProperty(Storage.prototype, key, {
"get": function() { return proto; },
"set": this.unreached_func("Should not call [[Set]] on prototype"),
"configurable": true,
});
this.add_cleanup(function() {
delete Storage.prototype[key];
delete storage[key];
assert_false(key in storage);
});
var value = "value for " + this.name;
assert_equals(storage[key], proto);
assert_equals(storage.getItem(key), null);
assert_equals(storage[key] = value, value);
// Property is hidden because no [LegacyOverrideBuiltIns].
assert_equals(storage[key], proto);
assert_equals(Object.getOwnPropertyDescriptor(storage, key), undefined);
assert_equals(storage.getItem(key), value);
}, "Setting property for key " + key + " on " + name + " with accessor property on prototype");
});
});

View File

@ -0,0 +1,16 @@
["localStorage", "sessionStorage"].forEach(function(name) {
test(function() {
var storage = window[name];
storage.clear();
assert_equals(storage.length, 0, "storage.length");
var builtins = ["key", "getItem", "setItem", "removeItem", "clear"];
var origBuiltins = builtins.map(function(b) { return Storage.prototype[b]; });
assert_array_equals(builtins.map(function(b) { return storage[b]; }), origBuiltins, "a");
builtins.forEach(function(b) { storage[b] = b; });
assert_array_equals(builtins.map(function(b) { return storage[b]; }), origBuiltins, "b");
assert_array_equals(builtins.map(function(b) { return storage.getItem(b); }), builtins, "c");
assert_equals(storage.length, builtins.length, "storage.length");
}, "Builtins in " + name);
});

View File

@ -0,0 +1,16 @@
["localStorage", "sessionStorage"].forEach(function(name) {
test(function() {
var storage = window[name];
storage.clear();
storage.setItem("name", "user1");
assert_equals(storage.getItem("name"), "user1");
assert_equals(storage.name, "user1");
assert_equals(storage.length, 1);
storage.clear();
assert_equals(storage.getItem("name"), null, "storage.getItem('name')");
assert_equals(storage.name, undefined, "storage.name");
assert_equals(storage.length, 0, "storage.length");
}, "Clear in " + name);
});

View File

@ -0,0 +1,55 @@
["localStorage", "sessionStorage"].forEach(function(name) {
test(function() {
assert_true(name in window, name + " exist");
var storage = window[name];
storage.clear();
Storage.prototype.prototypeTestKey = "prototypeTestValue";
storage.foo = "bar";
storage.fu = "baz";
storage.batman = "bin suparman";
storage.bar = "foo";
storage.alpha = "beta";
storage.zeta = "gamma";
const enumeratedArray = Object.keys(storage);
enumeratedArray.sort(); // Storage order is implementation-defined.
const expectArray = ["alpha", "bar", "batman", "foo", "fu", "zeta"];
assert_array_equals(enumeratedArray, expectArray);
// 'prototypeTestKey' is not an actual storage key, it is just a
// property set on Storage's prototype object.
assert_equals(storage.length, 6);
assert_equals(storage.getItem("prototypeTestKey"), null);
assert_equals(storage.prototypeTestKey, "prototypeTestValue");
}, name + ": enumerate a Storage object and get only the keys as a result and the built-in properties of the Storage object should be ignored");
test(function() {
const storage = window[name];
storage.clear();
storage.setItem("foo", "bar");
storage.baz = "quux";
storage.setItem(0, "alpha");
storage[42] = "beta";
for (let prop in storage) {
if (!storage.hasOwnProperty(prop))
continue;
const desc = Object.getOwnPropertyDescriptor(storage, prop);
assert_true(desc.configurable);
assert_true(desc.enumerable);
assert_true(desc.writable);
}
const keys = Object.keys(storage);
keys.sort(); // Storage order is implementation-defined.
assert_array_equals(keys, ["0", "42", "baz", "foo"]);
const values = Object.values(storage);
values.sort(); // Storage order is implementation-defined.
assert_array_equals(values, ["alpha", "bar", "beta", "quux"]);
}, name + ": test enumeration of numeric and non-numeric keys");
});

View File

@ -0,0 +1,37 @@
["localStorage", "sessionStorage"].forEach(function(name) {
test(function() {
var storage = window[name];
storage.clear();
runTest();
function doWedgeThySelf() {
storage.setItem("clear", "almost");
storage.setItem("key", "too");
storage.setItem("getItem", "funny");
storage.setItem("removeItem", "to");
storage.setItem("length", "be");
storage.setItem("setItem", "true");
}
function runTest() {
doWedgeThySelf();
assert_equals(storage.getItem('clear'), "almost");
assert_equals(storage.getItem('key'), "too");
assert_equals(storage.getItem('getItem'), "funny");
assert_equals(storage.getItem('removeItem'), "to");
assert_equals(storage.getItem('length'), "be");
assert_equals(storage.getItem('setItem'), "true");
// Test to see if an exception is thrown for any of the built in
// functions.
storage.setItem("test", "123");
storage.key(0);
storage.getItem("test");
storage.removeItem("test");
storage.clear();
assert_equals(storage.length, 0);
}
}, name + " should be not rendered unusable by setting a key with the same name as a storage function such that the function is hidden");
});

View File

@ -0,0 +1,34 @@
["localStorage", "sessionStorage"].forEach(function(name) {
test(function() {
var storage = window[name];
storage.clear();
storage.setItem("name", "x");
storage.setItem("undefined", "foo");
storage.setItem("null", "bar");
storage.setItem("", "baz");
test(function() {
assert_equals(storage.length, 4);
}, "All items should be added to " + name + ".");
test(function() {
assert_equals(storage["unknown"], undefined, "storage['unknown']")
assert_equals(storage["name"], "x", "storage['name']")
assert_equals(storage["undefined"], "foo", "storage['undefined']")
assert_equals(storage["null"], "bar", "storage['null']")
assert_equals(storage[undefined], "foo", "storage[undefined]")
assert_equals(storage[null], "bar", "storage[null]")
assert_equals(storage[""], "baz", "storage['']")
}, "Named access to " + name + " should be correct");
test(function() {
assert_equals(storage.getItem("unknown"), null, "storage.getItem('unknown')")
assert_equals(storage.getItem("name"), "x", "storage.getItem('name')")
assert_equals(storage.getItem("undefined"), "foo", "storage.getItem('undefined')")
assert_equals(storage.getItem("null"), "bar", "storage.getItem('null')")
assert_equals(storage.getItem(undefined), "foo", "storage.getItem(undefined)")
assert_equals(storage.getItem(null), "bar", "storage.getItem(null)")
assert_equals(storage.getItem(""), "baz", "storage.getItem('')")
}, name + ".getItem should be correct")
}, "Get value by getIten(key) and named access in " + name + ".");
});

View File

@ -0,0 +1,22 @@
["localStorage", "sessionStorage"].forEach(function(name) {
test(function() {
var storage = window[name];
storage.clear();
assert_false("name" in storage);
storage["name"] = "user1";
assert_true("name" in storage);
}, "The in operator in " + name + ": property access");
test(function() {
var storage = window[name];
storage.clear();
assert_false("name" in storage);
storage.setItem("name", "user1");
assert_true("name" in storage);
assert_equals(storage.name, "user1");
storage.removeItem("name");
assert_false("name" in storage);
}, "The in operator in " + name + ": method access");
});

View File

@ -0,0 +1,28 @@
["localStorage", "sessionStorage"].forEach(function(name) {
test(function() {
var storage = window[name];
storage.clear();
storage["name"] = "user1";
storage["age"] = "42";
test(function() {
assert_equals(storage[-1], undefined);
assert_equals(storage[0], undefined);
assert_equals(storage[1], undefined);
assert_equals(storage[2], undefined);
}, "Getting number properties on " + name);
test(function() {
assert_equals(storage["-1"], undefined);
assert_equals(storage["0"], undefined);
assert_equals(storage["1"], undefined);
assert_equals(storage["2"], undefined);
}, "Getting number-valued string properties on " + name);
test(function() {
storage.setItem(1, "number");
assert_equals(storage[1], "number");
assert_equals(storage["1"], "number");
}, "Getting existing number-valued properties on " + name);
}, "Indexed getter on " + name);
});

View File

@ -0,0 +1,51 @@
["localStorage", "sessionStorage"].forEach(function(name) {
test(function() {
var storage = window[name];
storage.clear();
storage.setItem("name", "user1");
storage.setItem("age", "20");
storage.setItem("a", "1");
storage.setItem("b", "2");
var keys = ["name", "age", "a", "b"];
function doTest(index) {
test(function() {
var key = storage.key(index);
assert_not_equals(key, null);
assert_true(keys.indexOf(key) >= 0,
"Unexpected key " + key + " found.");
}, name + ".key(" + index + ") should return the right thing.");
}
for (var i = 0; i < keys.length; ++i) {
doTest(i);
doTest(i + 0x100000000);
}
test(function() {
assert_equals(storage.key(-1), null, "storage.key(-1)");
assert_equals(storage.key(4), null, "storage.key(4)");
}, name + ".key() should return null for out-of-range arguments.");
}, name + ".key");
test(function() {
var get_keys = function(s) {
var keys = [];
for (var i = 0; i < s.length; ++i) {
keys.push(s.key(i));
}
return keys;
};
var storage = window[name];
storage.clear();
storage.setItem("name", "user1");
storage.setItem("age", "20");
storage.setItem("a", "1");
storage.setItem("b", "2");
var expected_keys = get_keys(storage);
storage.setItem("name", "user2");
assert_array_equals(get_keys(storage), expected_keys);
}, name + ".key with value changes");
});

View File

@ -0,0 +1,10 @@
["localStorage", "sessionStorage"].forEach(function(name) {
test(function () {
var storage = window[name];
storage.clear();
storage.setItem("", "empty string");
assert_equals(storage.getItem(""), "empty string");
}, name + ".key with empty string");
});

View File

@ -0,0 +1,23 @@
["localStorage", "sessionStorage"].forEach(function(name) {
test(function() {
var storage = window[name];
storage.clear();
assert_equals(storage.length, 0, "storage.length")
storage["name"] = "user1";
storage["age"] = "20";
assert_equals(storage.length, 2, "storage.length")
}, name + ".length (method access)");
test(function() {
var storage = window[name];
storage.clear();
assert_equals(storage.length, 0, "storage.length")
storage.setItem("name", "user1");
storage.setItem("age", "20");
assert_equals(storage.length, 2, "storage.length")
}, name + ".length (proprty access)");
});

View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>WebStorage Test: local storage</title>
<link rel="author" title="Intel" href="http://www.intel.com">
<meta name="flags" content="interact">
<h2>Description</h2>
<p>
This test validates that store data using Local Storage which means that close the page, and re-open it, the data saved before should be loaded again.
</p>
<h2>Preconditions</h2>
<ol class="instructions">
<li>
Click the "Clear" button, refresh the page once and then check if the page shows "You have viewed this page 1 time(s)"
</li>
<li>
Close the page, re-open it and then check if the page still shows "You have viewed this page 2 time(s)"
</li>
<li>
If the above two steps are all true the test case pass, otherwise it fail
</li>
</ol>
<p>
<h2>You have viewed this page
<span id="count">an untold number of</span>
time(s).</h2>
<button type="button" onclick="javascript:localStorage.pageLoadCount = 0;"><h3>Clear</h3></button>
</p>
<script>
if (!localStorage.pageLoadCount) {
localStorage.pageLoadCount = 0;
}
localStorage.pageLoadCount = parseInt(localStorage.pageLoadCount) + 1;
document.getElementById('count').textContent = localStorage.pageLoadCount;
</script>

View File

@ -0,0 +1,16 @@
test(function() {
localStorage.clear();
var index = 0;
var key = "name";
var val = "x".repeat(1024);
assert_throws_dom("QUOTA_EXCEEDED_ERR", function() {
while (true) {
index++;
localStorage.setItem("" + key + index, "" + val + index);
}
});
localStorage.clear();
}, "Throws QuotaExceededError when the quota has been exceeded");

View File

@ -0,0 +1,16 @@
async_test(function(t) {
var storage = window.localStorage;
storage.clear();
storage.setItem("FOO", "BAR");
var win = window.open("resources/storage_local_window_open_second.html");
window.addEventListener('message', t.step_func(function(e) {
e.data.forEach(t.step_func(function(assertion) {
assert_equals(assertion.actual, assertion.expected, assertion.message);
}));
win.close();
t.done();
}));
}, "A new window to make sure there is a copy of the previous window's localStorage, and that they do not diverge after a change");

View File

@ -0,0 +1,44 @@
["localStorage", "sessionStorage"].forEach(function(name) {
test(function() {
var storage = window[name];
storage.clear();
storage.setItem("name", "user1");
assert_equals(storage.getItem("name"), "user1");
storage.removeItem("name");
storage.removeItem("unknown");
assert_equals(storage.getItem("name"), null, "storage.getItem('name')")
}, name + ".removeItem()");
test(function() {
var storage = window[name];
storage.clear();
storage.setItem("name", "user1");
assert_equals(storage.getItem("name"), "user1");
delete storage["name"];
delete storage["unknown"];
assert_equals(storage.getItem("name"), null, "storage.getItem('name')")
}, "delete " + name + "[]");
test(function() {
var storage = window[name];
storage.clear();
storage.setItem("null", "test");
assert_true("null" in storage);
storage.removeItem(null);
assert_false("null" in storage);
}, name + ".removeItem(null)");
test(function() {
var storage = window[name];
storage.clear();
storage.setItem("undefined", "test");
assert_true("undefined" in storage);
storage.removeItem(undefined);
assert_false("undefined" in storage);
}, name + ".removeItem(undefined)");
});

View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>WebStorage Test: session storage</title>
<link rel="author" title="Intel" href="http://www.intel.com">
<meta name="flags" content="interact">
<h2>Description</h2>
<p>
This test validates that store the data using Session Storage which means that even if you close the page, and re-open it, the data saved before should be lost.
</p>
<ol class="instructions">
<li>
Click the "Clear" button, refresh the page once and then check if the page shows "You have viewed this page 1 time(s)"
</li>
<li>
Close the page, re-open it and then check if the page still shows "You have viewed this page 1 time(s)"
</li>
<li>
If the above two steps are all true the test case pass, otherwise it fail.<br>
</li>
</ol>
<p>
<h2>You have viewed this page
<span id="count">an untold number of</span>
time(s).</h2>
<button type="button" onclick="javascript:sessionStorage.pageLoadCount = 0;"><h3>Clear</h3></button>
</p>
<script>
if (!sessionStorage.pageLoadCount) {
sessionStorage.pageLoadCount = 0;
}
sessionStorage.pageLoadCount = parseInt(sessionStorage.pageLoadCount) + 1;
document.getElementById('count').textContent = sessionStorage.pageLoadCount;
</script>

View File

@ -0,0 +1,16 @@
test(function() {
sessionStorage.clear();
var index = 0;
var key = "name";
var val = "x".repeat(1024);
assert_throws_dom("QUOTA_EXCEEDED_ERR", function() {
while (true) {
index++;
sessionStorage.setItem("" + key + index, "" + val + index);
}
});
sessionStorage.clear();
}, "Throws QuotaExceededError when the quota has been exceeded");

View File

@ -0,0 +1,21 @@
async_test(function(t) {
var storage = window.sessionStorage;
storage.clear();
storage.setItem("FOO", "BAR");
let channel = new BroadcastChannel("storage_session_window_noopener");
channel.addEventListener("message", t.step_func(function(e) {
e.data.forEach(t.step_func(function(assertion) {
assert_equals(assertion.actual, assertion.expected, assertion.message);
}));
assert_equals(storage.getItem("FOO"), "BAR", "value for FOO in original window");
t.done();
}));
var win = window.open("resources/storage_session_window_noopener_second.html",
"_blank",
"noopener");
}, "A new noopener window to make sure there is a not copy of the previous window's sessionStorage");

View File

@ -0,0 +1,17 @@
async_test(function(t) {
var storage = window.sessionStorage;
storage.clear();
storage.setItem("FOO", "BAR");
var win = window.open("resources/storage_session_window_open_second.html");
storage.setItem("BAZ", "QUX");
window.addEventListener('message', t.step_func(function(e) {
e.data.forEach(t.step_func(function(assertion) {
assert_equals(assertion.actual, assertion.expected, assertion.message);
}));
win.close();
t.done();
}));
}, "A new window to make sure there is a copy of the previous window's sessionStorage, and that they diverge after a change");

View File

@ -0,0 +1,26 @@
test(function() {
var popup = window.open("", "sessionStorageTestWindow");
sessionStorage.setItem("FOO", "BAR");
var reopened = window.open("", "sessionStorageTestWindow");
assert_equals(
popup,
reopened,
"window.open with the same name should re-open the same window"
);
assert_equals(
sessionStorage.getItem("FOO"),
"BAR",
"local sessionStorage is correct"
);
assert_equals(
popup.sessionStorage.getItem("FOO"),
null,
"popup sessionStorage is correct"
);
popup.close();
}, "ensure that re-opening a named window doesn't copy sessionStorage");

View File

@ -0,0 +1,21 @@
var store_list = [
["key0", "value0"],
["key1", "value1"],
["key2", "value2"]
];
["localStorage", "sessionStorage"].forEach(function(name) {
test(function () {
var storage = window[name];
storage.clear();
store_list.forEach(function(item) {
storage.setItem(item[0], item[1]);
});
for (var i = 0; i < store_list.length; i++) {
var value = storage.getItem("key" + i);
assert_equals(value, "value" + i);
}
}, "enumerate a " + name + " object with the key and get the values");
});

View File

@ -0,0 +1,215 @@
["localStorage", "sessionStorage"].forEach(function(name) {
var test_error = { name: "test" };
var interesting_strs = ["\uD7FF", "\uD800", "\uDBFF", "\uDC00",
"\uDFFF", "\uE000", "\uFFFD", "\uFFFE", "\uFFFF",
"\uD83C\uDF4D", "\uD83Ca", "a\uDF4D",
"\uDBFF\uDFFF"];
for (var i = 0; i <= 0xFF; i++) {
interesting_strs.push(String.fromCharCode(i));
}
test(function() {
var storage = window[name];
storage.clear();
storage.setItem("name", "user1");
assert_equals(storage.length, 1, "localStorage.setItem")
}, name + ".setItem()");
test(function() {
var storage = window[name];
storage.clear();
storage["name"] = "user1";
assert_true("name" in storage);
assert_equals(storage.length, 1, "storage.length")
assert_equals(storage.getItem("name"), "user1");
assert_equals(storage["name"], "user1");
}, name + "[]");
test(function() {
var storage = window[name];
storage.clear();
storage["name"] = "user1";
storage["name"] = "user2";
assert_true("name" in storage);
assert_equals(storage.length, 1, "storage.length")
assert_equals(storage.getItem("name"), "user2");
assert_equals(storage["name"], "user2");
}, name + "[] update");
test(function() {
var storage = window[name];
storage.clear();
storage.setItem("age", null);
assert_true("age" in storage);
assert_equals(storage.length, 1, "storage.length")
assert_equals(storage.getItem("age"), "null");
assert_equals(storage["age"], "null");
}, name + ".setItem(_, null)");
test(function() {
var storage = window[name];
storage.clear();
storage["age"] = null;
assert_true("age" in storage);
assert_equals(storage.length, 1, "storage.length")
assert_equals(storage.getItem("age"), "null");
assert_equals(storage["age"], "null");
}, name + "[] = null");
test(function() {
var storage = window[name];
storage.clear();
storage.setItem("age", undefined);
assert_true("age" in storage);
assert_equals(storage.length, 1, "storage.length")
assert_equals(storage.getItem("age"), "undefined");
assert_equals(storage["age"], "undefined");
}, name + ".setItem(_, undefined)");
test(function() {
var storage = window[name];
storage.clear();
storage["age"] = undefined;
assert_true("age" in storage);
assert_equals(storage.length, 1, "storage.length")
assert_equals(storage.getItem("age"), "undefined");
assert_equals(storage["age"], "undefined");
}, name + "[] = undefined");
test(function() {
var storage = window[name];
storage.clear();
storage.setItem("age", "10");
assert_throws_exactly(test_error, function() {
storage.setItem("age",
{ toString: function() { throw test_error; } });
});
assert_true("age" in storage);
assert_equals(storage.length, 1, "storage.length")
assert_equals(storage.getItem("age"), "10");
assert_equals(storage["age"], "10");
}, name + ".setItem({ throws })");
test(function() {
var storage = window[name];
storage.clear();
storage.setItem("age", "10");
assert_throws_exactly(test_error, function() {
storage["age"] =
{ toString: function() { throw test_error; } };
});
assert_true("age" in storage);
assert_equals(storage.length, 1, "storage.length")
assert_equals(storage.getItem("age"), "10");
assert_equals(storage["age"], "10");
}, name + "[] = { throws }");
test(function() {
var storage = window[name];
storage.clear();
storage.setItem(undefined, "test");
assert_true("undefined" in storage);
assert_equals(storage.length, 1, "storage.length")
assert_equals(storage.getItem("undefined"), "test");
assert_equals(storage["undefined"], "test");
}, name + ".setItem(undefined, _)");
test(function() {
var storage = window[name];
storage.clear();
storage[undefined] = "test2";
assert_true("undefined" in storage);
assert_equals(storage.length, 1, "storage.length")
assert_equals(storage.getItem("undefined"), "test2");
assert_equals(storage["undefined"], "test2");
}, name + "[undefined]");
test(function() {
var storage = window[name];
storage.clear();
storage.setItem(null, "test");
assert_true("null" in storage);
assert_equals(storage.length, 1, "storage.length")
assert_equals(storage.getItem("null"), "test");
assert_equals(storage["null"], "test");
}, name + ".setItem(null, _)");
test(function() {
var storage = window[name];
storage.clear();
storage[null] = "test2";
assert_true("null" in storage);
assert_equals(storage.length, 1, "storage.length")
assert_equals(storage.getItem("null"), "test2");
assert_equals(storage["null"], "test2");
}, name + "[null]");
test(function() {
var storage = window[name];
storage.clear();
storage["foo\0bar"] = "user1";
assert_true("foo\0bar" in storage);
assert_false("foo\0" in storage);
assert_false("foo\0baz" in storage);
assert_false("foo" in storage);
assert_equals(storage.length, 1, "storage.length")
assert_equals(storage.getItem("foo\0bar"), "user1");
assert_equals(storage.getItem("foo\0"), null);
assert_equals(storage.getItem("foo\0baz"), null);
assert_equals(storage.getItem("foo"), null);
assert_equals(storage["foo\0bar"], "user1");
assert_equals(storage["foo\0"], undefined);
assert_equals(storage["foo\0baz"], undefined);
assert_equals(storage["foo"], undefined);
}, name + " key containing null");
test(function() {
var storage = window[name];
storage.clear();
storage["name"] = "foo\0bar";
assert_true("name" in storage);
assert_equals(storage.length, 1, "storage.length")
assert_equals(storage.getItem("name"), "foo\0bar");
assert_equals(storage["name"], "foo\0bar");
}, name + " value containing null");
for (i = 0; i < interesting_strs.length; i++) {
var str = interesting_strs[i];
test(function() {
var storage = window[name];
storage.clear();
storage[str] = "user1";
assert_true(str in storage);
assert_equals(storage.length, 1, "storage.length")
assert_equals(storage.getItem(str), "user1");
assert_equals(storage[str], "user1");
}, name + "[" + format_value(str) + "]");
test(function() {
var storage = window[name];
storage.clear();
storage["name"] = str;
assert_equals(storage.length, 1, "storage.length")
assert_equals(storage.getItem("name"), str);
assert_equals(storage["name"], str);
}, name + "[] = " + format_value(str));
}
});

Some files were not shown because too many files have changed in this diff Show More