zotero/chrome/content/zotero/xpcom/attachments.js

1077 lines
32 KiB
JavaScript

/*
***** BEGIN LICENSE BLOCK *****
Copyright (c) 2006 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://chnm.gmu.edu
Licensed under the Educational Community License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.opensource.org/licenses/ecl1.php
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
***** 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.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.getStorageDirectory = getStorageDirectory;
this.getPath = getPath;
var self = this;
function importFromFile(file, sourceItemID){
Zotero.debug('Importing attachment from file');
var title = file.leafName;
Zotero.DB.beginTransaction();
try {
// Create a new attachment
var attachmentItem = new Zotero.Item('attachment');
attachmentItem.setField('title', title);
attachmentItem.save();
var itemID = attachmentItem.getID();
// Create directory for attachment files within storage directory
var destDir = this.createDirectoryForItem(itemID);
file.copyTo(destDir, null);
// Point to copied file
var newFile = Components.classes["@mozilla.org/file/local;1"]
.createInstance(Components.interfaces.nsILocalFile);
newFile.initWithFile(destDir);
newFile.append(title);
var mimeType = Zotero.MIME.getMIMETypeFromFile(newFile);
_addToDB(newFile, null, null, this.LINK_MODE_IMPORTED_FILE,
mimeType, null, sourceItemID, itemID);
Zotero.DB.commitTransaction();
// Determine charset and build fulltext index
_postProcessFile(itemID, newFile, mimeType);
}
catch (e){
// hmph
Zotero.DB.rollbackTransaction();
try {
// Clean up
if (itemID){
var itemDir = Zotero.getStorageDirectory();
itemDir.append(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');
attachmentItem.setField('title', title);
attachmentItem.setField('url', url);
// 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
attachmentItem.save();
var itemID = attachmentItem.getID();
var storageDir = Zotero.getStorageDirectory();
file.parent.copyTo(storageDir, itemID);
// Point to copied file
var newFile = Components.classes["@mozilla.org/file/local;1"]
.createInstance(Components.interfaces.nsILocalFile);
newFile.initWithFile(storageDir);
newFile.append(itemID);
newFile.append(file.leafName);
_addToDB(newFile, url, null, this.LINK_MODE_IMPORTED_URL, mimeType,
charsetID, sourceItemID, itemID);
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 = Zotero.getStorageDirectory();
itemDir.append(itemID);
if (itemDir.exists()){
itemDir.remove(true);
}
}
}
catch (e) {}
throw (e);
}
return itemID;
}
function importFromURL(url, sourceItemID, forceTitle, forceFileBaseName, parentCollectionIDs){
Zotero.debug('Importing attachment from URL');
// Throw error on invalid URLs
urlRe = /^https?:\/\/[^\s]*$/;
var matches = urlRe.exec(url);
if (!matches) {
throw ("Invalid URL '" + url + "' in Zotero.Attachments.importFromURL()");
}
Zotero.Utilities.HTTP.doHead(url, function(obj){
var mimeType = obj.channel.contentType;
var nsIURL = Components.classes["@mozilla.org/network/standard-url;1"]
.createInstance(Components.interfaces.nsIURL);
nsIURL.spec = url;
var ext = nsIURL.fileExtension;
// 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)
//
// Downloaded file is inspected below and deleted if actually HTML
if (ext == 'pdf') {
mimeType = 'application/pdf';
}
// If we can load this natively, use a hidden browser (so we can
// get the charset and title and index the document)
if (Zotero.MIME.hasNativeHandler(mimeType, ext)){
var browser = Zotero.Browser.createHiddenBrowser();
var imported = false;
var onpageshow = function() {
// pageshow can be triggered multiple times on some pages,
// so make sure we only import once
// (https://www.zotero.org/trac/ticket/795)
if (imported) {
return;
}
var callback = function () {
browser.removeEventListener("pageshow", onpageshow, false);
Zotero.Browser.deleteHiddenBrowser(browser);
};
Zotero.Attachments.importFromDocument(browser.contentDocument,
sourceItemID, forceTitle, parentCollectionIDs, callback);
imported = true;
};
browser.addEventListener("pageshow", onpageshow, false);
browser.loadURI(url);
}
// Otherwise use a remote web page persist
else {
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;
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 itemID = attachmentItem.save();
// 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 = Components.classes["@mozilla.org/file/local;1"].
createInstance(Components.interfaces.nsILocalFile);
file.initWithFile(destDir);
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') {
Zotero.debug("Downloaded PDF did not have MIME type "
+ "'application/pdf' in Attachments.importFromURL()", 2);
var item = Zotero.Items.get(itemID);
item.erase();
return;
}
_addToDB(file, url, title, Zotero.Attachments.LINK_MODE_IMPORTED_URL,
mimeType, null, sourceItemID, itemID);
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
var timer = Components.classes["@mozilla.org/timer;1"].
createInstance(Components.interfaces.nsITimer);
timer.initWithCallback({notify: function() {
Zotero.Fulltext.indexItems([itemID]);
}}, 1000, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
}
catch (e) {
// Clean up
var item = Zotero.Items.get(itemID);
item.erase();
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();
}
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 link attachment from a URL
*
* Returns the itemID of the created attachment
*/
function linkFromURL(url, sourceItemID, mimeType, title){
Zotero.debug('Linking attachment from URL');
// Throw error on invalid URLs
urlRe = /^https?:\/\/[^\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';
}
// Disable the Notifier if we're going to do a HEAD for the MIME type
if (!mimeType) {
var disabled = Zotero.Notifier.disable();
}
var itemID = _addToDB(null, url, title, this.LINK_MODE_LINKED_URL,
mimeType, null, sourceItemID);
if (disabled) {
Zotero.Notifier.enable();
}
if (!mimeType) {
// If we don't have the MIME type, do a HEAD request for it
Zotero.Utilities.HTTP.doHead(url, function(obj){
var mimeType = obj.channel.contentType;
if (mimeType) {
var sql = "UPDATE itemAttachments SET mimeType=? WHERE itemID=?";
Zotero.DB.query(sql, [mimeType, itemID]);
}
Zotero.Notifier.trigger('add', 'item', itemID);
});
}
return itemID;
}
// TODO: what if called on file:// document?
function linkFromDocument(document, sourceItemID, parentCollectionIDs){
Zotero.debug('Linking attachment from document');
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) {
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" );
}
}
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");
var itemID = attachmentItem.save();
// 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);
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();
}
};
}
if (mimeType == 'text/html') {
var sync = true;
// Load WebPageDump code
Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Components.interfaces.mozIJSSubScriptLoader)
.loadSubScript("chrome://zotero/content/webpagedump/common.js");
Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Components.interfaces.mozIJSSubScriptLoader)
.loadSubScript("chrome://zotero/content/webpagedump/domsaver.js");
wpdDOMSaver.init(file.path, document);
wpdDOMSaver.saveHTMLDocument();
_addToDB(file, url, title, Zotero.Attachments.LINK_MODE_IMPORTED_URL,
mimeType, charsetID, sourceItemID, itemID);
}
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 {
_addToDB(file, url, title, Zotero.Attachments.LINK_MODE_IMPORTED_URL,
mimeType, charsetID, sourceItemID, itemID);
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
var timer = Components.classes["@mozilla.org/timer;1"].
createInstance(Components.interfaces.nsITimer);
timer.initWithCallback({notify: f}, 1000,
Components.interfaces.nsITimer.TYPE_ONE_SHOT);
}
catch (e) {
// Clean up
var item = Zotero.Items.get(itemID);
item.erase();
throw (e);
}
});
wbp.saveURI(nsIURL, null, null, null, null, file);
}
// 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)
var timer = Components.classes["@mozilla.org/timer;1"].
createInstance(Components.interfaces.nsITimer);
timer.initWithCallback({notify: f}, 1000,
Components.interfaces.nsITimer.TYPE_ONE_SHOT);
}
}
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);
}
}
/*
* 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);
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');
// Strip potentially invalid characters
// See http://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
formatString = formatString.replace(/[\/\\\?\*:|"<>\.]/g, '');
return formatString;
}
/*
* Create directory for attachment files within storage directory
*/
function createDirectoryForItem(itemID) {
var dir = this.getStorageDirectory(itemID);
if (!dir.exists()) {
dir.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0755);
}
return dir;
}
function getStorageDirectory(itemID) {
var dir = Zotero.getStorageDirectory();
dir.append(itemID);
return dir;
}
/*
* Gets a relative descriptor for imported attachments and a persistent
* descriptor for files outside the storage directory
*/
function getPath(file, linkMode) {
if (!file.exists()) {
throw ('Zotero.Attachments.getPath() cannot be called on non-existent file');
}
file.QueryInterface(Components.interfaces.nsILocalFile);
if (linkMode == self.LINK_MODE_IMPORTED_URL ||
linkMode == self.LINK_MODE_IMPORTED_FILE) {
var storageDir = Zotero.getStorageDirectory();
storageDir.QueryInterface(Components.interfaces.nsILocalFile);
return file.getRelativeDescriptor(storageDir);
}
return file.persistentDescriptor;
}
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;
}
nsIURL.fileBaseName = Zotero.File.getValidFileName(nsIURL.fileBaseName);
return 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);
}
/**
* Create a new item of type 'attachment' and add to the itemAttachments table
*
* Passing an itemID causes it to skip new item creation and use the specified
* item instead -- used when importing files (since we have to know
* the itemID before copying in a file and don't want to update the DB before
* the file is saved)
*
* Returns the itemID of the new attachment
**/
function _addToDB(file, url, title, linkMode, mimeType, charsetID, sourceItemID, itemID){
Zotero.DB.beginTransaction();
if (sourceItemID){
var sourceItem = Zotero.Items.get(sourceItemID);
if (!sourceItem){
Zotero.DB.commitTransaction();
throw ("Cannot set attachment source to invalid item " + sourceItemID);
}
if (sourceItem.isAttachment()){
Zotero.DB.commitTransaction();
throw ("Cannot set attachment source to another file (" + sourceItemID + ")");
}
}
// If an itemID is provided, use that
if (itemID){
var attachmentItem = Zotero.Items.get(itemID);
if (!attachmentItem.isAttachment()){
throw ("Item " + itemID + " is not a valid attachment in _addToDB()");
}
}
// Otherwise create a new attachment
else {
var attachmentItem = new Zotero.Item('attachment');
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");
}
attachmentItem.save();
}
if (file) {
if (file.exists()) {
var path = getPath(file, linkMode);
}
// If file doesn't exist, create one temporarily so we can get the
// relative path (since getPath() doesn't work on non-existent files)
else if (linkMode == self.LINK_MODE_IMPORTED_URL ||
linkMode == self.LINK_MODE_IMPORTED_FILE) {
var missingFile = self.createDirectoryForItem(attachmentItem.id);
missingFile.append(file.leafName);
missingFile.create(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0644);
var path = getPath(missingFile, linkMode);
var parentDir = missingFile.parent;
missingFile.remove(null);
parentDir.remove(null);
}
}
var sql = "INSERT INTO itemAttachments (itemID, sourceItemID, linkMode, "
+ "mimeType, charsetID, path) VALUES (?,?,?,?,?,?)";
var bindParams = [
attachmentItem.getID(),
sourceItemID ? {int:sourceItemID} : null,
{int:linkMode},
mimeType ? {string:mimeType} : null,
charsetID ? {int:charsetID} : null,
path ? {string:path} : null
];
Zotero.DB.query(sql, bindParams);
if (sourceItemID){
sourceItem.incrementAttachmentCount();
Zotero.Notifier.trigger('modify', 'item', sourceItemID);
}
Zotero.DB.commitTransaction();
return attachmentItem.getID();
}
/*
* 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){
// 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 (mimeType.substr(0, 5)!='text/' ||
!Zotero.MIME.hasInternalHandler(mimeType, ext)){
return;
}
var browser = Zotero.Browser.createHiddenBrowser();
Zotero.File.addCharsetListener(browser, new function(){
return function(charset, id){
var charsetID = Zotero.CharacterSets.getID(charset);
if (charsetID){
var sql = "UPDATE itemAttachments SET charsetID=" + charsetID
+ " WHERE itemID=" + itemID;
Zotero.DB.query(sql);
}
// Chain fulltext indexer inside the charset callback,
// since it's asynchronous and a prerequisite
Zotero.Fulltext.indexDocument(browser.contentDocument, itemID);
Zotero.Browser.deleteHiddenBrowser(browser);
};
}, itemID);
var url = Components.classes["@mozilla.org/network/protocol;1?name=file"]
.getService(Components.interfaces.nsIFileProtocolHandler)
.getURLSpecFromFile(file);
browser.loadURI(url);
}
}