Localisation updates from https://translatewiki.net.
[mediawiki.git] / resources / src / mediawiki.editRecovery / storage.js
blob2758f372978bcb21c0240b918a41470f81566025
1 /*!
2  * Common indexedDB-access methods, only for use by the ResourceLoader modules this directory.
3  */
5 const config = require( './config.json' );
6 const dbName = mw.config.get( 'wgDBname' ) + '_editRecovery';
7 const editRecoveryExpiry = config.EditRecoveryExpiry;
8 const objectStoreName = 'unsaved-page-data';
10 let db = null;
12 // TODO: Document Promise objects as native promises, not jQuery ones.
14 /**
15  * @ignore
16  * @return {jQuery.Promise} Promise which resolves on success
17  */
18 function openDatabaseLocal() {
19         return new Promise( ( resolve, reject ) => {
20                 const schemaNumber = 3;
21                 const openRequest = window.indexedDB.open( dbName, schemaNumber );
22                 openRequest.addEventListener( 'upgradeneeded', upgradeDatabase );
23                 openRequest.addEventListener( 'success', ( event ) => {
24                         db = event.target.result;
25                         resolve();
26                 } );
27                 openRequest.addEventListener( 'error', ( event ) => {
28                         reject( 'EditRecovery error: ' + event.target.error );
29                 } );
30         } );
33 /**
34  * @private
35  * @param {Object} versionChangeEvent
36  */
37 function upgradeDatabase( versionChangeEvent ) {
38         const keyPathParts = [ 'pageName', 'section' ];
39         let objectStore;
41         db = versionChangeEvent.target.result;
42         if ( !db.objectStoreNames.contains( objectStoreName ) ) {
43                 // ObjectStore does not yet exist, create it.
44                 objectStore = db.createObjectStore( objectStoreName, { keyPath: keyPathParts } );
45         } else {
46                 // ObjectStore exists, but needs to be upgraded.
47                 objectStore = versionChangeEvent.target.transaction.objectStore( objectStoreName );
48         }
50         // Create indexes if they don't exist.
51         if ( !objectStore.indexNames.contains( 'pageName-section' ) ) {
52                 objectStore.createIndex( 'pageName-section', keyPathParts, { unique: true } );
53         }
54         if ( !objectStore.indexNames.contains( 'expiry' ) ) {
55                 objectStore.createIndex( 'expiry', 'expiry' );
56         }
58         // Delete old indexes.
59         if ( objectStore.indexNames.contains( 'lastModified' ) ) {
60                 objectStore.deleteIndex( 'lastModified' );
61         }
62         if ( objectStore.indexNames.contains( 'expiryDate' ) ) {
63                 objectStore.deleteIndex( 'expiryDate' );
64         }
67 /**
68  * Load data relating to a specific page and section
69  *
70  * @ignore
71  * @param {string} pageName The current page name (with underscores)
72  * @param {string|null} section The section ID, or null if the whole page is being edited
73  * @return {jQuery.Promise} Promise which resolves with the page data on success, or rejects with an error message.
74  */
75 function loadData( pageName, section ) {
76         return new Promise( ( resolve, reject ) => {
77                 if ( !db ) {
78                         reject( 'DB not opened' );
79                 }
80                 const transaction = db.transaction( objectStoreName, 'readonly' );
81                 const key = [ pageName, section || '' ];
82                 const findExisting = transaction
83                         .objectStore( objectStoreName )
84                         .get( key );
85                 findExisting.addEventListener( 'success', () => {
86                         resolve( findExisting.result );
87                 } );
88         } );
91 function loadAllData() {
92         return new Promise( ( resolve, reject ) => {
93                 if ( !db ) {
94                         reject( 'DB not opened' );
95                 }
96                 const transaction = db.transaction( objectStoreName, 'readonly' );
97                 const requestAll = transaction
98                         .objectStore( objectStoreName )
99                         .getAll();
100                 requestAll.addEventListener( 'success', () => {
101                         resolve( requestAll.result );
102                 } );
103         } );
107  * Save data for a specific page and section
109  * @ignore
110  * @param {string} pageName The current page name (with underscores)
111  * @param {string|null} section The section ID, or null if the whole page is being edited
112  * @param {Object} pageData The page data to save
113  * @return {jQuery.Promise} Promise which resolves on success, or rejects with an error message.
114  */
115 function saveData( pageName, section, pageData ) {
116         return new Promise( ( resolve, reject ) => {
117                 if ( !db ) {
118                         reject( 'DB not opened' );
119                 }
121                 // Add indexed fields.
122                 pageData.pageName = pageName;
123                 pageData.section = section || '';
124                 pageData.expiry = getExpiryDate( editRecoveryExpiry );
126                 const transaction = db.transaction( objectStoreName, 'readwrite' );
127                 const objectStore = transaction.objectStore( objectStoreName );
129                 const request = objectStore.put( pageData );
130                 request.addEventListener( 'success', ( event ) => {
131                         resolve( event );
132                 } );
133                 request.addEventListener( 'error', ( event ) => {
134                         reject( 'Error saving data: ' + event.target.errorCode );
135                 } );
136         } );
140  * Delete data relating to a specific page
142  * @ignore
143  * @param {string} pageName The current page name (with underscores)
144  * @param {string|null} section The section ID, or null if the whole page is being edited
145  * @return {jQuery.Promise} Promise which resolves on success, or rejects with an error message.
146  */
147 function deleteData( pageName, section ) {
148         return new Promise( ( resolve, reject ) => {
149                 if ( !db ) {
150                         reject( 'DB not opened' );
151                 }
153                 const transaction = db.transaction( objectStoreName, 'readwrite' );
154                 const objectStore = transaction.objectStore( objectStoreName );
156                 const request = objectStore.delete( [ pageName, section || '' ] );
157                 request.addEventListener( 'success', resolve );
158                 request.addEventListener( 'error', () => {
159                         reject( 'Error opening cursor' );
160                 } );
161         } );
165  * Returns the date diff seconds in the future
167  * @ignore
168  * @param {number} diff Seconds in the future
169  * @return {number} Timestamp of diff days in the future
170  */
171 function getExpiryDate( diff ) {
172         return ( Date.now() / 1000 ) + diff;
176  * Delete expired data
178  * @ignore
179  * @return {jQuery.Promise} Promise which resolves on success, or rejects with an error message.
180  */
181 function deleteExpiredData() {
182         return new Promise( ( resolve, reject ) => {
183                 if ( !db ) {
184                         reject( 'DB not opened' );
185                 }
187                 const transaction = db.transaction( objectStoreName, 'readwrite' );
188                 const objectStore = transaction.objectStore( objectStoreName );
189                 const expiry = objectStore.index( 'expiry' );
190                 const now = Date.now() / 1000;
192                 const expired = expiry.getAll( IDBKeyRange.upperBound( now, true ) );
194                 expired.addEventListener( 'success', ( event ) => {
195                         const cursors = event.target.result;
196                         if ( cursors.length > 0 ) {
197                                 const deletions = [];
198                                 cursors.forEach( ( cursor ) => {
199                                         deletions.push( deleteData( cursor.pageName, cursor.section ) );
200                                 } );
201                                 Promise.all( deletions ).then( resolve );
202                         } else {
203                                 resolve();
204                         }
205                 } );
207                 expired.addEventListener( 'error', () => {
208                         reject( 'Error getting filtered data' );
209                 } );
210         } );
214  * Close database
216  * @ignore
217  */
218 function closeDatabase() {
219         if ( db ) {
220                 db.close();
221         }
224 module.exports = {
225         openDatabase: openDatabaseLocal,
226         closeDatabase,
227         loadData,
228         loadAllData,
229         saveData,
230         deleteData,
231         deleteExpiredData