From e3db4afe222a678cbe204bf07e558bae0bdfd683 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Tue, 28 Aug 2018 13:12:22 +0200 Subject: [PATCH] MOBILE-2471 sqlite: Escape multi-byte characters --- src/classes/site.ts | 5 ++- src/classes/sqlitedb.ts | 51 ++++++++++++++++++++--- src/config.json | 2 +- src/providers/update-manager.ts | 71 +++++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 8 deletions(-) diff --git a/src/classes/site.ts b/src/classes/site.ts index 503a6b279..07047aabf 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -960,9 +960,10 @@ export class CoreSite { this.logger.debug('Invalidate cache for key starting with: ' + key); - const sql = 'UPDATE ' + this.WS_CACHE_TABLE + ' SET expirationTime=0 WHERE key LIKE ?'; + const sql = 'UPDATE ' + this.WS_CACHE_TABLE + ' SET expirationTime=0 WHERE key LIKE ? ESCAPE ?'; + const params = [this.db.encodeValue(key).replace(/%/g, '\\%') + '%', '\\']; - return this.db.execute(sql, [key + '%']); + return this.db.execute(sql, params); } /** diff --git a/src/classes/sqlitedb.ts b/src/classes/sqlitedb.ts index 8e650767b..e69ee8350 100644 --- a/src/classes/sqlitedb.ts +++ b/src/classes/sqlitedb.ts @@ -259,6 +259,24 @@ export class SQLiteDB { return Promise.all(promises); } + /** + * Decode a value returned from the database if it's a string. + * + * @param {any} value The value. + * @return {any} The decoded string or the original value if it's not a string. + */ + decodeValue(value: any): any { + if (typeof value === 'string') { + try { + value = decodeURI(value); + } catch (ex) { + // Error, use the original value. + } + } + + return value; + } + /** * Delete the records from a table where all the given conditions met. * If conditions not specified, table is truncated. @@ -308,6 +326,16 @@ export class SQLiteDB { return this.execute(`DELETE FROM ${table} ${select}`, params); } + /** + * Encode a value that will be inserted into the database or compared to values in the database. + * + * @param {any} value The value. + * @return {any} The encoded string or the original value if it's not a string. + */ + encodeValue(value: any): any { + return (typeof value === 'string') ? encodeURI(value) : value; + } + /** * Execute a SQL query. * IMPORTANT: Use this function only if you cannot use any of the other functions in this API. Please take into account that @@ -352,6 +380,8 @@ export class SQLiteDB { const value = data[name]; if (typeof value == 'undefined') { delete data[name]; + } else { + data[name] = this.encodeValue(value); } } } @@ -456,7 +486,7 @@ export class SQLiteDB { params = items; } - return [sql, params]; + return [sql, params.map(this.encodeValue)]; } /** @@ -607,7 +637,11 @@ export class SQLiteDB { // Retrieve the records. const records = []; for (let i = 0; i < result.rows.length; i++) { - records.push(result.rows.item(i)); + const record = result.rows.item(i); + for (const key in record) { + record[key] = this.decodeValue(record[key]); + } + records.push(record); } return records; @@ -819,6 +853,8 @@ export class SQLiteDB { * @return {Promise} Promise resolved when updated. */ updateRecordsWhere(table: string, data: any, where?: string, whereParams?: any[]): Promise { + this.formatDataToInsert(data); + if (!data || !Object.keys(data).length) { // No fields to update, consider it's done. return Promise.resolve(); @@ -850,9 +886,10 @@ export class SQLiteDB { * Returns the SQL WHERE conditions. * * @param {object} [conditions] The conditions to build the where clause. Must not contain numeric indexes. + * @param {boolean} [encodeValues=true] Encode condiiton values. True by default. * @return {any[]} An array list containing sql 'where' part and 'params'. */ - whereClause(conditions: any = {}): any[] { + whereClause(conditions: any = {}, encodeValues: boolean = true): any[] { if (!conditions || !Object.keys(conditions).length) { return ['1 = 1', []]; } @@ -861,7 +898,11 @@ export class SQLiteDB { params = []; for (const key in conditions) { - const value = conditions[key]; + let value = conditions[key]; + + if (encodeValues) { + value = this.encodeValue(value); + } if (typeof value == 'undefined' || value === null) { where.push(key + ' IS NULL'); @@ -897,7 +938,7 @@ export class SQLiteDB { if (typeof value == 'undefined' || value === null) { select = field + ' IS NULL'; } else { - params.push(value); + params.push(this.encodeValue(value)); } }); diff --git a/src/config.json b/src/config.json index 80abf6de7..b2f1abf23 100644 --- a/src/config.json +++ b/src/config.json @@ -2,7 +2,7 @@ "app_id" : "com.moodle.moodlemobile", "appname": "Moodle Mobile", "desktopappname": "Moodle Desktop", - "versioncode" : 3510, + "versioncode" : 3520, "versionname" : "3.5.1", "cache_expiration_time" : 300000, "default_lang" : "en", diff --git a/src/providers/update-manager.ts b/src/providers/update-manager.ts index b4017eb1f..ef0a70a9c 100644 --- a/src/providers/update-manager.ts +++ b/src/providers/update-manager.ts @@ -346,6 +346,13 @@ export class CoreUpdateManagerProvider implements CoreInitHandler { // DBs migrated, get the version applied again. return this.configProvider.get(this.VERSION_APPLIED, 0); }); + } else if (versionCode >= 3520 && versionApplied < 3520 && versionApplied > 0) { + // Encode special characters in the contents of all DBs to work around Unicode bugs in the Cordova plugin. + // This is not needed if the DBs are created from scratch, because all content is already encoded. + // We do this before any other update because SQLiteDB methods expect the content to be encoded. + return this.encodeAllDBs().then(() => { + return versionApplied; + }); } else { return versionApplied; } @@ -706,4 +713,68 @@ export class CoreUpdateManagerProvider implements CoreInitHandler { return this.utils.allPromises(promises); }); } + + /** + * Encode all DBs to escape special characters. + * + * @return {Promise} Promise resolved when done. + */ + protected encodeAllDBs(): Promise { + // First encode the app DB. + return this.encodeDB(this.appProvider.getDB()).then(() => { + // Now encode all site DBs. + return this.sitesProvider.getSitesIds(); + }).then((ids) => { + return this.utils.allPromises(ids.map((siteId) => { + return this.sitesProvider.getSiteDb(siteId).then((db) => { + return this.encodeDB(db); + }); + })); + }); + } + + /** + * Encode content of a certain DB to escape special characters. + * + * @param {SQLiteDB} db The DB. + * @return {Promise} Promise resolved when done. + */ + protected encodeDB(db: SQLiteDB): Promise { + const sql = 'SELECT tbl_name FROM sqlite_master WHERE type = ?'; + const params = ['table']; + + return db.execute(sql, params).then((result) => { + const promises = []; + + for (let i = 0; i < result.rows.length; i++) { + const table = result.rows.item(i).tbl_name; + promises.push(this.encodeTable(db, table)); + } + + return this.utils.allPromises(promises); + }); + } + + /** + * Encode content of a certain table to escape special characters. + * + * @param {SQLiteDB} db The DB. + * @param {string} table Name of the table. + * @return {Promise} Promise resolved when done. + */ + protected encodeTable(db: SQLiteDB, table: string): Promise { + const sql = 'SELECT * FROM ' + table; + + return db.execute(sql).then((result) => { + const promises = []; + + for (let i = 0; i < result.rows.length; i++) { + const record = result.rows.item(i); + const selectAndParams = db.whereClause(record, false); + promises.push(db.updateRecordsWhere(table, record, selectAndParams[0], selectAndParams[1])); + } + + return this.utils.allPromises(promises); + }); + } }