d3bc693dab
The Scholar database is backed up on browser close. On startup, if the main database is damaged, the extension saves a copy of the damaged file and tries to restore from the last automatic backup. If it fails, it creates a new database file. New methods: Scholar.getScholarDatabase(string [ext]) Scholar.backupDatabase() Scholar.moveToUnique(file, newFile) -- find a unique filename using the leafName of newFile as the suggested name (using the built-in Mozilla functionality) and move the file there Scholar.Date.getFileDateString(file) Scholar.Date.getFileTimeString(file)
658 lines
No EOL
16 KiB
JavaScript
658 lines
No EOL
16 KiB
JavaScript
const SCHOLAR_CONFIG = {
|
|
GUID: 'scholar@chnm.gmu.edu',
|
|
DB_FILE: 'scholar.sqlite',
|
|
DB_REBUILD: false, // erase DB and recreate from schema
|
|
DEBUG_LOGGING: true,
|
|
DEBUG_TO_CONSOLE: true, // dump debug messages to console rather than (much slower) Debug Logger
|
|
REPOSITORY_URL: 'http://chnm.gmu.edu/firefoxscholar/repo',
|
|
REPOSITORY_CHECK_INTERVAL: 86400, // 24 hours
|
|
REPOSITORY_RETRY_INTERVAL: 3600 // 1 hour
|
|
};
|
|
|
|
/*
|
|
* Core functions
|
|
*/
|
|
var Scholar = new function(){
|
|
var _initialized = false;
|
|
var _shutdown = false;
|
|
var _localizedStringBundle;
|
|
|
|
// Privileged (public) methods
|
|
this.init = init;
|
|
this.shutdown = shutdown;
|
|
this.getProfileDirectory = getProfileDirectory;
|
|
this.getScholarDirectory = getScholarDirectory;
|
|
this.getStorageDirectory = getStorageDirectory;
|
|
this.getScholarDatabase = getScholarDatabase;
|
|
this.backupDatabase = backupDatabase;
|
|
this.debug = debug;
|
|
this.varDump = varDump;
|
|
this.getString = getString;
|
|
this.flattenArguments = flattenArguments;
|
|
this.join = join;
|
|
this.inArray = inArray;
|
|
this.arraySearch = arraySearch;
|
|
this.randomString = randomString;
|
|
this.getRandomID = getRandomID;
|
|
this.moveToUnique = moveToUnique;
|
|
|
|
// Public properties
|
|
this.version;
|
|
|
|
/*
|
|
* Initialize the extension
|
|
*/
|
|
function init(){
|
|
if (_initialized){
|
|
return false;
|
|
}
|
|
|
|
// Register shutdown handler to call Scholar.shutdown()
|
|
var observerService = Components.classes["@mozilla.org/observer-service;1"]
|
|
.getService(Components.interfaces.nsIObserverService);
|
|
observerService.addObserver({
|
|
observe: function(subject, topic, data){
|
|
Scholar.shutdown(subject, topic, data)
|
|
}
|
|
}, "xpcom-shutdown", false);
|
|
|
|
// Load in the preferences branch for the extension
|
|
Scholar.Prefs.init();
|
|
|
|
// Load in the extension version from the extension manager
|
|
var nsIUpdateItem = Components.interfaces.nsIUpdateItem;
|
|
var gExtensionManager =
|
|
Components.classes["@mozilla.org/extensions/manager;1"]
|
|
.getService(Components.interfaces.nsIExtensionManager);
|
|
var itemType = nsIUpdateItem.TYPE_EXTENSION;
|
|
this.version
|
|
= gExtensionManager.getItemForID(SCHOLAR_CONFIG['GUID']).version;
|
|
|
|
|
|
// Load in the localization stringbundle for use by getString(name)
|
|
var src = 'chrome://scholar/locale/scholar.properties';
|
|
var localeService =
|
|
Components.classes["@mozilla.org/intl/nslocaleservice;1"]
|
|
.getService(Components.interfaces.nsILocaleService);
|
|
var appLocale = localeService.getApplicationLocale();
|
|
var stringBundleService =
|
|
Components.classes["@mozilla.org/intl/stringbundle;1"]
|
|
.getService(Components.interfaces.nsIStringBundleService);
|
|
_localizedStringBundle = stringBundleService.createBundle(src, appLocale);
|
|
|
|
// Trigger updating of schema and scrapers
|
|
Scholar.Schema.updateSchema();
|
|
Scholar.Schema.updateScrapersRemote();
|
|
|
|
_initialized = true;
|
|
return true;
|
|
}
|
|
|
|
|
|
function shutdown(subject, topic, data){
|
|
// Called twice otherwise, for some reason
|
|
if (_shutdown){
|
|
return false;
|
|
}
|
|
|
|
_shutdown = true;
|
|
|
|
Scholar.backupDatabase();
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
function getProfileDirectory(){
|
|
return Components.classes["@mozilla.org/file/directory_service;1"]
|
|
.getService(Components.interfaces.nsIProperties)
|
|
.get("ProfD", Components.interfaces.nsIFile);
|
|
}
|
|
|
|
|
|
function getScholarDirectory(){
|
|
var file = Scholar.getProfileDirectory();
|
|
|
|
file.append('scholar');
|
|
// If it doesn't exist, create
|
|
if (!file.exists() || !file.isDirectory()){
|
|
file.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0664);
|
|
}
|
|
return file;
|
|
}
|
|
|
|
|
|
function getStorageDirectory(){
|
|
var file = Scholar.getScholarDirectory();
|
|
|
|
file.append('storage');
|
|
// If it doesn't exist, create
|
|
if (!file.exists() || !file.isDirectory()){
|
|
file.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0664);
|
|
}
|
|
return file;
|
|
}
|
|
|
|
function getScholarDatabase(ext){
|
|
ext = ext ? '.' + ext : '';
|
|
|
|
var file = Scholar.getScholarDirectory();
|
|
file.append(SCHOLAR_CONFIG['DB_FILE'] + ext);
|
|
return file;
|
|
}
|
|
|
|
|
|
/*
|
|
* Back up the main database file
|
|
*
|
|
* This could probably create a corrupt file fairly easily if all changes
|
|
* haven't been flushed to disk -- proceed with caution
|
|
*/
|
|
function backupDatabase(){
|
|
if (Scholar.DB.transactionInProgress()){
|
|
Scholar.debug('Transaction in progress--skipping DB backup', 2);
|
|
return false;
|
|
}
|
|
|
|
Scholar.debug('Backing up database');
|
|
|
|
var file = Scholar.getScholarDatabase();
|
|
var backupFile = Scholar.getScholarDatabase('bak');
|
|
|
|
// Copy via a temporary file so we don't run into disk space issues
|
|
// after deleting the old backup file
|
|
var tmpFile = Scholar.getScholarDatabase('tmp');
|
|
if (tmpFile.exists()){
|
|
tmpFile.remove(null);
|
|
}
|
|
|
|
try {
|
|
file.copyTo(file.parent, tmpFile.leafName);
|
|
}
|
|
catch (e){
|
|
// TODO: deal with low disk space
|
|
throw (e);
|
|
}
|
|
|
|
// Remove old backup file
|
|
if (backupFile.exists()){
|
|
backupFile.remove(null);
|
|
}
|
|
|
|
tmpFile.moveTo(tmpFile.parent, backupFile.leafName);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/*
|
|
* Debug logging function
|
|
*
|
|
* Uses DebugLogger extension available from http://mozmonkey.com/debuglogger/
|
|
* if available, otherwise the console (in which case boolean browser.dom.window.dump.enabled
|
|
* must be created and set to true in about:config)
|
|
*
|
|
* Defaults to log level 3 if level not provided
|
|
*/
|
|
function debug(message, level) {
|
|
if (!SCHOLAR_CONFIG['DEBUG_LOGGING']){
|
|
return false;
|
|
}
|
|
|
|
if (typeof message!='string'){
|
|
message = Scholar.varDump(message);
|
|
}
|
|
|
|
if (!level){
|
|
level = 3;
|
|
}
|
|
|
|
if (!SCHOLAR_CONFIG['DEBUG_TO_CONSOLE']){
|
|
try {
|
|
var logManager =
|
|
Components.classes["@mozmonkey.com/debuglogger/manager;1"]
|
|
.getService(Components.interfaces.nsIDebugLoggerManager);
|
|
var logger = logManager.registerLogger("Scholar for Firefox");
|
|
}
|
|
catch (e){}
|
|
}
|
|
|
|
if (logger){
|
|
logger.log(level, message);
|
|
}
|
|
else {
|
|
dump('scholar(' + level + '): ' + message + "\n\n");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* PHP var_dump equivalent for JS
|
|
*
|
|
* Adapted from http://binnyva.blogspot.com/2005/10/dump-function-javascript-equivalent-of.html
|
|
*/
|
|
function varDump(arr,level) {
|
|
var dumped_text = "";
|
|
if (!level){
|
|
level = 0;
|
|
}
|
|
|
|
// The padding given at the beginning of the line.
|
|
var level_padding = "";
|
|
for (var j=0;j<level+1;j++){
|
|
level_padding += " ";
|
|
}
|
|
|
|
if (typeof(arr) == 'object') { // Array/Hashes/Objects
|
|
for (var item in arr) {
|
|
var value = arr[item];
|
|
|
|
if (typeof(value) == 'object') { // If it is an array,
|
|
dumped_text += level_padding + "'" + item + "' ...\n";
|
|
dumped_text += arguments.callee(value,level+1);
|
|
}
|
|
else {
|
|
if (typeof value == 'function'){
|
|
dumped_text += level_padding + "'" + item + "' => function(...){...} \n";
|
|
}
|
|
else {
|
|
dumped_text += level_padding + "'" + item + "' => \"" + value + "\"\n";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else { // Stings/Chars/Numbers etc.
|
|
dumped_text = "===>"+arr+"<===("+typeof(arr)+")";
|
|
}
|
|
return dumped_text;
|
|
}
|
|
|
|
|
|
function getString(name){
|
|
try {
|
|
var l10n = _localizedStringBundle.GetStringFromName(name);
|
|
}
|
|
catch (e){
|
|
throw ('Localized string not available for ' + name);
|
|
}
|
|
return l10n;
|
|
}
|
|
|
|
|
|
/*
|
|
* Flattens mixed arrays/values in a passed _arguments_ object and returns
|
|
* an array of values -- allows for functions to accept both arrays of
|
|
* values and/or an arbitrary number of individual values
|
|
*/
|
|
function flattenArguments(args){
|
|
// Put passed scalar values into an array
|
|
if (typeof args!='object'){
|
|
args = [args];
|
|
}
|
|
|
|
var returns = new Array();
|
|
|
|
for (var i=0; i<args.length; i++){
|
|
if (typeof args[i]=='object'){
|
|
for (var j=0; j<args[i].length; j++){
|
|
returns.push(args[i][j]);
|
|
}
|
|
}
|
|
else {
|
|
returns.push(args[i]);
|
|
}
|
|
}
|
|
|
|
return returns;
|
|
}
|
|
|
|
|
|
/*
|
|
* A version of join() that operates externally for use on objects other
|
|
* than arrays (e.g. _arguments_)
|
|
*
|
|
* Note that this is safer than extending Object()
|
|
*/
|
|
function join(obj, delim){
|
|
var a = [];
|
|
for (var i=0, len=obj.length; i<len; i++){
|
|
a.push(obj[i]);
|
|
}
|
|
return a.join(delim);
|
|
}
|
|
|
|
|
|
/*
|
|
* PHP's in_array() for JS -- returns true if a value is contained in
|
|
* an array, false otherwise
|
|
*/
|
|
function inArray(needle, haystack){
|
|
for (var i in haystack){
|
|
if (haystack[i]==needle){
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/*
|
|
* PHP's array_search() for JS -- searches an array for a value and
|
|
* returns the key if found, false otherwise
|
|
*/
|
|
function arraySearch(needle, haystack){
|
|
for (var i in haystack){
|
|
if (haystack[i]==needle){
|
|
return i;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Generate a random string of length 'len' (defaults to 8)
|
|
**/
|
|
function randomString(len) {
|
|
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
|
|
if (!len){
|
|
len = 8;
|
|
}
|
|
var randomstring = '';
|
|
for (var i=0; i<len; i++) {
|
|
var rnum = Math.floor(Math.random() * chars.length);
|
|
randomstring += chars.substring(rnum,rnum+1);
|
|
}
|
|
return randomstring;
|
|
}
|
|
|
|
|
|
/**
|
|
* Find a unique random id for use in a DB table
|
|
**/
|
|
function getRandomID(table, column, max){
|
|
if (!table){
|
|
throw('SQL query not provided');
|
|
}
|
|
|
|
if (!column){
|
|
throw('SQL query not provided');
|
|
}
|
|
|
|
var sql = 'SELECT COUNT(*) FROM ' + table + ' WHERE ' + column + '=';
|
|
|
|
if (!max){
|
|
max = 16383;
|
|
}
|
|
|
|
var tries = 3; // # of tries to find a unique id
|
|
do {
|
|
// If no luck after number of tries, try a larger range
|
|
if (!tries){
|
|
max = max * 128;
|
|
}
|
|
var rnd = Math.floor(Math.random()*max);
|
|
var exists = Scholar.DB.valueQuery(sql + rnd);
|
|
tries--;
|
|
}
|
|
while (exists);
|
|
|
|
return rnd;
|
|
}
|
|
|
|
|
|
function moveToUnique(file, newFile){
|
|
newFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0644);
|
|
var newName = newFile.leafName;
|
|
newFile.remove(null);
|
|
|
|
// Move file to unique name
|
|
file.moveTo(newFile.parent, newName);
|
|
return file;
|
|
}
|
|
};
|
|
|
|
|
|
|
|
Scholar.Prefs = new function(){
|
|
// Privileged methods
|
|
this.init = init;
|
|
this.get = get;
|
|
this.set = set;
|
|
|
|
this.register = register;
|
|
this.unregister = unregister;
|
|
this.observe = observe;
|
|
|
|
// Public properties
|
|
this.prefBranch; // set in Scholar.init()
|
|
|
|
function init(){
|
|
var prefs = Components.classes["@mozilla.org/preferences-service;1"]
|
|
.getService(Components.interfaces.nsIPrefService);
|
|
this.prefBranch = prefs.getBranch("extensions.scholar.");
|
|
|
|
// Register observer to handle pref changes
|
|
this.register();
|
|
}
|
|
|
|
|
|
/**
|
|
* Retrieve a preference
|
|
**/
|
|
function get(pref){
|
|
try {
|
|
switch (this.prefBranch.getPrefType(pref)){
|
|
case this.prefBranch.PREF_BOOL:
|
|
return this.prefBranch.getBoolPref(pref);
|
|
case this.prefBranch.PREF_STRING:
|
|
return this.prefBranch.getCharPref(pref);
|
|
case this.prefBranch.PREF_INT:
|
|
return this.prefBranch.getIntPref(pref);
|
|
}
|
|
}
|
|
catch (e){
|
|
throw ("Invalid preference '" + pref + "'");
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Set a preference
|
|
**/
|
|
function set(pref, value){
|
|
try {
|
|
switch (this.prefBranch.getPrefType(pref)){
|
|
case this.prefBranch.PREF_BOOL:
|
|
return this.prefBranch.setBoolPref(pref, value);
|
|
case this.prefBranch.PREF_STRING:
|
|
return this.prefBranch.setCharPref(pref, value);
|
|
case this.prefBranch.PREF_INT:
|
|
return this.prefBranch.setIntPref(pref, value);
|
|
}
|
|
}
|
|
catch (e){
|
|
throw ("Invalid preference '" + pref + "'");
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// Methods to register a preferences observer
|
|
//
|
|
function register(){
|
|
this.prefBranch.QueryInterface(Components.interfaces.nsIPrefBranch2);
|
|
this.prefBranch.addObserver("", this, false);
|
|
}
|
|
|
|
function unregister(){
|
|
if (!this.prefBranch){
|
|
return;
|
|
}
|
|
this.prefBranch.removeObserver("", this);
|
|
}
|
|
|
|
function observe(subject, topic, data){
|
|
if(topic!="nsPref:changed"){
|
|
return;
|
|
}
|
|
// subject is the nsIPrefBranch we're observing (after appropriate QI)
|
|
// data is the name of the pref that's been changed (relative to subject)
|
|
switch (data){
|
|
case "automaticScraperUpdates":
|
|
if (this.get('automaticScraperUpdates')){
|
|
Scholar.Schema.updateScrapersRemote();
|
|
}
|
|
else {
|
|
Scholar.Schema.stopRepositoryTimer();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Class for creating hash arrays that behave a bit more sanely
|
|
*
|
|
* Hashes can be created in the constructor by alternating key and val:
|
|
*
|
|
* var hasharray = new Scholar.Hash('foo','foovalue','bar','barvalue');
|
|
*
|
|
* Or using hasharray.set(key, val)
|
|
*
|
|
* _val_ defaults to true if not provided
|
|
*
|
|
* If using foreach-style looping, be sure to use _for (i in arr.items)_
|
|
* rather than just _for (i in arr)_, or else you'll end up with the
|
|
* methods and members instead of the hash items
|
|
*
|
|
* Most importantly, hasharray.length will work as expected, even with
|
|
* non-numeric keys
|
|
*
|
|
* Adapated from http://www.mojavelinux.com/articles/javascript_hashes.html
|
|
* (c) Mojavelinux, Inc.
|
|
* License: Creative Commons
|
|
**/
|
|
Scholar.Hash = function(){
|
|
this.length = 0;
|
|
this.items = new Array();
|
|
|
|
// Public methods defined on prototype below
|
|
|
|
for (var i = 0; i < arguments.length; i += 2) {
|
|
if (typeof(arguments[i + 1]) != 'undefined') {
|
|
this.items[arguments[i]] = arguments[i + 1];
|
|
this.length++;
|
|
}
|
|
}
|
|
}
|
|
|
|
Scholar.Hash.prototype.get = function(in_key){
|
|
return this.items[in_key];
|
|
}
|
|
|
|
Scholar.Hash.prototype.set = function(in_key, in_value){
|
|
// Default to a boolean hash if value not provided
|
|
if (typeof(in_value) == 'undefined'){
|
|
in_value = true;
|
|
}
|
|
|
|
if (typeof(this.items[in_key]) == 'undefined') {
|
|
this.length++;
|
|
}
|
|
|
|
this.items[in_key] = in_value;
|
|
|
|
return in_value;
|
|
}
|
|
|
|
Scholar.Hash.prototype.remove = function(in_key){
|
|
var tmp_value;
|
|
if (typeof(this.items[in_key]) != 'undefined') {
|
|
this.length--;
|
|
var tmp_value = this.items[in_key];
|
|
delete this.items[in_key];
|
|
}
|
|
|
|
return tmp_value;
|
|
}
|
|
|
|
Scholar.Hash.prototype.has = function(in_key){
|
|
return typeof(this.items[in_key]) != 'undefined';
|
|
}
|
|
|
|
|
|
|
|
Scholar.Date = new function(){
|
|
this.sqlToDate = sqlToDate;
|
|
this.getFileDateString = getFileDateString;
|
|
this.getFileTimeString = getFileTimeString;
|
|
|
|
/**
|
|
* Convert an SQL date in the form '2006-06-13 11:03:05' into a JS Date object
|
|
*
|
|
* Can also accept just the date part (e.g. '2006-06-13')
|
|
**/
|
|
function sqlToDate(sqldate){
|
|
try {
|
|
var datetime = sqldate.split(' ');
|
|
var dateparts = datetime[0].split('-');
|
|
if (datetime[1]){
|
|
var timeparts = datetime[1].split(':');
|
|
}
|
|
else {
|
|
timeparts = [false, false, false];
|
|
}
|
|
return new Date(dateparts[0], dateparts[1]-1, dateparts[2],
|
|
timeparts[0], timeparts[1], timeparts[2]);
|
|
}
|
|
catch (e){
|
|
Scholar.debug(sqldate + ' is not a valid SQL date', 2)
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
function getFileDateString(file){
|
|
var date = new Date();
|
|
date.setTime(file.lastModifiedTime);
|
|
return date.toLocaleDateString();
|
|
}
|
|
|
|
|
|
function getFileTimeString(file){
|
|
var date = new Date();
|
|
date.setTime(file.lastModifiedTime);
|
|
return date.toLocaleTimeString();
|
|
}
|
|
}
|
|
|
|
Scholar.Browser = new function() {
|
|
this.createHiddenBrowser = createHiddenBrowser;
|
|
this.deleteHiddenBrowser = deleteHiddenBrowser;
|
|
|
|
function createHiddenBrowser(myWindow) {
|
|
if(!myWindow) {
|
|
var myWindow = Components.classes["@mozilla.org/appshell/appShellService;1"]
|
|
.getService(Components.interfaces.nsIAppShellService)
|
|
.hiddenDOMWindow;
|
|
}
|
|
|
|
// Create a hidden browser
|
|
var newHiddenBrowser = myWindow.document.createElement("browser");
|
|
var windows = myWindow.document.getElementsByTagName("window");
|
|
windows[0].appendChild(newHiddenBrowser);
|
|
Scholar.debug("created hidden browser");
|
|
return newHiddenBrowser;
|
|
}
|
|
|
|
function deleteHiddenBrowser(myBrowser) {
|
|
// Delete a hidden browser
|
|
myBrowser.parentNode.removeChild(myBrowser);
|
|
delete myBrowser;
|
|
Scholar.debug("deleted hidden browser");
|
|
}
|
|
} |