2  * This file is part of WolneLektury-Mobile, licensed under GNU Affero GPLv3 or later.
 
   3  * Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 
   8 var WL_INITIAL = WL + '/media/api/mobile/initial/initial.db';
 
   9 var WL_UPDATE = WL + '/api/changes/SINCE.json?book_fields=author,html,parent,parent_number,sort_key,title' +
 
  10                 '&tag_fields=books,category,name,sort_key' +
 
  11                 '&tag_categories=author,epoch,genre,kind';
 
  15 var categories = {'author': 'autor',
 
  22 // FIXME: htmlescape strings!
 
  25 // for preparing sql statements
 
  27 //   var s = new Sql("INSERT ... '{0}', '{1}' ...";
 
  28 //   s.prepare("abc", ...)
 
  29 var Sql = function(scheme) {
 
  33         self.sql_escape = function(term) {
 
  34                 return term.toString().replace("'", "''");
 
  37         self.prepare = function() {
 
  39                 return self.text.replace(/{(\d+)}/g, function(match, number) {
 
  40                         return self.sql_escape(args[parseInt(number)]);
 
  46 var Catalogue = new function() {
 
  47         /* API for database */
 
  52         this.init = function(success, error) {
 
  53                 console.log('Catalogue.init');
 
  55                 self.updateDB(function() {
 
  56                         self.db = window.openDatabase("wolnelektury", "1.0", "WL Catalogue", 1);
 
  59                                                 onFunctionCall: function(val) {
 
  60                                                         var re = new RegExp(val.getString(0));
 
  61                                                                 if (val.getString(1).match(re))
 
  67                                 self.db.createFunction("REGEXP", 2, regexp);*/
 
  71                                 error && error('Nie mogę otworzyć bazy danych: ' + err);
 
  75                         error && error('Błąd migracji: ' + err);
 
  79         self.sqlSanitize = function(term) {
 
  80                 return term.toString().replace("'", "''");
 
  84         /* check if DB needs updating and upload a fresh copy, if so */
 
  85         this.updateDB = function(success, error) {
 
  86                 var has_ver = window.localStorage.getItem('db_ver');
 
  87                 if (has_ver == DB_VER) {
 
  88                         console.log('db ok, skipping')
 
  93                 var done = function() {
 
  95                         window.localStorage.setItem('db_ver', DB_VER);
 
  96                         console.log('db updated');
 
 101                 // this is Android-specific for now
 
 102                 var android = device.version.split('.')[0];
 
 104                         self.upload_db_android(done, error);
 
 106                         error && error("Nieobsługiwana wersja systemu. Wymagany Android>=2.0.");
 
 111         this.upload_db_android = function(success, error) {
 
 112                 // TODO: this should be downloaded from teh net, not stored in res
 
 114                 console.log('upload db for Android 2.x+');
 
 115                 // upload databases description file
 
 117                 var dbname = "wolnelektury";
 
 118                 var db = window.openDatabase(dbname, "1.0", "WL Catalogue", 500000);
 
 120                         console.log('db created successfully');
 
 121                         DBPut.fetch(WL_INITIAL, function(data) {
 
 122                                 console.log('db fetch successful');
 
 123                                 success && success();
 
 125                                 error && error('Błąd podczas pobierania bazy danych: ' + data);
 
 128                         error && error('Błąd podczas inicjowania bazy danych: ' + data);
 
 133         this.withState = function(callback) {
 
 134                 self.db.transaction(function(tx) {
 
 135                         tx.executeSql("SELECT * FROM state", [], 
 
 136                                 function(tx, results) {
 
 137                                         if (results.rows.length) {
 
 138                                                 callback(results.rows.item(0));
 
 141                                                 callback({last_checked: 0});
 
 148         this.withBook = function(id, callback, error) {
 
 149                 console.log('withBook '+id)
 
 150                 self.db.transaction(function(tx) {
 
 151                         tx.executeSql("SELECT * FROM book WHERE id="+id, [], 
 
 152                                 function(tx, results) {
 
 153                                         if (results.rows.length) {
 
 154                                                 callback(results.rows.item(0));
 
 163         this.withBooks = function(ids, callback) {
 
 164                 console.log('withBooks ' + ids)
 
 165                 self.db.transaction(function(tx) {
 
 166                         tx.executeSql("SELECT * FROM book WHERE id IN ("+ids+") ORDER BY sort_key", [], 
 
 167                                 function(tx, results) {
 
 169                                         var count = results.rows.length;
 
 170                                         for (var i=0; i<count; ++i) {
 
 171                                                 items.push(results.rows.item(i));
 
 179         this.withChildren = function(id, callback) {
 
 180                 console.log('withChildren ' + id)
 
 181                 self.db.transaction(function(tx) {
 
 182                         tx.executeSql("SELECT * FROM book WHERE parent="+id+" ORDER BY parent_number, sort_key", [], 
 
 183                                 function(tx, results) {
 
 185                                         var count = results.rows.length;
 
 186                                         for (var i=0; i<count; ++i) {
 
 187                                                 books.push(results.rows.item(i));
 
 194         this.withTag = function(id, callback, error) {
 
 195                 console.log('withTag '+id)
 
 196                 self.db.transaction(function(tx) {
 
 197                         tx.executeSql("SELECT * FROM tag WHERE id="+id, [], 
 
 198                                 function(tx, results) {
 
 199                                         if (results.rows.length) {
 
 200                                                 callback(results.rows.item(0));
 
 209         this.withCategory = function(category, callback) {
 
 210                 console.log('withCategory ' + category)
 
 211                 self.db.transaction(function(tx) {
 
 212                         tx.executeSql("SELECT * FROM tag WHERE category='"+category+"' ORDER BY sort_key", [], 
 
 213                                 function(tx, results) {
 
 215                                         var count = results.rows.length;
 
 216                                         for (var i=0; i<count; ++i)
 
 217                                                 items.push(results.rows.item(i));
 
 224         /* takes a query, returns a list of {view,id,label} objects to a callback */
 
 225         this.withSearch = function(term, callback) {
 
 226                 console.log('searching...');
 
 227                 term =  term.replace(/^\s+|\s+$/g, '') ;
 
 230                 function booksFound(tx, results) {
 
 231                         var len = results.rows.length;
 
 232                         console.log('found books: ' + len);
 
 233                         for (var i=0; i<len; i++) {
 
 234                                 var item = results.rows.item(i);
 
 242                 function tagsFound(tx, results) {
 
 243                         var len = results.rows.length;
 
 244                         console.log('found tags: ' + len);
 
 245                         for (var i=0; i<len; i++) {
 
 246                                 var item = results.rows.item(i);
 
 252                         // TODO error handling
 
 258                 // TODO pliterki, start of the word match
 
 259                 self.db.transaction(function(tx) {
 
 260                         sql_term = self.sqlSanitize(term); // this is still insane, % and _
 
 261                         tx.executeSql("SELECT * FROM book WHERE title LIKE '%"+sql_term+"%' ORDER BY sort_key LIMIT 10", [],
 
 262                         //tx.executeSql("SELECT * FROM book WHERE title REGEXP '.*"+sql_term+".*' ORDER BY sort_key", [],
 
 263                                 function(tx, results) {
 
 265                                         booksFound(tx, results);
 
 266                                         // and proceed to tags
 
 267                                         tx.executeSql("SELECT * FROM tag WHERE name LIKE '%"+sql_term+"%' ORDER BY sort_key LIMIT 10",
 
 271                                         console.log('ERROR:search: '+err.code);
 
 277         self.chainSqls = function(sqls, success, error) {
 
 278                 self.db.transaction(function(tx) {
 
 279                         var do_next = function() {
 
 281                                         var sql = sqls.shift();
 
 283                                         tx.executeSql(sql, [], do_next, error);
 
 286                                         success && success();
 
 294         self.update = function(data, success, error) {
 
 295                 var addBookSql = new Sql("\
 
 296                         INSERT OR REPLACE INTO book \
 
 297                                 (id, title, html_file,  html_file_size, parent, parent_number, sort_key, pretty_size, authors) \
 
 299                                 ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}', '{8}')");
 
 300                 var addTagSql = new Sql("INSERT OR REPLACE INTO tag (id, category, name, sort_key, books) VALUES ('{0}', '{1}', '{2}', '{3}', '{4}')");
 
 305                         for (i in data.deleted.books) {
 
 306                                 var book_id = data.deleted.books[i];
 
 307                                 sqls.push("DELETE FROM book WHERE id=" + book_id);
 
 308                                 FileRepo.deleteIfExists(book_id);
 
 311                         for (i in data.deleted.tags) {
 
 312                                 var tag_id = data.deleted.tags[i];
 
 313                                 sqls.push("DELETE FROM tag WHERE id=" + tag_id);
 
 318                         for (i in data.updated.books) {
 
 319                                 var book = data.updated.books[i];
 
 320                                 if (!book.html) book.html = {};
 
 321                                 if (!book.html.url) book.html.url = '';
 
 322                                 if (!book.html.size) book.html.size = '';
 
 323                                 if (!book.parent) book.parent = '';
 
 324                                 if (!book.parent_number) book.parent_number = '';
 
 325                                 var pretty_size = prettySize(book.html.size);
 
 326                                 sqls.push(addBookSql.prepare(
 
 327                                         book.id, book.title, book.html.url, book.html.size,
 
 328                                         book.parent, book.parent_number, book.sort_key, pretty_size, book.author
 
 330                                 FileRepo.deleteIfExists(book.id);
 
 333                         for (i in data.updated.tags) {
 
 334                                 var tag = data.updated.tags[i];
 
 335                                 var category = categories[tag.category];
 
 336                                 var books = tag.books.join(',');
 
 337                                 sqls.push(addTagSql.prepare(tag.id, category, tag.name, tag.sort_key, books));
 
 341                 sqls.push("UPDATE state SET last_checked=" + data.time_checked);
 
 343                 self.chainSqls(sqls, success, error);
 
 347         this.sync = function(success, error) {
 
 348                 self.withState(function(state) {
 
 349                         var url = WL_UPDATE.replace("SINCE", state.last_checked); 
 
 350                         console.log('sync: ' + url);
 
 351                         var xhr = new XMLHttpRequest();
 
 352                         xhr.open("GET", url);
 
 353                         xhr.onload = function() {
 
 354                                 console.log('sync: fetched by ajax: ' + url);                   
 
 355                                 self.update(JSON.parse(xhr.responseText), success, error);
 
 357                         xhr.onerror = function(e) {
 
 358                                 error && error("Błąd aktualizacji bazy danych." + e);
 
 364         this.updateLocal = function() {
 
 365                 FileRepo.withLocal(function(local) {
 
 366                         self.db.transaction(function(tx) {
 
 367                                 tx.executeSql("UPDATE book SET _local=0", [], function(tx, results) {
 
 370                                         for (var i = 0; i < ll; i ++) {
 
 371                                                 ids.push(local[i].name);
 
 374                                         tx.executeSql("UPDATE book SET _local=1 where id in ("+ids+")"); 
 
 378                         self.db.transaction(function(tx) {
 
 379                                 tx.executeSql("UPDATE book SET _local=0");