zotero/chrome/content/zotero/xpcom/attachments.js
Dan Stillman 5787b230f7 Use asynchronous DB and file access for schema checks/updates
This change should improve Firefox startup time. If the Zotero pane is
opened before Zotero has initialized, the pane will be blocked with a
progress bar until it's done. Standalone currently does the same, but it
should be changed to just delay opening the window if possible.

The upgrade wizard has been disabled for schema upgrades, in the hope
that upgrades can be done in a way that won't break compatibility with
earlier versions (or that can at least be done in a way such that we can
put out point releases of the last major version that provide
compatibility during beta/post-upgrade periods).

This patch likely breaks many things, and definitely breaks connector
mode.

This change also removes upgrade steps for databases from Zotero 2.1b2
and earlier. Users with such databases will need to upgrade via Zotero
4.0.x first or delete their data directories and start anew. This should
only affect users who haven't opened Zotero since Nov. 2010.
2013-08-11 21:53:41 -04:00

1513 lines
43 KiB
JavaScript

/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2009 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
Zotero.Attachments = new function(){
this.LINK_MODE_IMPORTED_FILE = 0;
this.LINK_MODE_IMPORTED_URL = 1;
this.LINK_MODE_LINKED_FILE = 2;
this.LINK_MODE_LINKED_URL = 3;
this.BASE_PATH_PLACEHOLDER = 'attachments:';
this.importFromFile = importFromFile;
this.linkFromFile = linkFromFile;
this.importSnapshotFromFile = importSnapshotFromFile;
this.importFromURL = importFromURL;
this.linkFromURL = linkFromURL;
this.linkFromDocument = linkFromDocument;
this.importFromDocument = importFromDocument;
this.createMissingAttachment = createMissingAttachment;
this.getFileBaseNameFromItem = getFileBaseNameFromItem;
this.createDirectoryForItem = createDirectoryForItem;
this.createDirectoryForMissingItem = createDirectoryForMissingItem;
this.getStorageDirectory = getStorageDirectory;
this.getPath = getPath;
var self = this;
function importFromFile(file, sourceItemID, libraryID) {
Zotero.debug('Importing attachment from file');
var newName = Zotero.File.getValidFileName(file.leafName);
if (!file.isFile()) {
throw ("'" + file.leafName + "' must be a file in Zotero.Attachments.importFromFile()");
}
Zotero.DB.beginTransaction();
try {
// Create a new attachment
var attachmentItem = new Zotero.Item('attachment');
if (sourceItemID) {
var parentItem = Zotero.Items.get(sourceItemID);
attachmentItem.libraryID = parentItem.libraryID;
}
else if (libraryID) {
attachmentItem.libraryID = libraryID;
}
attachmentItem.setField('title', newName);
attachmentItem.setSource(sourceItemID);
attachmentItem.attachmentLinkMode = this.LINK_MODE_IMPORTED_FILE;
var itemID = attachmentItem.save();
attachmentItem = Zotero.Items.get(itemID);
// Create directory for attachment files within storage directory
var destDir = this.createDirectoryForItem(itemID);
// Point to copied file
var newFile = destDir.clone();
newFile.append(newName);
// Copy file to unique filename, which automatically shortens long filenames
newFile = Zotero.File.copyToUnique(file, newFile);
var mimeType = Zotero.MIME.getMIMETypeFromFile(newFile);
attachmentItem.attachmentMIMEType = mimeType;
attachmentItem.attachmentPath = this.getPath(newFile, this.LINK_MODE_IMPORTED_FILE);
attachmentItem.save();
Zotero.DB.commitTransaction();
// Determine charset and build fulltext index
_postProcessFile(itemID, newFile, mimeType);
}
catch (e){
// hmph
Zotero.DB.rollbackTransaction();
var msg = "Failed importing file " + file.path;
Components.utils.reportError(msg);
Zotero.debug(msg, 1);
try {
// Clean up
if (itemID) {
var itemDir = this.getStorageDirectory(itemID);
if (itemDir.exists()) {
itemDir.remove(true);
}
}
}
catch (e) {}
throw (e);
}
return itemID;
}
function linkFromFile(file, sourceItemID){
Zotero.debug('Linking attachment from file');
var title = file.leafName;
var mimeType = Zotero.MIME.getMIMETypeFromFile(file);
var itemID = _addToDB(file, null, title, this.LINK_MODE_LINKED_FILE, mimeType,
null, sourceItemID);
// Determine charset and build fulltext index
_postProcessFile(itemID, file, mimeType);
return itemID;
}
function importSnapshotFromFile(file, url, title, mimeType, charset, sourceItemID){
Zotero.debug('Importing snapshot from file');
var charsetID = charset ? Zotero.CharacterSets.getID(charset) : null;
Zotero.DB.beginTransaction();
try {
// Create a new attachment
var attachmentItem = new Zotero.Item('attachment');
if (sourceItemID) {
var parentItem = Zotero.Items.get(sourceItemID);
attachmentItem.libraryID = parentItem.libraryID;
}
attachmentItem.setField('title', title);
attachmentItem.setField('url', url);
attachmentItem.setSource(sourceItemID);
attachmentItem.attachmentLinkMode = this.LINK_MODE_IMPORTED_URL;
attachmentItem.attachmentMIMEType = mimeType;
attachmentItem.attachmentCharset = charset;
// DEBUG: this should probably insert access date too so as to
// create a proper item, but at the moment this is only called by
// translate.js, which sets the metadata fields itself
var itemID = attachmentItem.save();
attachmentItem = Zotero.Items.get(itemID)
var storageDir = Zotero.getStorageDirectory();
var destDir = this.getStorageDirectory(itemID);
_moveOrphanedDirectory(destDir);
file.parent.copyTo(storageDir, destDir.leafName);
// Point to copied file
var newFile = destDir.clone();
newFile.append(file.leafName);
attachmentItem.attachmentPath = this.getPath(newFile, this.LINK_MODE_IMPORTED_URL);
attachmentItem.save();
Zotero.DB.commitTransaction();
// Determine charset and build fulltext index
_postProcessFile(itemID, newFile, mimeType);
}
catch (e){
Zotero.DB.rollbackTransaction();
try {
// Clean up
if (itemID) {
var itemDir = this.getStorageDirectory(itemID);
if (itemDir.exists()) {
itemDir.remove(true);
}
}
}
catch (e) {}
throw (e);
}
return itemID;
}
function importFromURL(url, sourceItemID, forceTitle, forceFileBaseName, parentCollectionIDs,
mimeType, libraryID, callback, cookieSandbox) {
Zotero.debug('Importing attachment from URL');
if (sourceItemID && parentCollectionIDs) {
var msg = "parentCollectionIDs is ignored when sourceItemID is set in Zotero.Attachments.importFromURL()";
Zotero.debug(msg, 2);
Components.utils.reportError(msg);
parentCollectionIDs = undefined;
}
// Throw error on invalid URLs
//
// TODO: allow other schemes
var urlRe = /^https?:\/\/[^\s]*$/;
var matches = urlRe.exec(url);
if (!matches) {
if(callback) callback(false);
throw ("Invalid URL '" + url + "' in Zotero.Attachments.importFromURL()");
}
// Save using a hidden browser
var nativeHandlerImport = function () {
var browser = Zotero.HTTP.processDocuments(url, function() {
var importCallback = function (item) {
Zotero.Browser.deleteHiddenBrowser(browser);
if(callback) callback(item);
};
Zotero.Attachments.importFromDocument(browser.contentDocument,
sourceItemID, forceTitle, parentCollectionIDs, importCallback, libraryID);
}, undefined, undefined, true, cookieSandbox);
};
// Save using remote web browser persist
var externalHandlerImport = function (mimeType) {
if (forceFileBaseName) {
var ext = _getExtensionFromURL(url, mimeType);
var fileName = forceFileBaseName + (ext != '' ? '.' + ext : '');
}
else {
var fileName = _getFileNameFromURL(url, mimeType);
}
var title = forceTitle ? forceTitle : fileName;
const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
var wbp = Components
.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
.createInstance(nsIWBP);
wbp.persistFlags = nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
if(cookieSandbox) cookieSandbox.attachToInterfaceRequestor(wbp);
var encodingFlags = false;
Zotero.DB.beginTransaction();
try {
// Create a new attachment
var attachmentItem = new Zotero.Item('attachment');
if (libraryID) {
attachmentItem.libraryID = libraryID;
}
else if (sourceItemID) {
var parentItem = Zotero.Items.get(sourceItemID);
attachmentItem.libraryID = parentItem.libraryID;
}
attachmentItem.setField('title', title);
attachmentItem.setField('url', url);
attachmentItem.setField('accessDate', "CURRENT_TIMESTAMP");
attachmentItem.setSource(sourceItemID);
attachmentItem.attachmentLinkMode = Zotero.Attachments.LINK_MODE_IMPORTED_URL;
attachmentItem.attachmentMIMEType = mimeType;
var itemID = attachmentItem.save();
attachmentItem = Zotero.Items.get(itemID);
// Add to collections
if (parentCollectionIDs){
var ids = Zotero.flattenArguments(parentCollectionIDs);
for each(var id in ids){
var col = Zotero.Collections.get(id);
col.addItem(itemID);
}
}
// Create a new folder for this item in the storage directory
var destDir = Zotero.Attachments.createDirectoryForItem(itemID);
var file = destDir.clone();
file.append(fileName);
wbp.progressListener = new Zotero.WebProgressFinishListener(function(){
try {
var str = Zotero.File.getSample(file);
if (mimeType == 'application/pdf' &&
Zotero.MIME.sniffForMIMEType(str) != 'application/pdf') {
var errString = "Downloaded PDF did not have MIME type "
+ "'application/pdf' in Attachments.importFromURL()";
Zotero.debug(errString, 2);
Zotero.debug(str);
attachmentItem.erase();
if(callback) callback(false, new Error(errString));
return;
}
attachmentItem.attachmentPath =
Zotero.Attachments.getPath(
file, Zotero.Attachments.LINK_MODE_IMPORTED_URL
);
attachmentItem.save();
Zotero.Notifier.trigger('add', 'item', itemID);
Zotero.Notifier.trigger('modify', 'item', sourceItemID);
if(callback) callback(attachmentItem);
// We don't have any way of knowing that the file
// is flushed to disk, so we just wait a second
// and hope for the best -- we'll index it later
// if it fails
//
// TODO: index later
setTimeout(function() {
Zotero.Fulltext.indexItems([itemID]);
}, 1000);
}
catch (e) {
// Clean up
attachmentItem.erase();
if(callback) callback(false, e);
throw (e);
}
});
// Disable the Notifier during the commit
var disabled = Zotero.Notifier.disable();
// The attachment is still incomplete here, but we can't risk
// leaving the transaction open if the callback never triggers
Zotero.DB.commitTransaction();
if (disabled) {
Zotero.Notifier.enable();
}
var nsIURL = Components.classes["@mozilla.org/network/standard-url;1"]
.createInstance(Components.interfaces.nsIURL);
nsIURL.spec = url;
try {
wbp.saveURI(nsIURL, null, null, null, null, file);
} catch(e if e.name === "NS_ERROR_XPC_NOT_ENOUGH_ARGS") {
// https://bugzilla.mozilla.org/show_bug.cgi?id=794602
// XXX Always use when we no longer support Firefox < 18
wbp.saveURI(nsIURL, null, null, null, null, file, null);
}
return attachmentItem;
}
catch (e){
Zotero.DB.rollbackTransaction();
try {
// Clean up
if (itemID) {
var itemDir = this.getStorageDirectory(itemID);
if (itemDir.exists()) {
itemDir.remove(true);
}
}
}
catch (e) {}
throw (e);
}
}
var process = function (mimeType, hasNativeHandler) {
// If we can load this natively, use a hidden browser
// (so we can get the charset and title and index the document)
if (hasNativeHandler) {
nativeHandlerImport();
}
// Otherwise use a remote web page persist
else {
return externalHandlerImport(mimeType);
}
}
if (mimeType) {
return process(mimeType, Zotero.MIME.hasNativeHandler(mimeType));
}
else {
Zotero.MIME.getMIMETypeFromURL(url, function (mimeType, hasNativeHandler) {
process(mimeType, hasNativeHandler);
}, cookieSandbox);
}
}
/*
* Create a link attachment from a URL
*
* @param {String} url
* @param {Integer} sourceItemID Parent item
* @param {String} [mimeType] MIME type of page
* @param {String} [title] Title to use for attachment
*/
function linkFromURL(url, sourceItemID, mimeType, title){
Zotero.debug('Linking attachment from URL');
/* Throw error on invalid URLs
We currently accept the following protocols:
PersonalBrain (brain://)
DevonThink (x-devonthink-item://)
Notational Velocity (nv://)
MyLife Organized (mlo://)
Evernote (evernote://)
OneNote (onenote://)
Kindle (kindle://)
Logos (logosres:)
Zotero (zotero://) */
var urlRe = /^((https?|zotero|evernote|onenote|brain|nv|mlo|kindle|x-devonthink-item|ftp):\/\/|logosres:)[^\s]*$/;
var matches = urlRe.exec(url);
if (!matches) {
throw ("Invalid URL '" + url + "' in Zotero.Attachments.linkFromURL()");
}
// If no title provided, figure it out from the URL
if (!title){
title = url.substring(url.lastIndexOf('/')+1);
}
// Override MIME type to application/pdf if extension is .pdf --
// workaround for sites that respond to the HEAD request with an
// invalid MIME type (https://www.zotero.org/trac/ticket/460)
var ext = _getExtensionFromURL(url);
if (ext == 'pdf') {
mimeType = 'application/pdf';
}
var itemID = _addToDB(null, url, title, this.LINK_MODE_LINKED_URL,
mimeType, null, sourceItemID);
return itemID;
}
// TODO: what if called on file:// document?
function linkFromDocument(document, sourceItemID, parentCollectionIDs){
Zotero.debug('Linking attachment from document');
if (sourceItemID && parentCollectionIDs) {
var msg = "parentCollectionIDs is ignored when sourceItemID is set in Zotero.Attachments.linkFromDocument()";
Zotero.debug(msg, 2);
Components.utils.reportError(msg);
parentCollectionIDs = undefined;
}
var url = document.location.href;
var title = document.title; // TODO: don't use Mozilla-generated title for images, etc.
var mimeType = document.contentType;
var charsetID = Zotero.CharacterSets.getID(document.characterSet);
Zotero.DB.beginTransaction();
var itemID = _addToDB(null, url, title, this.LINK_MODE_LINKED_URL,
mimeType, charsetID, sourceItemID);
// Add to collections
if (parentCollectionIDs){
var ids = Zotero.flattenArguments(parentCollectionIDs);
for each(var id in ids){
var col = Zotero.Collections.get(id);
col.addItem(itemID);
}
}
Zotero.DB.commitTransaction();
// Run the fulltext indexer asynchronously (actually, it hangs the UI
// thread, but at least it lets the menu close)
setTimeout(function() {
if (Zotero.Fulltext.isCachedMIMEType(mimeType)) {
// No file, so no point running the PDF indexer
//Zotero.Fulltext.indexItems([itemID]);
}
else {
Zotero.Fulltext.indexDocument(document, itemID);
}
}, 50);
return itemID;
}
/*
* Save a snapshot -- uses synchronous WebPageDump or asynchronous saveURI()
*/
function importFromDocument(document, sourceItemID, forceTitle, parentCollectionIDs, callback, libraryID) {
Zotero.debug('Importing attachment from document');
if (sourceItemID && parentCollectionIDs) {
var msg = "parentCollectionIDs is ignored when sourceItemID is set in Zotero.Attachments.importFromDocument()";
Zotero.debug(msg, 2);
Components.utils.reportError(msg);
parentCollectionIDs = undefined;
}
var url = document.location.href;
var title = forceTitle ? forceTitle : document.title;
var mimeType = document.contentType;
if(Zotero.Attachments.isPDFJS(document)) {
mimeType = "application/pdf";
}
var charsetID = Zotero.CharacterSets.getID(document.characterSet);
if (!forceTitle) {
// Remove e.g. " - Scaled (-17%)" from end of images saved from links,
// though I'm not sure why it's getting added to begin with
if (mimeType.indexOf('image/') === 0) {
title = title.replace(/(.+ \([^,]+, [0-9]+x[0-9]+[^\)]+\)) - .+/, "$1" );
}
// If not native type, strip mime type data in parens
else if (!Zotero.MIME.hasNativeHandler(mimeType, _getExtensionFromURL(url))) {
title = title.replace(/(.+) \([a-z]+\/[^\)]+\)/, "$1" );
}
}
Zotero.DB.beginTransaction();
try {
// Create a new attachment
var attachmentItem = new Zotero.Item('attachment');
if (libraryID) {
attachmentItem.libraryID = libraryID;
}
else if (sourceItemID) {
var parentItem = Zotero.Items.get(sourceItemID);
attachmentItem.libraryID = parentItem.libraryID;
}
attachmentItem.setField('title', title);
attachmentItem.setField('url', url);
attachmentItem.setField('accessDate', "CURRENT_TIMESTAMP");
attachmentItem.setSource(sourceItemID);
attachmentItem.attachmentLinkMode = Zotero.Attachments.LINK_MODE_IMPORTED_URL;
attachmentItem.attachmentCharset = charsetID;
attachmentItem.attachmentMIMEType = mimeType;
var itemID = attachmentItem.save();
attachmentItem = Zotero.Items.get(itemID);
// Create a new folder for this item in the storage directory
var destDir = this.createDirectoryForItem(itemID);
var file = Components.classes["@mozilla.org/file/local;1"].
createInstance(Components.interfaces.nsILocalFile);
file.initWithFile(destDir);
var fileName = Zotero.File.truncateFileName(
_getFileNameFromURL(url, mimeType),
100 //make sure this matches WPD settings in webpagedump/common.js
);
file.append(fileName)
if (mimeType == 'application/pdf') {
var f = function() {
Zotero.Fulltext.indexPDF(file, itemID);
Zotero.Notifier.trigger('refresh', 'item', itemID);
};
}
else {
var f = function() {
Zotero.Fulltext.indexDocument(document, itemID);
Zotero.Notifier.trigger('refresh', 'item', itemID);
if (callback) {
callback(attachmentItem);
}
};
}
if (mimeType === 'text/html' || mimeType === 'application/xhtml+xml') {
var sync = true;
// Load WebPageDump code
var wpd = {"Zotero":Zotero};
Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Components.interfaces.mozIJSSubScriptLoader)
.loadSubScript("chrome://zotero/content/webpagedump/common.js", wpd);
Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Components.interfaces.mozIJSSubScriptLoader)
.loadSubScript("chrome://zotero/content/webpagedump/domsaver.js", wpd);
wpd.wpdDOMSaver.init(file.path, document);
wpd.wpdDOMSaver.saveHTMLDocument();
attachmentItem.attachmentPath = this.getPath(
file, Zotero.Attachments.LINK_MODE_IMPORTED_URL
);
attachmentItem.save();
}
else {
Zotero.debug('Saving with saveURI()');
const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
var wbp = Components
.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
.createInstance(nsIWBP);
wbp.persistFlags = nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION
| nsIWBP.PERSIST_FLAGS_FROM_CACHE;
var ioService = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
var nsIURL = ioService.newURI(url, null, null);
wbp.progressListener = new Zotero.WebProgressFinishListener(function () {
try {
attachmentItem.attachmentPath = Zotero.Attachments.getPath(
file,
Zotero.Attachments.LINK_MODE_IMPORTED_URL
);
attachmentItem.save();
Zotero.Notifier.trigger('add', 'item', itemID);
// We don't have any way of knowing that the file is flushed to
// disk, so we just wait a second and hope for the best --
// we'll index it later if it fails
//
// TODO: index later
setTimeout(function () {
f();
}, 1000);
}
catch (e) {
// Clean up
var item = Zotero.Items.get(itemID);
item.erase();
if(callback) callback(false, e);
throw (e);
}
});
try {
wbp.saveURI(nsIURL, null, null, null, null, file);
} catch(e if e.name === "NS_ERROR_XPC_NOT_ENOUGH_ARGS") {
// https://bugzilla.mozilla.org/show_bug.cgi?id=794602
// XXX Always use when we no longer support Firefox < 18
wbp.saveURI(nsIURL, null, null, null, null, file, null);
}
}
// Add to collections
if (parentCollectionIDs){
var ids = Zotero.flattenArguments(parentCollectionIDs);
for each(var id in ids){
var col = Zotero.Collections.get(id);
col.addItem(itemID);
}
}
// Disable the Notifier during the commit if this is async
if (!sync) {
var disabled = Zotero.Notifier.disable();
}
Zotero.DB.commitTransaction();
if (disabled) {
Zotero.Notifier.enable();
}
if (sync) {
Zotero.Notifier.trigger('add', 'item', itemID);
// Wait a second before indexing (see note above)
setTimeout(function () {
f();
}, 1000);
}
// Caution: Take care using this itemID. The notifier may not yet have been called,
// so the attachment may not be available in, for example, the items list
return itemID;
}
catch (e) {
Zotero.DB.rollbackTransaction();
try {
// Clean up
if (itemID) {
var itemDir = this.getStorageDirectory(itemID);
if (itemDir.exists()) {
itemDir.remove(true);
}
}
}
catch (e) {}
throw (e);
}
}
/*
* Previous asynchronous snapshot method -- disabled in favor of WebPageDump
*/
/*
function importFromDocument(document, sourceItemID, forceTitle, parentCollectionIDs, callback){
Zotero.debug('Importing attachment from document');
var url = document.location.href;
var title = forceTitle ? forceTitle : document.title;
var mimeType = document.contentType;
var charsetID = Zotero.CharacterSets.getID(document.characterSet);
if (!forceTitle) {
// Remove e.g. " - Scaled (-17%)" from end of images saved from links,
// though I'm not sure why it's getting added to begin with
if (mimeType.indexOf('image/') === 0) {
title = title.replace(/(.+ \([^,]+, [0-9]+x[0-9]+[^\)]+\)) - .+/, "$1" );
}
// If not native type, strip mime type data in parens
else if (!Zotero.MIME.hasNativeHandler(mimeType, _getExtensionFromURL(url))) {
title = title.replace(/(.+) \([a-z]+\/[^\)]+\)/, "$1" );
}
}
const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
var wbp = Components
.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
.createInstance(nsIWBP);
wbp.persistFlags = nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
var encodingFlags = false;
Zotero.DB.beginTransaction();
try {
// Create a new attachment
var attachmentItem = new Zotero.Item('attachment');
attachmentItem.setField('title', title);
attachmentItem.setField('url', url);
attachmentItem.setField('accessDate', "CURRENT_TIMESTAMP");
// Don't send a Notifier event on the incomplete item
var disabled = Zotero.Notifier.disable();
attachmentItem.save();
if (disabled) {
Zotero.Notifier.enable();
}
var itemID = attachmentItem.getID();
// Create a new folder for this item in the storage directory
var destDir = this.createDirectoryForItem(itemID);
var file = Components.classes["@mozilla.org/file/local;1"].
createInstance(Components.interfaces.nsILocalFile);
file.initWithFile(destDir);
var fileName = _getFileNameFromURL(url, mimeType);
file.append(fileName);
wbp.progressListener = new Zotero.WebProgressFinishListener(function(){
try {
Zotero.DB.beginTransaction();
_addToDB(file, url, title, Zotero.Attachments.LINK_MODE_IMPORTED_URL, mimeType,
charsetID, sourceItemID, itemID);
Zotero.Notifier.trigger('add', 'item', itemID);
// Add to collections
if (parentCollectionIDs){
var ids = Zotero.flattenArguments(parentCollectionIDs);
for each(var id in ids){
var col = Zotero.Collections.get(id);
col.addItem(itemID);
}
}
Zotero.DB.commitTransaction();
}
catch (e) {
Zotero.DB.rollbackTransaction();
// Clean up
if (itemID) {
var item = Zotero.Items.get(itemID);
if (item) {
item.erase();
}
try {
var destDir = Zotero.getStorageDirectory();
destDir.append(itemID);
if (destDir.exists()) {
destDir.remove(true);
}
}
catch (e) {}
}
throw (e);
}
Zotero.Fulltext.indexDocument(document, itemID);
if (callback) {
callback();
}
});
// The attachment is still incomplete here, but we can't risk
// leaving the transaction open if the callback never triggers
Zotero.DB.commitTransaction();
if (mimeType == 'text/html') {
Zotero.debug('Saving with saveDocument() to ' + destDir.path);
wbp.saveDocument(document, file, destDir, mimeType, encodingFlags, false);
}
else {
Zotero.debug('Saving with saveURI()');
var ioService = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
var nsIURL = ioService.newURI(url, null, null);
wbp.saveURI(nsIURL, null, null, null, null, file);
}
}
catch (e) {
Zotero.DB.rollbackTransaction();
try {
// Clean up
if (itemID) {
var destDir = Zotero.getStorageDirectory();
destDir.append(itemID);
if (destDir.exists()) {
destDir.remove(true);
}
}
}
catch (e) {}
throw (e);
}
}
*/
/*
* Create a new attachment with a missing file
*/
function createMissingAttachment(linkMode, file, url, title, mimeType, charset, sourceItemID) {
if (linkMode == this.LINK_MODE_LINKED_URL) {
throw ('Zotero.Attachments.createMissingAttachment() cannot be used to create linked URLs');
}
var charsetID = charset ? Zotero.CharacterSets.getID(charset) : null;
return _addToDB(file, url, title, linkMode, mimeType,
charsetID, sourceItemID);
}
/*
* Returns a formatted string to use as the basename of an attachment
* based on the metadata of the specified item and a format string
*
* (Optional) |formatString| specifies the format string -- otherwise
* the 'attachmentRenameFormatString' pref is used
*
* Valid substitution markers:
*
* %c -- firstCreator
* %y -- year (extracted from Date field)
* %t -- title
*
* Fields can be truncated to a certain length by appending an integer
* within curly brackets -- e.g. %t{50} truncates the title to 50 characters
*/
function getFileBaseNameFromItem(itemID, formatString) {
if (!formatString) {
formatString = Zotero.Prefs.get('attachmentRenameFormatString');
}
var item = Zotero.Items.get(itemID);
if (!item) {
throw ('Invalid itemID ' + itemID + ' in Zotero.Attachments.getFileBaseNameFromItem()');
}
// Replaces the substitution marker with the field value,
// truncating based on the {[0-9]+} modifier if applicable
function rpl(field, str) {
if (!str) {
str = formatString;
}
switch (field) {
case 'creator':
field = 'firstCreator';
var rpl = '%c';
break;
case 'year':
var rpl = '%y';
break;
case 'title':
var rpl = '%t';
break;
}
switch (field) {
case 'year':
var value = item.getField('date', true, true);
if (value) {
value = Zotero.Date.multipartToSQL(value).substr(0, 4);
if (value == '0000') {
value = '';
}
}
break;
default:
var value = '' + item.getField(field, false, true);
}
var re = new RegExp("\{?([^%\{\}]*)" + rpl + "(\{[0-9]+\})?" + "([^%\{\}]*)\}?");
// If no value for this field, strip entire conditional block
// (within curly braces)
if (!value) {
if (str.match(re)) {
return str.replace(re, '')
}
}
var f = function(match, p1, p2, p3) {
var maxChars = p2 ? p2.replace(/[^0-9]+/g, '') : false;
return p1 + (maxChars ? value.substr(0, maxChars) : value) + p3;
}
return str.replace(re, f);
}
formatString = rpl('creator');
formatString = rpl('year');
formatString = rpl('title');
formatString = Zotero.File.getValidFileName(formatString);
return formatString;
}
/*
* Create directory for attachment files within storage directory
*
* @param integer itemID Item id
*
* If a directory exists with the same name, move it to orphaned-files
*/
function createDirectoryForItem(itemID) {
var dir = this.getStorageDirectory(itemID);
_moveOrphanedDirectory(dir);
if (!dir.exists()) {
dir.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0755);
}
return dir;
}
/*
* Create directory for missing attachment files within storage directory
*
* @param string key Item secondary lookup key
*/
function createDirectoryForMissingItem(key) {
var dir = this.getStorageDirectoryByKey(key);
if (!dir.exists()) {
dir.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0755);
}
return dir;
}
function getStorageDirectory(itemID) {
if (!itemID) {
throw ("itemID not provided in Zotero.Attachments.getStorageDirectory()");
}
var item = Zotero.Items.get(itemID);
if (!item) {
throw ("Item " + itemID + " not found in Zotero.Attachments.getStorageDirectory()");
}
if (!item.key) {
throw ("No item key in Zotero.Attachments.getStorageDirectory()");
}
var dir = Zotero.getStorageDirectory();
dir.append(item.key);
return dir;
}
this.getStorageDirectoryByKey = function (key) {
if (typeof key != 'string' || !key.match(/^[A-Z0-9]{8}$/)) {
throw ('key must be an 8-character string in '
+ 'Zotero.Attachments.getStorageDirectoryByKey()')
}
var dir = Zotero.getStorageDirectory();
dir.append(key);
return dir;
}
/*
* Gets a relative descriptor for imported attachments and a persistent
* descriptor for files outside the storage directory
*/
function getPath(file, linkMode) {
file.QueryInterface(Components.interfaces.nsILocalFile);
if (linkMode == self.LINK_MODE_IMPORTED_URL ||
linkMode == self.LINK_MODE_IMPORTED_FILE) {
var fileName = file.getRelativeDescriptor(file.parent);
return 'storage:' + fileName;
}
return file.persistentDescriptor;
}
/**
* If file is within the attachment base directory, return a relative
* path prefixed by BASE_PATH_PLACEHOLDER. Otherwise, return unchanged.
*/
this.getBaseDirectoryRelativePath = function (path) {
if (!path || path.indexOf(this.BASE_PATH_PLACEHOLDER) == 0) {
return path;
}
var basePath = Zotero.Prefs.get('baseAttachmentPath');
if (!basePath) {
return path;
}
// Get nsIFile for base directory
var baseDir = Components.classes["@mozilla.org/file/local;1"]
.createInstance(Components.interfaces.nsILocalFile);
try {
baseDir.persistentDescriptor = basePath;
}
catch (e) {
Zotero.debug(e, 1);
Components.utils.reportError(e);
return path;
}
// Get nsIFile for file
var attachmentFile = Components.classes["@mozilla.org/file/local;1"]
.createInstance(Components.interfaces.nsILocalFile);
try {
attachmentFile.persistentDescriptor = path;
}
catch (e) {
Zotero.debug(e, 1);
Components.utils.reportError(e);
return path;
}
if (Zotero.File.directoryContains(baseDir, attachmentFile)) {
path = this.BASE_PATH_PLACEHOLDER
+ attachmentFile.getRelativeDescriptor(baseDir);
}
return path;
}
/**
* Get a file from this path, if we can
*
* @param {String} path Absolute path or relative path prefixed
* by BASE_PATH_PLACEHOLDER
* @param {Boolean} asFile Return nsIFile instead of path
* @return {String|nsIFile|FALSE} Persistent descriptor string, file,
* of FALSE if no path
*/
this.resolveRelativePath = function (path) {
if (path.indexOf(Zotero.Attachments.BASE_PATH_PLACEHOLDER) != 0) {
return false;
}
var basePath = Zotero.Prefs.get('baseAttachmentPath');
if (!basePath) {
Zotero.debug("No base attachment path set -- can't resolve '" + path + "'", 2);
return false;
}
// Get file from base directory
var baseDir = Components.classes["@mozilla.org/file/local;1"]
.createInstance(Components.interfaces.nsILocalFile);
try {
baseDir.persistentDescriptor = basePath;
}
catch (e) {
Zotero.debug(e, 1);
Components.utils.reportError(e);
Zotero.debug("Invalid base attachment path -- can't resolve'" + row.path + "'", 2);
return false;
}
// Get file from relative path
var relativePath = path.substr(
Zotero.Attachments.BASE_PATH_PLACEHOLDER.length
);
var file = Components.classes["@mozilla.org/file/local;1"]
.createInstance(Components.interfaces.nsILocalFile);
try {
file.setRelativeDescriptor(baseDir, relativePath);
}
catch (e) {
Zotero.debug("Invalid relative descriptor '" + relativePath + "'", 2);
return false;
}
return file;
}
/**
* Returns the number of files in the attachment directory
*
* Only counts if MIME type is text/html
*
* @param {Zotero.Item} item Attachment item
*/
this.getNumFiles = function (item) {
var funcName = "Zotero.Attachments.getNumFiles()";
if (!item.isAttachment()) {
throw ("Item is not an attachment in " + funcName);
}
var linkMode = item.attachmentLinkMode;
switch (linkMode) {
case Zotero.Attachments.LINK_MODE_IMPORTED_URL:
case Zotero.Attachments.LINK_MODE_IMPORTED_FILE:
break;
default:
throw ("Invalid attachment link mode in " + funcName);
}
if (item.attachmentMIMEType != 'text/html') {
return 1;
}
var file = item.getFile();
if (!file) {
throw ("File not found in " + funcName);
}
var numFiles = 0;
var parentDir = file.parent;
var files = parentDir.directoryEntries;
while (files.hasMoreElements()) {
file = files.getNext();
file.QueryInterface(Components.interfaces.nsIFile);
if (file.leafName.indexOf('.') == 0) {
continue;
}
numFiles++;
}
return numFiles;
}
/**
* @param {Zotero.Item} item
* @param {Boolean} [skipHidden=FALSE] Don't count hidden files
* @return {Integer} Total file size in bytes
*/
this.getTotalFileSize = function (item, skipHidden) {
var funcName = "Zotero.Attachments.getTotalFileSize()";
if (!item.isAttachment()) {
throw ("Item is not an attachment in " + funcName);
}
var linkMode = item.attachmentLinkMode;
switch (linkMode) {
case Zotero.Attachments.LINK_MODE_IMPORTED_URL:
case Zotero.Attachments.LINK_MODE_IMPORTED_FILE:
case Zotero.Attachments.LINK_MODE_LINKED_FILE:
break;
default:
throw ("Invalid attachment link mode in " + funcName);
}
var file = item.getFile();
if (!file) {
throw ("File not found in " + funcName);
}
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
return item.fileSize;
}
var parentDir = file.parent;
var files = parentDir.directoryEntries;
var size = 0;
while (files.hasMoreElements()) {
file = files.getNext();
file.QueryInterface(Components.interfaces.nsIFile);
if (skipHidden && file.leafName.indexOf('.') == 0) {
continue;
}
size += file.fileSize;
}
return size;
}
/**
* Copy attachment item, including files, to another library
*/
this.copyAttachmentToLibrary = function (attachment, libraryID, sourceItemID) {
var linkMode = attachment.attachmentLinkMode;
if (attachment.libraryID == libraryID) {
throw ("Attachment is already in library " + libraryID);
}
var newAttachment = new Zotero.Item('attachment');
newAttachment.libraryID = libraryID;
// Link mode needs to be set when saving new attachment
newAttachment.attachmentLinkMode = linkMode;
if (attachment.isImportedAttachment()) {
// Attachment path isn't copied over by clone() if libraryID is different
newAttachment.attachmentPath = attachment.attachmentPath;
}
// DEBUG: save here because clone() doesn't currently work on unsaved tagged items
var id = newAttachment.save();
newAttachment = Zotero.Items.get(id);
attachment.clone(false, newAttachment);
if (sourceItemID) {
newAttachment.setSource(sourceItemID);
}
newAttachment.save();
// Copy over files if they exist
if (newAttachment.isImportedAttachment() && attachment.getFile()) {
var dir = Zotero.Attachments.getStorageDirectory(attachment.id);
var newDir = Zotero.Attachments.createDirectoryForItem(newAttachment.id);
Zotero.File.copyDirectory(dir, newDir);
}
attachment.addLinkedItem(newAttachment);
return newAttachment.id;
}
function _getFileNameFromURL(url, mimeType){
var nsIURL = Components.classes["@mozilla.org/network/standard-url;1"]
.createInstance(Components.interfaces.nsIURL);
nsIURL.spec = url;
var ext = Zotero.MIME.getPrimaryExtension(mimeType, nsIURL.fileExtension);
if (!nsIURL.fileName) {
var matches = nsIURL.directory.match(/\/([^\/]+)\/$/);
// If no filename, use the last part of the path if there is one
if (matches) {
nsIURL.fileName = matches[1];
}
// Or just use the host
else {
nsIURL.fileName = nsIURL.host;
var tld = nsIURL.fileExtension;
}
}
// If we found a better extension, use that
if (ext && (!nsIURL.fileExtension || nsIURL.fileExtension != ext)) {
nsIURL.fileExtension = ext;
}
// If we replaced the TLD (which would've been interpreted as the extension), add it back
if (tld && tld != nsIURL.fileExtension) {
nsIURL.fileBaseName = nsIURL.fileBaseName + '.' + tld;
}
// Test unencoding fileBaseName
try {
decodeURIComponent(nsIURL.fileBaseName);
}
catch (e) {
if (e.name == 'URIError') {
// If we got a 'malformed URI sequence' while decoding,
// use MD5 of fileBaseName
nsIURL.fileBaseName = Zotero.Utilities.Internal.md5(nsIURL.fileBaseName, false);
}
else {
throw e;
}
}
// Pass unencoded name to getValidFileName() so that percent-encoded
// characters aren't stripped to just numbers
return Zotero.File.getValidFileName(decodeURIComponent(nsIURL.fileName));
}
function _getExtensionFromURL(url, mimeType) {
var nsIURL = Components.classes["@mozilla.org/network/standard-url;1"]
.createInstance(Components.interfaces.nsIURL);
nsIURL.spec = url;
return Zotero.MIME.getPrimaryExtension(mimeType, nsIURL.fileExtension);
}
/**
* If directory exists and is non-empty, move it to orphaned-files directory
*
* If empty, just remove it
*/
function _moveOrphanedDirectory(dir) {
if (!dir.exists()) {
return;
}
dir = dir.clone();
// If directory is empty or has only hidden files, delete it
var files = dir.directoryEntries;
files.QueryInterface(Components.interfaces.nsIDirectoryEnumerator);
var empty = true;
while (files.hasMoreElements()) {
var file = files.getNext();
file.QueryInterface(Components.interfaces.nsIFile);
if (file.leafName[0] == '.') {
continue;
}
empty = false;
break;
}
files.close();
if (empty) {
dir.remove(true);
return;
}
// Create orphaned-files directory if it doesn't exist
var orphaned = Zotero.getZoteroDirectory();
orphaned.append('orphaned-files');
if (!orphaned.exists()) {
orphaned.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0755);
}
// Find unique filename for orphaned file
var orphanTarget = orphaned.clone();
orphanTarget.append(dir.leafName);
var newName = null;
if (orphanTarget.exists()) {
try {
orphanTarget.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0644);
newName = orphanTarget.leafName;
}
catch (e) {
// DEBUG: Work around createUnique() brokenness on Windows
// as of Fx3.0.3 (https://bugzilla.mozilla.org/show_bug.cgi?id=452217)
//
// We just delete the conflicting file
if (Zotero.isWin && e.name == 'NS_ERROR_FILE_ACCESS_DENIED') {
orphanTarget.remove(true);
}
else {
throw (e);
}
}
if (newName) {
orphanTarget.remove(false);
}
}
// Move target to orphaned files directory
dir.moveTo(orphaned, newName);
}
/**
* Create a new item of type 'attachment' and add to the itemAttachments table
*
* Returns the itemID of the new attachment
**/
function _addToDB(file, url, title, linkMode, mimeType, charsetID, sourceItemID) {
Zotero.DB.beginTransaction();
var attachmentItem = new Zotero.Item('attachment');
if (sourceItemID) {
var parentItem = Zotero.Items.get(sourceItemID);
if (parentItem.libraryID && linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
throw ("Cannot save linked file in non-local library");
}
attachmentItem.libraryID = parentItem.libraryID;
}
attachmentItem.setField('title', title);
if (linkMode == self.LINK_MODE_IMPORTED_URL
|| linkMode == self.LINK_MODE_LINKED_URL) {
attachmentItem.setField('url', url);
attachmentItem.setField('accessDate', "CURRENT_TIMESTAMP");
}
// Get path
if (file) {
attachmentItem.attachmentPath
= Zotero.Attachments.getPath(file, linkMode);
}
attachmentItem.setSource(sourceItemID);
attachmentItem.attachmentLinkMode = linkMode;
attachmentItem.attachmentMIMEType = mimeType;
attachmentItem.attachmentCharset = charsetID;
attachmentItem.save();
Zotero.DB.commitTransaction();
return attachmentItem.id;
}
/*
* Since we have to load the content into the browser to get the
* character set (at least until we figure out a better way to get
* at the native detectors), we create the item above and update
* asynchronously after the fact
*/
function _postProcessFile(itemID, file, mimeType){
// Don't try to process if MIME type is unknown
if (!mimeType) {
return;
}
// MIME types that get cached by the fulltext indexer can just be
// indexed directly
if (Zotero.Fulltext.isCachedMIMEType(mimeType)) {
Zotero.Fulltext.indexItems([itemID]);
return;
}
var ext = Zotero.File.getExtension(file);
if (!Zotero.MIME.hasInternalHandler(mimeType, ext)) {
return;
}
var browser = Zotero.Browser.createHiddenBrowser();
var callback = function(charset, args) {
// ignore spurious about:blank loads
if(browser.contentDocument.location.href == "about:blank") return;
var writeCallback = function () {
var charsetID = Zotero.CharacterSets.getID(charset);
if (charsetID) {
var disabled = Zotero.Notifier.disable();
var item = Zotero.Items.get(itemID);
item.attachmentCharset = charsetID;
item.save();
if (disabled) {
Zotero.Notifier.enable();
}
}
// Chain fulltext indexer inside the charset callback,
// since it's asynchronous and a prerequisite
Zotero.Fulltext.indexDocument(browser.contentDocument, itemID);
Zotero.Browser.deleteHiddenBrowser(browser);
}
// Since the callback can be called during an import process that uses
// Zotero.wait(), wait until we're unlocked
Zotero.unlockPromise
.then(function () {
writeCallback();
});
};
Zotero.File.addCharsetListener(browser, callback, itemID);
var url = Components.classes["@mozilla.org/network/protocol;1?name=file"]
.getService(Components.interfaces.nsIFileProtocolHandler)
.getURLSpecFromFile(file);
browser.loadURI(url);
}
/**
* Determines if a given document is an instance of PDFJS
* @return {Boolean}
*/
this.isPDFJS = function(doc) {
// pdf.js HACK
// This may no longer be necessary (as of Fx 23)
if(doc.contentType === "text/html") {
var win = doc.defaultView;
if(win) {
win = win.wrappedJSObject;
if(win && "PDFJS" in win) {
return true;
}
}
}
return false;
}
}