MOBILE-2471 sqlite: Escape multi-byte characters

main
Albert Gasset 2018-08-28 13:12:22 +02:00
parent ab3da0f38c
commit e3db4afe22
4 changed files with 121 additions and 8 deletions

View File

@ -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);
}
/**

View File

@ -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<any>} Promise resolved when updated.
*/
updateRecordsWhere(table: string, data: any, where?: string, whereParams?: any[]): Promise<any> {
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));
}
});

View File

@ -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",

View File

@ -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<any>} Promise resolved when done.
*/
protected encodeAllDBs(): Promise<any> {
// 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<any>} Promise resolved when done.
*/
protected encodeDB(db: SQLiteDB): Promise<any> {
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<any>} Promise resolved when done.
*/
protected encodeTable(db: SQLiteDB, table: string): Promise<any> {
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);
});
}
}