View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.javascript.host;
16  
17  import static org.htmlunit.BrowserVersionFeatures.JS_STORAGE_PRESERVED_INCLUDED;
18  
19  import java.util.Arrays;
20  import java.util.HashSet;
21  import java.util.Map;
22  
23  import org.htmlunit.corejs.javascript.Scriptable;
24  import org.htmlunit.javascript.HtmlUnitScriptable;
25  import org.htmlunit.javascript.JavaScriptEngine;
26  import org.htmlunit.javascript.configuration.JsxClass;
27  import org.htmlunit.javascript.configuration.JsxConstructor;
28  import org.htmlunit.javascript.configuration.JsxFunction;
29  import org.htmlunit.javascript.configuration.JsxGetter;
30  import org.w3c.dom.DOMException;
31  
32  /**
33   * JavaScript host object for {@code Storage}.
34   *
35   * @author Ahmed Ashour
36   * @author Marc Guillemot
37   * @author Ronald Brill
38   * @author Kanoko Yamamoto
39   *
40   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Storage">MDN Documentation</a>
41   */
42  @JsxClass
43  public class Storage extends HtmlUnitScriptable {
44  
45      private static final HashSet<String> RESERVED_NAMES_ = new HashSet<>(Arrays.asList(
46          "clear", "key", "getItem", "length", "removeItem",
47          "setItem", "constructor", "toString", "toLocaleString", "valueOf", "hasOwnProperty", "propertyIsEnumerable",
48          "isPrototypeOf", "__defineGetter__", "__defineSetter__", "__lookupGetter__", "__lookupSetter__"));
49  
50      private static final long STORE_SIZE_KIMIT = 5_200_000;
51  
52      private final Map<String, String> store_;
53      private long storeSize_;
54  
55      /**
56       * Default constructor for prototype instantiation only.
57       */
58      public Storage() {
59          super();
60          store_ = null;
61      }
62  
63      /**
64       * Creates an instance of this object.
65       */
66      @JsxConstructor
67      public void jsConstructor() {
68          // nothing to do
69      }
70  
71      /**
72       * Creates a new {@code Storage} instance backed by the given store.
73       *
74       * @param window the parent scope
75       * @param store the backing map for this storage
76       */
77      public Storage(final Window window, final Map<String, String> store) {
78          super();
79          store_ = store;
80          storeSize_ = 0L;
81          setParentScope(getTopLevelScope(window.getParentScope()));
82          setPrototype(window.getPrototype(Storage.class));
83      }
84  
85      /**
86       * {@inheritDoc}
87       */
88      @Override
89      public void put(final String name, final Scriptable start, final Object value) {
90          final boolean isReserved = RESERVED_NAMES_.contains(name);
91          if (store_ == null || isReserved) {
92              super.put(name, start, value);
93          }
94          if (store_ != null && (!isReserved || getBrowserVersion().hasFeature(JS_STORAGE_PRESERVED_INCLUDED))) {
95              setItem(name, JavaScriptEngine.toString(value));
96          }
97      }
98  
99      /**
100      * {@inheritDoc}
101      */
102     @Override
103     public Object get(final String name, final Scriptable start) {
104         if (store_ == null || RESERVED_NAMES_.contains(name)) {
105             return super.get(name, start);
106         }
107         final Object value = getItem(name);
108         if (value != null) {
109             return value;
110         }
111         return super.get(name, start);
112     }
113 
114     /**
115      * Returns the number of items in the storage.
116      *
117      * @return the number of items
118      */
119     @JsxGetter
120     public int getLength() {
121         return store_.size();
122     }
123 
124     /**
125      * Removes the item with the specified key.
126      *
127      * @param key the key of the item to remove
128      */
129     @JsxFunction
130     public void removeItem(final String key) {
131         final String removed = store_.remove(key);
132         if (removed != null) {
133             storeSize_ -= removed.length();
134         }
135     }
136 
137     /**
138      * Returns the key at the specified index.
139      *
140      * @param index the index of the key to retrieve
141      * @return the key at the given index, or {@code null} if out of range
142      */
143     @JsxFunction
144     public String key(final int index) {
145         if (index >= 0) {
146             int counter = 0;
147             for (final String key : store_.keySet()) {
148                 if (counter == index) {
149                     return key;
150                 }
151                 counter++;
152             }
153         }
154         return null;
155     }
156 
157     /**
158      * Returns the value associated with the given key.
159      *
160      * @param key the key of the item to retrieve
161      * @return the value for the given key, or {@code null} if not found
162      */
163     @JsxFunction
164     public Object getItem(final String key) {
165         return store_.get(key);
166     }
167 
168     /**
169      * Sets the value for the given key.
170      *
171      * @param key the key of the item to set
172      * @param data the new value
173      */
174     @JsxFunction
175     public void setItem(final String key, final String data) {
176         final String existingData = store_.get(key);
177         final long storeSize = storeSize_ + data.length() - (existingData != null ? existingData.length() : 0);
178         if (storeSize > STORE_SIZE_KIMIT) {
179             throw JavaScriptEngine.throwAsScriptRuntimeEx(
180                     new DOMException((short) 22, "QuotaExceededError: Failed to execute 'setItem' on 'Storage': "
181                             + "Setting the value of '" + key + "' exceeded the quota."));
182         }
183         storeSize_ = storeSize;
184         store_.put(key, data);
185     }
186 
187     /**
188      * Removes all items from this storage.
189      */
190     @JsxFunction
191     public void clear() {
192         store_.clear();
193         storeSize_ = 0;
194     }
195 }