Retraction Watch integration
- Check for retracted items using data from Retraction Watch - Show an X next to retracted items in the items list, and show a scary message at the top of the item pane with more info and links. - Lookup is done in a privacy-preserving manner using k-anonymity -- the server is unable to determine the specific items that exist in the client, so people who don't sync don't need to share any library data (though the server doesn't log the lookups anyway). TODO: - Pop up an alert when new items are found - Show a confirmation prompt when citing a retracted item - Support items without DOIs or PMIDs - Add a proper PMID field and expand DOI to more item types so these values don't need to be parsed out of Extra - Clear the banner immediately when all possible fields are cleared instead of waiting a few seconds
This commit is contained in:
parent
897a042ee0
commit
48580c49d1
15 changed files with 969 additions and 107 deletions
|
@ -28,8 +28,8 @@
|
||||||
<!-- <!DOCTYPE bindings SYSTEM "chrome://zotero/locale/itembox.dtd"> -->
|
<!-- <!DOCTYPE bindings SYSTEM "chrome://zotero/locale/itembox.dtd"> -->
|
||||||
|
|
||||||
<bindings xmlns="http://www.mozilla.org/xbl"
|
<bindings xmlns="http://www.mozilla.org/xbl"
|
||||||
xmlns:xbl="http://www.mozilla.org/xbl"
|
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||||
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
xmlns:html="http://www.w3.org/1999/xhtml">
|
||||||
|
|
||||||
<binding id="item-box">
|
<binding id="item-box">
|
||||||
<resources>
|
<resources>
|
||||||
|
@ -291,6 +291,8 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateRetracted();
|
||||||
|
|
||||||
if (this.clickByItem) {
|
if (this.clickByItem) {
|
||||||
var itemBox = document.getAnonymousNodes(this)[0];
|
var itemBox = document.getAnonymousNodes(this)[0];
|
||||||
itemBox.setAttribute('onclick',
|
itemBox.setAttribute('onclick',
|
||||||
|
@ -2374,6 +2376,125 @@
|
||||||
</method>
|
</method>
|
||||||
|
|
||||||
|
|
||||||
|
<method name="updateRetracted">
|
||||||
|
<body><![CDATA[
|
||||||
|
this._id('retraction-box').hidden = true;
|
||||||
|
|
||||||
|
return (async function () {
|
||||||
|
var htmlNS = 'http://www.w3.org/1999/xhtml';
|
||||||
|
|
||||||
|
var data = await Zotero.Retractions.getData(this.item);
|
||||||
|
if (!data) {
|
||||||
|
this._id('retraction-box').hidden = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._id('retraction-box').hidden = false;
|
||||||
|
this._id('retraction-banner').textContent = Zotero.getString('retraction.banner');
|
||||||
|
|
||||||
|
// Date
|
||||||
|
if (data.date) {
|
||||||
|
this._id('retraction-date').hidden = false;
|
||||||
|
this._id('retraction-date').textContent = Zotero.getString(
|
||||||
|
'retraction.date',
|
||||||
|
data.date.toLocaleDateString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._id('retraction-date').hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reasons
|
||||||
|
if (data.reasons.length) {
|
||||||
|
let elem = this._id('retraction-reasons');
|
||||||
|
elem.hidden = false;
|
||||||
|
elem.textContent = '';
|
||||||
|
for (let reason of data.reasons) {
|
||||||
|
let dt = document.createElementNS(htmlNS, 'dt');
|
||||||
|
let dd = document.createElementNS(htmlNS, 'dd');
|
||||||
|
|
||||||
|
dt.textContent = reason;
|
||||||
|
dd.textContent = Zotero.Retractions.getReasonDescription(reason);
|
||||||
|
|
||||||
|
elem.appendChild(dt);
|
||||||
|
elem.appendChild(dd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._id('retraction-reasons').hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retraction DOI or PubMed ID
|
||||||
|
if (data.doi || data.pmid) {
|
||||||
|
let div = this._id('retraction-notice');
|
||||||
|
div.textContent = '';
|
||||||
|
let a = document.createElementNS(htmlNS, 'a');
|
||||||
|
a.textContent = Zotero.getString('retraction.notice');
|
||||||
|
if (data.doi) {
|
||||||
|
a.href = 'https://doi.org/' + data.doi;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
a.href = `https://www.ncbi.nlm.nih.gov/pubmed/${data.pmid}/`;
|
||||||
|
}
|
||||||
|
div.appendChild(a);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._id('retraction-notice').hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Links
|
||||||
|
if (data.urls.length) {
|
||||||
|
let div = this._id('retraction-links');
|
||||||
|
div.hidden = false;
|
||||||
|
div.textContent = '';
|
||||||
|
|
||||||
|
let p = document.createElementNS(htmlNS, 'p');
|
||||||
|
p.textContent = Zotero.getString('retraction.details');
|
||||||
|
|
||||||
|
let ul = document.createElementNS(htmlNS, 'ul');
|
||||||
|
for (let url of data.urls) {
|
||||||
|
let li = document.createElementNS(htmlNS, 'li');
|
||||||
|
let a = document.createElementNS(htmlNS, 'a');
|
||||||
|
url = url.replace(/^http:/, 'https:');
|
||||||
|
a.href = url;
|
||||||
|
a.textContent = url;
|
||||||
|
li.appendChild(a);
|
||||||
|
ul.appendChild(li);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.appendChild(p);
|
||||||
|
div.appendChild(ul);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._id('retraction-links').hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let creditElem = this._id('retraction-credit');
|
||||||
|
if (!creditElem.childNodes.length) {
|
||||||
|
let text = Zotero.getString(
|
||||||
|
'retraction.credit',
|
||||||
|
'<a href="https://retractionwatch.com">Retraction Watch</a>'
|
||||||
|
);
|
||||||
|
let parts = Zotero.Utilities.parseMarkup(text);
|
||||||
|
for (let part of parts) {
|
||||||
|
if (part.type == 'text') {
|
||||||
|
creditElem.appendChild(document.createTextNode(part.text));
|
||||||
|
}
|
||||||
|
else if (part.type == 'link') {
|
||||||
|
let a = document.createElementNS(htmlNS, 'a');
|
||||||
|
a.href = part.attributes.href;
|
||||||
|
a.textContent = part.text;
|
||||||
|
creditElem.appendChild(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Zotero.Utilities.Internal.updateHTMLInXUL(this._id('retraction-box'));
|
||||||
|
}.bind(this))();
|
||||||
|
]]></body>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
|
||||||
<method name="_id">
|
<method name="_id">
|
||||||
<parameter name="id"/>
|
<parameter name="id"/>
|
||||||
<body>
|
<body>
|
||||||
|
@ -2484,6 +2605,22 @@
|
||||||
</menupopup>
|
</menupopup>
|
||||||
<zoteroguidancepanel id="zotero-author-guidance" about="authorMenu" position="after_end" x="-25"/>
|
<zoteroguidancepanel id="zotero-author-guidance" about="authorMenu" position="after_end" x="-25"/>
|
||||||
</popupset>
|
</popupset>
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
id="retraction-box"
|
||||||
|
hidden="hidden">
|
||||||
|
<div id="retraction-banner"/>
|
||||||
|
<div id="retraction-details">
|
||||||
|
<p id="retraction-date"/>
|
||||||
|
|
||||||
|
<dl id="retraction-reasons"/>
|
||||||
|
|
||||||
|
<p id="retraction-notice"/>
|
||||||
|
|
||||||
|
<div id="retraction-links"/>
|
||||||
|
|
||||||
|
<p id="retraction-credit"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<grid flex="1">
|
<grid flex="1">
|
||||||
<columns>
|
<columns>
|
||||||
<column/>
|
<column/>
|
||||||
|
|
|
@ -3717,28 +3717,32 @@ Zotero.Item.prototype.getImageSrcWithTags = Zotero.Promise.coroutine(function* (
|
||||||
|
|
||||||
var uri = this.getImageSrc();
|
var uri = this.getImageSrc();
|
||||||
|
|
||||||
|
var retracted = Zotero.Retractions.isRetracted(this);
|
||||||
|
|
||||||
var tags = this.getTags();
|
var tags = this.getTags();
|
||||||
if (!tags.length) {
|
if (!tags.length && !retracted) {
|
||||||
return uri;
|
return uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tagColors = Zotero.Tags.getColors(this.libraryID);
|
|
||||||
var colorData = [];
|
var colorData = [];
|
||||||
for (let i=0; i<tags.length; i++) {
|
if (tags.length) {
|
||||||
let tag = tags[i];
|
let tagColors = Zotero.Tags.getColors(this.libraryID);
|
||||||
let data = tagColors.get(tag.tag);
|
for (let tag of tags) {
|
||||||
if (data) {
|
let data = tagColors.get(tag.tag);
|
||||||
colorData.push(data);
|
if (data) {
|
||||||
|
colorData.push(data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (!colorData.length && !retracted) {
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
colorData.sort(function (a, b) {
|
||||||
|
return a.position - b.position;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (!colorData.length) {
|
|
||||||
return uri;
|
|
||||||
}
|
|
||||||
colorData.sort(function (a, b) {
|
|
||||||
return a.position - b.position;
|
|
||||||
});
|
|
||||||
var colors = colorData.map(val => val.color);
|
var colors = colorData.map(val => val.color);
|
||||||
return Zotero.Tags.generateItemsListImage(colors, uri);
|
|
||||||
|
return Zotero.Tags.generateItemsListImage(colors, uri, retracted);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,6 @@ Zotero.Tags = new function() {
|
||||||
var _libraryColors = {};
|
var _libraryColors = {};
|
||||||
var _libraryColorsByName = {};
|
var _libraryColorsByName = {};
|
||||||
var _itemsListImagePromises = {};
|
var _itemsListImagePromises = {};
|
||||||
var _itemsListExtraImagePromises = {};
|
|
||||||
|
|
||||||
|
|
||||||
this.init = Zotero.Promise.coroutine(function* () {
|
this.init = Zotero.Promise.coroutine(function* () {
|
||||||
|
@ -780,23 +779,29 @@ Zotero.Tags = new function() {
|
||||||
* so we need to generate a composite image containing the existing item type
|
* so we need to generate a composite image containing the existing item type
|
||||||
* icon and one or more tag color swatches.
|
* icon and one or more tag color swatches.
|
||||||
*
|
*
|
||||||
* @params {Array} colors Array of swatch colors
|
* @params {String[]} colors - Array of swatch colors
|
||||||
* @params {String} extraImage Chrome URL of image to add to final image
|
* @params {String} extraImage - chrome:// URL of image to add to final image
|
||||||
* @return {Q Promise} A Q promise for a data: URL for a PNG
|
* @params {Boolean} [retracted = false] - If true, show an icon indicating the item was retracted
|
||||||
|
* @return {Promise<String>} - A promise for a data: URL for a PNG
|
||||||
*/
|
*/
|
||||||
this.generateItemsListImage = function (colors, extraImage) {
|
this.generateItemsListImage = async function (colors, extraImage, retracted) {
|
||||||
var multiplier = Zotero.hiDPI ? 2 : 1;
|
var multiplier = Zotero.hiDPI ? 2 : 1;
|
||||||
|
|
||||||
var swatchWidth = 8 * multiplier;
|
var canvasHeight = 16 * multiplier;
|
||||||
var separator = 3 * multiplier;
|
|
||||||
var extraImageSeparator = 1 * multiplier;
|
|
||||||
var extraImageWidth = 16 * multiplier;
|
var extraImageWidth = 16 * multiplier;
|
||||||
var extraImageHeight = 16 * multiplier;
|
var extraImageHeight = 16 * multiplier;
|
||||||
var canvasHeight = 16 * multiplier;
|
var retractionImage = `chrome://zotero/skin/cross${Zotero.hiDPI ? '@1.5x' : ''}.png`;
|
||||||
|
var retractionImageLeftPadding = 1 * multiplier;
|
||||||
|
var retractionImageWidth = 16 * multiplier;
|
||||||
|
var retractionImageHeight = 16 * multiplier;
|
||||||
|
var retractionImageScaledWidth = 12 * multiplier;
|
||||||
|
var retractionImageScaledHeight = 12 * multiplier;
|
||||||
|
var tagsLeftPadding = 3 * multiplier;
|
||||||
|
var swatchSeparator = 3 * multiplier;
|
||||||
|
var swatchWidth = 8 * multiplier;
|
||||||
var swatchHeight = 8 * multiplier;
|
var swatchHeight = 8 * multiplier;
|
||||||
var prependExtraImage = true;
|
|
||||||
|
|
||||||
var hash = colors.join("") + (extraImage ? extraImage : "");
|
var hash = colors.join("") + (extraImage ? extraImage : "") + (retracted ? "retracted" : "");
|
||||||
|
|
||||||
if (_itemsListImagePromises[hash]) {
|
if (_itemsListImagePromises[hash]) {
|
||||||
return _itemsListImagePromises[hash];
|
return _itemsListImagePromises[hash];
|
||||||
|
@ -808,86 +813,79 @@ Zotero.Tags = new function() {
|
||||||
var doc = win.document;
|
var doc = win.document;
|
||||||
var canvas = doc.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
|
var canvas = doc.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
|
||||||
|
|
||||||
var width = colors.length * (swatchWidth + separator);
|
var width = extraImageWidth
|
||||||
if (extraImage) {
|
+ (retracted
|
||||||
width += (colors.length ? extraImageSeparator : 0) + extraImageWidth;
|
? (retractionImageLeftPadding
|
||||||
}
|
+ ((retractionImageWidth - retractionImageScaledWidth) / 2)
|
||||||
else if (colors.length) {
|
+ retractionImageScaledWidth)
|
||||||
width -= separator;
|
: 0)
|
||||||
}
|
+ (colors.length ? tagsLeftPadding : 0)
|
||||||
|
+ (colors.length * (swatchWidth + swatchSeparator));
|
||||||
|
|
||||||
canvas.width = width;
|
canvas.width = width;
|
||||||
canvas.height = canvasHeight;
|
canvas.height = canvasHeight;
|
||||||
var swatchTop = Math.floor((canvasHeight - swatchHeight) / 2);
|
var swatchTop = Math.floor((canvasHeight - swatchHeight) / 2);
|
||||||
var ctx = canvas.getContext('2d');
|
var ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
var x = prependExtraImage ? extraImageWidth + separator + extraImageSeparator : 0;
|
// If extra image hasn't started loading, start now
|
||||||
for (let i=0, len=colors.length; i<len; i++) {
|
if (_itemsListImagePromises[extraImage] === undefined) {
|
||||||
|
let ios = Services.io;
|
||||||
|
let uri = ios.newURI(extraImage, null, null);
|
||||||
|
let img = new win.Image();
|
||||||
|
img.src = uri.spec;
|
||||||
|
_itemsListImagePromises[extraImage] = new Zotero.Promise((resolve) => {
|
||||||
|
img.onload = function () {
|
||||||
|
resolve(img);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If retraction image hasn't started loading, start now
|
||||||
|
if (_itemsListImagePromises[retractionImage] === undefined) {
|
||||||
|
let ios = Services.io;
|
||||||
|
let uri = ios.newURI(retractionImage, null, null);
|
||||||
|
let img = new win.Image();
|
||||||
|
img.src = uri.spec;
|
||||||
|
_itemsListImagePromises[retractionImage] = new Zotero.Promise((resolve) => {
|
||||||
|
img.onload = function () {
|
||||||
|
resolve(img);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var x = extraImageWidth
|
||||||
|
+ (retracted ? retractionImageLeftPadding + retractionImageWidth: 0)
|
||||||
|
+ tagsLeftPadding;
|
||||||
|
for (let i = 0, len = colors.length; i < len; i++) {
|
||||||
ctx.fillStyle = colors[i];
|
ctx.fillStyle = colors[i];
|
||||||
_canvasRoundRect(ctx, x, swatchTop + 1, swatchWidth, swatchHeight, 2, true, false)
|
_canvasRoundRect(ctx, x, swatchTop + 1, swatchWidth, swatchHeight, 2, true, false);
|
||||||
x += swatchWidth + separator;
|
x += swatchWidth + swatchSeparator;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's no extra iamge, resolve a promise now
|
if (retracted) {
|
||||||
if (!extraImage) {
|
let [img1, img2] = await Zotero.Promise.all([
|
||||||
var dataURI = canvas.toDataURL("image/png");
|
_itemsListImagePromises[extraImage],
|
||||||
var dataURIPromise = Zotero.Promise.resolve(dataURI);
|
_itemsListImagePromises[retractionImage]
|
||||||
_itemsListImagePromises[hash] = dataURIPromise;
|
]);
|
||||||
return dataURIPromise;
|
ctx.drawImage(img1, 0, 0, extraImageWidth, extraImageHeight);
|
||||||
}
|
ctx.drawImage(
|
||||||
|
img2,
|
||||||
// Add an extra image to the beginning or end of the swatches
|
extraImageWidth + retractionImageLeftPadding
|
||||||
if (prependExtraImage) {
|
+ ((retractionImageWidth - retractionImageScaledWidth) / 2),
|
||||||
x = 0;
|
(retractionImageHeight - retractionImageScaledHeight) / 2 + 1, // Lower by 1
|
||||||
|
retractionImageScaledWidth,
|
||||||
|
retractionImageScaledHeight
|
||||||
|
);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
x += extraImageSeparator;
|
let img = await _itemsListImagePromises[extraImage];
|
||||||
|
ctx.drawImage(img, 0, 0, extraImageWidth, extraImageHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If extra image hasn't started loading, start now
|
var dataURI = canvas.toDataURL("image/png");
|
||||||
if (typeof _itemsListExtraImagePromises[extraImage] == 'undefined') {
|
_itemsListImagePromises[hash] = Zotero.Promise.resolve(dataURI);
|
||||||
let ios = Components.classes['@mozilla.org/network/io-service;1']
|
return dataURI;
|
||||||
.getService(Components.interfaces["nsIIOService"]);
|
};
|
||||||
let uri = ios.newURI(extraImage, null, null);
|
|
||||||
|
|
||||||
var img = new win.Image();
|
|
||||||
img.src = uri.spec;
|
|
||||||
|
|
||||||
// Mark that we've started loading
|
|
||||||
var deferred = Zotero.Promise.defer();
|
|
||||||
var extraImageDeferred = Zotero.Promise.defer();
|
|
||||||
_itemsListExtraImagePromises[extraImage] = extraImageDeferred.promise;
|
|
||||||
|
|
||||||
// When extra image has loaded, draw it
|
|
||||||
img.onload = function () {
|
|
||||||
ctx.drawImage(img, x, 0, extraImageWidth, extraImageHeight);
|
|
||||||
|
|
||||||
var dataURI = canvas.toDataURL("image/png");
|
|
||||||
var dataURIPromise = Zotero.Promise.resolve(dataURI);
|
|
||||||
_itemsListImagePromises[hash] = dataURIPromise;
|
|
||||||
|
|
||||||
// Fulfill the promise for this call
|
|
||||||
deferred.resolve(dataURI);
|
|
||||||
|
|
||||||
// And resolve the extra image's promise to fulfill
|
|
||||||
// other promises waiting on it
|
|
||||||
extraImageDeferred.resolve(img);
|
|
||||||
}
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If extra image has already started loading, return a promise
|
|
||||||
// for the composite image once it's ready
|
|
||||||
return _itemsListExtraImagePromises[extraImage]
|
|
||||||
.then(function (img) {
|
|
||||||
ctx.drawImage(img, x, 0, extraImageWidth, extraImageHeight);
|
|
||||||
|
|
||||||
var dataURI = canvas.toDataURL("image/png");
|
|
||||||
var dataURIPromise = Zotero.Promise.resolve(dataURI);
|
|
||||||
_itemsListImagePromises[hash] = dataURIPromise;
|
|
||||||
return dataURIPromise;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -593,14 +593,23 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio
|
||||||
}
|
}
|
||||||
// If refreshing a single item, clear caches and then deselect and reselect row
|
// If refreshing a single item, clear caches and then deselect and reselect row
|
||||||
else if (savedSelection.length == 1 && savedSelection[0] == ids[0]) {
|
else if (savedSelection.length == 1 && savedSelection[0] == ids[0]) {
|
||||||
let row = this._rowMap[ids[0]];
|
let id = ids[0];
|
||||||
|
let row = this._rowMap[id];
|
||||||
delete this._cellTextCache[row];
|
delete this._cellTextCache[row];
|
||||||
|
delete this._itemImages[id];
|
||||||
|
this._treebox.invalidateRow(row);
|
||||||
|
|
||||||
this.selection.clearSelection();
|
this.selection.clearSelection();
|
||||||
this.rememberSelection(savedSelection);
|
this.rememberSelection(savedSelection);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this._cellTextCache = {};
|
for (let id of ids) {
|
||||||
|
let row = this._rowMap[id];
|
||||||
|
if (row === undefined) continue;
|
||||||
|
delete this._cellTextCache[row];
|
||||||
|
delete this._itemImages[id];
|
||||||
|
this._treebox.invalidateRow(row);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For a refresh on an item in the trash, check if the item still belongs
|
// For a refresh on an item in the trash, check if the item still belongs
|
||||||
|
|
623
chrome/content/zotero/xpcom/retractions.js
Normal file
623
chrome/content/zotero/xpcom/retractions.js
Normal file
|
@ -0,0 +1,623 @@
|
||||||
|
Zotero.Retractions = {
|
||||||
|
TYPE_DOI: 'd',
|
||||||
|
TYPE_PMID: 'p',
|
||||||
|
TYPE_NAMES: ['DOI', 'PMID'],
|
||||||
|
|
||||||
|
_version: 1,
|
||||||
|
_cacheFile: null,
|
||||||
|
_cacheETag: null,
|
||||||
|
_cacheDOIPrefixLength: null,
|
||||||
|
_cachePMIDPrefixLength: null,
|
||||||
|
_cachePrefixList: new Set(), // Prefix strings from server
|
||||||
|
|
||||||
|
_queuedItems: new Set(),
|
||||||
|
_queuedPrefixStrings: new Set(),
|
||||||
|
|
||||||
|
_keyItems: {},
|
||||||
|
_itemKeys: {},
|
||||||
|
|
||||||
|
init: async function () {
|
||||||
|
this._cacheFile = OS.Path.join(Zotero.Profile.dir, 'retractions.json');
|
||||||
|
|
||||||
|
await this._cacheItems();
|
||||||
|
Zotero.Notifier.registerObserver(this, ['item'], 'retractions');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this._loadCacheFile();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
Zotero.logError("Error loading retractions cache file");
|
||||||
|
Zotero.logError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemIDs = await Zotero.DB.columnQueryAsync("SELECT itemID FROM retractedItems");
|
||||||
|
this.retractedItems = new Set(itemIDs);
|
||||||
|
|
||||||
|
// TEMP
|
||||||
|
Zotero.Schema.schemaUpdatePromise.then(() => {
|
||||||
|
this.updateFromServer();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Zotero.Item}
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
isRetracted: function (item) {
|
||||||
|
return this.retractedItems.has(item.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
notify: async function (action, type, ids, extraData) {
|
||||||
|
if (action == 'add') {
|
||||||
|
for (let id of ids) {
|
||||||
|
this._updateItem(Zotero.Items.get(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (action == 'modify') {
|
||||||
|
for (let id of ids) {
|
||||||
|
let item = Zotero.Items.get(id);
|
||||||
|
for (let type of this.TYPE_NAMES) {
|
||||||
|
let typeID = this['TYPE_' + type];
|
||||||
|
let fieldVal = this['_getItem' + type](item);
|
||||||
|
if (fieldVal) {
|
||||||
|
// If the item isn't already mapped to the key, re-map
|
||||||
|
let key = this._itemKeys[typeID].get(item.id);
|
||||||
|
let newKey = this._valueToKey(typeID, fieldVal);
|
||||||
|
if (key != newKey) {
|
||||||
|
this._deleteItemKeyMappings(id);
|
||||||
|
this._updateItem(item);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If a previous key value was cleared, re-map
|
||||||
|
else if (this._itemKeys[typeID].get(item.id)) {
|
||||||
|
this._deleteItemKeyMappings(id);
|
||||||
|
this._updateItem(item);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (action == 'delete') {
|
||||||
|
for (let id of ids) {
|
||||||
|
await this.removeEntry(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for possible matches for items in the queue (debounced)
|
||||||
|
*/
|
||||||
|
checkQueuedItems: Zotero.Utilities.debounce(async function () {
|
||||||
|
Zotero.debug("Checking updated items for retractions");
|
||||||
|
|
||||||
|
// If no possible matches, clear retraction flag on any items that changed
|
||||||
|
if (!this._queuedPrefixStrings.size) {
|
||||||
|
for (let item of this._queuedItems) {
|
||||||
|
await this.removeEntry(item.id);
|
||||||
|
}
|
||||||
|
this._queuedItems.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = [...this._queuedItems];
|
||||||
|
var prefixStrings = [...this._queuedPrefixStrings];
|
||||||
|
this._queuedItems.clear();
|
||||||
|
this._queuedPrefixStrings.clear();
|
||||||
|
var addedItems = [];
|
||||||
|
try {
|
||||||
|
addedItems = await this._downloadPossibleMatches(prefixStrings);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
// Add back to queue on failure
|
||||||
|
for (let item of items) {
|
||||||
|
this._queuedItems.add(item);
|
||||||
|
}
|
||||||
|
for (let prefixStr of prefixStrings) {
|
||||||
|
this._queuedPrefixStrings.add(prefixStr);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove retraction status for items that were checked but didn't match
|
||||||
|
for (let item of items) {
|
||||||
|
if (!addedItems.includes(item.id)) {
|
||||||
|
await this.removeEntry(item.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 5000),
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Number[]} - Array of added item ids
|
||||||
|
*/
|
||||||
|
_downloadPossibleMatches: async function (prefixStrings) {
|
||||||
|
var urlPrefix = (Zotero.Prefs.get("api.url") || ZOTERO_CONFIG.API_URL) + '/retractions/';
|
||||||
|
var req = await Zotero.HTTP.request(
|
||||||
|
"POST",
|
||||||
|
urlPrefix + 'search',
|
||||||
|
{
|
||||||
|
body: JSON.stringify(prefixStrings),
|
||||||
|
responseType: 'json'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
var results = req.response;
|
||||||
|
Zotero.debug(`Retrieved ${results.length} possible `
|
||||||
|
+ Zotero.Utilities.pluralize(results.length, ['match', 'matches']));
|
||||||
|
|
||||||
|
// Look for local items that match
|
||||||
|
var addedItems = new Set();
|
||||||
|
for (let row of results) {
|
||||||
|
if (row.doi) {
|
||||||
|
let ids = this._keyItems[this.TYPE_DOI].get(row.doi);
|
||||||
|
if (ids) {
|
||||||
|
for (let id of ids) {
|
||||||
|
addedItems.add(id);
|
||||||
|
await this.addEntry(id, row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (row.pmid) {
|
||||||
|
let ids = this._keyItems[this.TYPE_PMID].get(row.pmid.toString());
|
||||||
|
if (ids) {
|
||||||
|
for (let id of ids) {
|
||||||
|
addedItems.add(id);
|
||||||
|
await this.addEntry(id, row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Zotero.debug(`Found ${addedItems.size} retracted `
|
||||||
|
+ Zotero.Utilities.pluralize(addedItems.size, 'item'));
|
||||||
|
|
||||||
|
return [...addedItems];
|
||||||
|
},
|
||||||
|
|
||||||
|
getData: async function (item) {
|
||||||
|
var data = await Zotero.DB.valueQueryAsync(
|
||||||
|
"SELECT data FROM retractedItems WHERE itemID=?", item.id
|
||||||
|
);
|
||||||
|
if (!data) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
Zotero.logError(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (data.date) {
|
||||||
|
data.date = Zotero.Date.sqlToDate(data.date);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
data.date = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
Zotero.logError("Error parsing retraction date: " + data.date);
|
||||||
|
data.date = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getReasonDescription: function (reason) {
|
||||||
|
var description = this._reasonDescriptions[reason];
|
||||||
|
if (!description) {
|
||||||
|
Zotero.warn(`Description not found for retraction reason "${reason}"`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return description;
|
||||||
|
},
|
||||||
|
|
||||||
|
addEntry: async function (itemID, data) {
|
||||||
|
var o = {};
|
||||||
|
Object.assign(o, data);
|
||||||
|
// Replace original ids with retraction ids
|
||||||
|
if (data.retractionDOI) o.doi = data.retractionDOI;
|
||||||
|
if (data.retractionPMID) o.pmid = data.retractionPMID;
|
||||||
|
delete o.retractionDOI;
|
||||||
|
delete o.retractionPMID;
|
||||||
|
|
||||||
|
var sql = "REPLACE INTO retractedItems VALUES (?, ?)";
|
||||||
|
await Zotero.DB.queryAsync(sql, [itemID, JSON.stringify(o)]);
|
||||||
|
|
||||||
|
this.retractedItems.add(itemID);
|
||||||
|
|
||||||
|
await Zotero.Notifier.trigger('refresh', 'item', [itemID]);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeEntry: async function (itemID) {
|
||||||
|
this._deleteItemKeyMappings(itemID);
|
||||||
|
|
||||||
|
if (this.retractedItems.has(itemID)) {
|
||||||
|
await Zotero.DB.queryAsync("DELETE FROM retractedItems WHERE itemID=?", itemID);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.retractedItems.delete(itemID);
|
||||||
|
|
||||||
|
await Zotero.Notifier.trigger('refresh', 'item', [itemID]);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateFromServer: async function () {
|
||||||
|
var urlPrefix = (Zotero.Prefs.get("api.url") || ZOTERO_CONFIG.API_URL) + '/retractions/';
|
||||||
|
|
||||||
|
// Download list
|
||||||
|
var headers = {};
|
||||||
|
if (this._cacheETag) {
|
||||||
|
headers["If-None-Match"] = this._cacheETag;
|
||||||
|
}
|
||||||
|
var req = await Zotero.HTTP.request(
|
||||||
|
"GET",
|
||||||
|
urlPrefix + 'list',
|
||||||
|
{
|
||||||
|
headers,
|
||||||
|
noCache: true,
|
||||||
|
successCodes: [200, 304]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (req.status == 304) {
|
||||||
|
Zotero.debug("Retraction list is up to date");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var etag = req.getResponseHeader('ETag');
|
||||||
|
var list = req.response.trim().split('\n');
|
||||||
|
|
||||||
|
// Calculate prefix length automatically
|
||||||
|
var doiPrefixLength;
|
||||||
|
var pmidPrefixLength = 0;
|
||||||
|
for (let row of list) {
|
||||||
|
let [prefixStr, _date] = row.split(' ');
|
||||||
|
let type = prefixStr[0];
|
||||||
|
let prefix = prefixStr.substr(1);
|
||||||
|
if (type == this.TYPE_DOI && !doiPrefixLength) {
|
||||||
|
doiPrefixLength = prefix.length;
|
||||||
|
}
|
||||||
|
else if (type == this.TYPE_PMID) {
|
||||||
|
pmidPrefixLength = Math.max(pmidPrefixLength, prefix.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get possible local matches
|
||||||
|
var prefixStrings = new Set([
|
||||||
|
...Object.keys(this._keyItems[this.TYPE_DOI])
|
||||||
|
.map(x => this.TYPE_DOI + x.substr(0, doiPrefixLength)),
|
||||||
|
...Object.keys(this._keyItems[this.TYPE_PMID])
|
||||||
|
.map(x => this.TYPE_PMID + x.substr(0, pmidPrefixLength))
|
||||||
|
]);
|
||||||
|
var prefixesToSend = new Set();
|
||||||
|
for (let row of list) {
|
||||||
|
let [prefixStr, date] = row.split(' ');
|
||||||
|
let type = prefixStr[0];
|
||||||
|
let prefix = prefixStr.substr(1);
|
||||||
|
if (!type || !prefix || !date) {
|
||||||
|
Zotero.warn("Bad line in retractions data: " + row);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (prefixStrings.has(prefixStr)) {
|
||||||
|
prefixesToSend.add(prefixStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prefixesToSend.size) {
|
||||||
|
Zotero.debug("No possible retractions");
|
||||||
|
await Zotero.DB.queryAsync("DELETE FROM retractedItems");
|
||||||
|
this.retractedItems.clear();
|
||||||
|
await this._saveCacheFile(list, etag, doiPrefixLength, pmidPrefixLength);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Diff list
|
||||||
|
|
||||||
|
await this.downloadPossibleMatches([...prefixesToSend]);
|
||||||
|
await this._cacheList(list, etag, doiPrefixLength, pmidPrefixLength);
|
||||||
|
|
||||||
|
// TODO: Prompt
|
||||||
|
},
|
||||||
|
|
||||||
|
_getItemDOI: function (item) {
|
||||||
|
var itemDOI = item.getField('DOI') || item.getExtraField('doi');
|
||||||
|
if (itemDOI) {
|
||||||
|
itemDOI = Zotero.Utilities.cleanDOI(itemDOI);
|
||||||
|
}
|
||||||
|
return itemDOI || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
_getItemPMID: function (item) {
|
||||||
|
// TEMP
|
||||||
|
return this._extractPMID(item.getField('extra')) || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// TEMP
|
||||||
|
_extractPMID: function (str) {
|
||||||
|
if (!str) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var lines = str.split(/\n+/g);
|
||||||
|
var fields = new Map();
|
||||||
|
for (let line of lines) {
|
||||||
|
let parts = line.match(/^([a-z \-]+):(.+)/i);
|
||||||
|
if (!parts) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let [_, originalField, value] = parts;
|
||||||
|
|
||||||
|
let field = originalField.trim().toLowerCase()
|
||||||
|
// Strip spaces
|
||||||
|
.replace(/\s+/g, '')
|
||||||
|
// Old citeproc.js cheater syntax
|
||||||
|
.replace(/{:([^:]+):([^}]+)}/);
|
||||||
|
value = value.trim();
|
||||||
|
|
||||||
|
if (field == 'pmid' || field == 'pubmedid') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
_valueToKey: function (type, value) {
|
||||||
|
if (type == this.TYPE_DOI) {
|
||||||
|
return Zotero.Utilities.Internal.sha1(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
|
||||||
|
_getDOIPrefix: function (value, length) {
|
||||||
|
var hash = this._valueToKey(this.TYPE_DOI, value);
|
||||||
|
return hash.substr(0, length);
|
||||||
|
},
|
||||||
|
|
||||||
|
_getPMIDPrefix: function (value, length) {
|
||||||
|
return value.substr(0, length);
|
||||||
|
},
|
||||||
|
|
||||||
|
_cacheItems: async function () {
|
||||||
|
await this._cacheDOIItems();
|
||||||
|
await this._cachePMIDItems();
|
||||||
|
},
|
||||||
|
|
||||||
|
_cacheDOIItems: async function () {
|
||||||
|
this._keyItems[this.TYPE_DOI] = new Map();
|
||||||
|
this._itemKeys[this.TYPE_DOI] = new Map();
|
||||||
|
|
||||||
|
var sql = "SELECT itemID AS id, value FROM itemData "
|
||||||
|
+ "JOIN itemDataValues USING (valueID) WHERE fieldID=?";
|
||||||
|
var rows = await Zotero.DB.queryAsync(sql, Zotero.ItemFields.getID('DOI'));
|
||||||
|
for (let row of rows) {
|
||||||
|
let value = Zotero.Utilities.cleanDOI(row.value);
|
||||||
|
if (!value) continue;
|
||||||
|
this._addItemKeyMapping(this.TYPE_DOI, value, row.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract from Extract field
|
||||||
|
sql = "SELECT itemID AS id, value FROM itemData "
|
||||||
|
+ "JOIN itemDataValues USING (valueID) WHERE fieldID=?";
|
||||||
|
rows = await Zotero.DB.queryAsync(sql, Zotero.ItemFields.getID('extra'));
|
||||||
|
for (let row of rows) {
|
||||||
|
let fields = Zotero.Utilities.Internal.extractExtraFields(row.value);
|
||||||
|
let doi = fields.get('doi');
|
||||||
|
if (!doi || !doi.value) continue;
|
||||||
|
let value = Zotero.Utilities.cleanDOI(doi.value);
|
||||||
|
if (!value) continue;
|
||||||
|
this._addItemKeyMapping(this.TYPE_DOI, value, row.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_cachePMIDItems: async function () {
|
||||||
|
this._keyItems[this.TYPE_PMID] = new Map();
|
||||||
|
this._itemKeys[this.TYPE_PMID] = new Map();
|
||||||
|
|
||||||
|
var sql = "SELECT itemID AS id, value FROM itemData "
|
||||||
|
+ "JOIN itemDataValues USING (valueID) WHERE fieldID=?";
|
||||||
|
var rows = await Zotero.DB.queryAsync(sql, Zotero.ItemFields.getID('extra'));
|
||||||
|
for (let row of rows) {
|
||||||
|
/*
|
||||||
|
let fields = Zotero.Utilities.Internal.extractExtraFields(row.value);
|
||||||
|
let pmid = fields.get('pmid') || fields.get('pubmedID');
|
||||||
|
if (!pmid || !pmid.value) continue;
|
||||||
|
this._addItemKeyMapping(this.TYPE_PMID, pmid.value, row.id);
|
||||||
|
*/
|
||||||
|
let pmid = this._extractPMID(row.value);
|
||||||
|
if (!pmid) continue;
|
||||||
|
this._addItemKeyMapping(this.TYPE_PMID, pmid, row.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_addItemKeyMapping: function (type, value, itemID) {
|
||||||
|
var key = this._valueToKey(type, value);
|
||||||
|
// Map key to item id
|
||||||
|
var ids = this._keyItems[type].get(key);
|
||||||
|
if (!ids) {
|
||||||
|
ids = new Set();
|
||||||
|
this._keyItems[type].set(key, ids);
|
||||||
|
}
|
||||||
|
ids.add(itemID);
|
||||||
|
|
||||||
|
// Map item id to key so we can clear on change
|
||||||
|
this._itemKeys[type].set(itemID, key);
|
||||||
|
},
|
||||||
|
|
||||||
|
_deleteItemKeyMappings: function (itemID) {
|
||||||
|
for (let type of [this.TYPE_DOI, this.TYPE_PMID]) {
|
||||||
|
var key = this._itemKeys[type].get(itemID);
|
||||||
|
if (key) {
|
||||||
|
this._keyItems[type].get(key).delete(itemID);
|
||||||
|
this._itemKeys[type].delete(itemID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add new key mappings for an item, check if it matches a cached prefix, and queue it for full
|
||||||
|
* checking if so
|
||||||
|
*/
|
||||||
|
_updateItem: function (item) {
|
||||||
|
if (!item.isRegularItem()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._queuedItems.add(item);
|
||||||
|
var doi = this._getItemDOI(item);
|
||||||
|
if (doi) {
|
||||||
|
this._addItemKeyMapping(this.TYPE_DOI, doi, item.id);
|
||||||
|
let prefixStr = this.TYPE_DOI + this._getDOIPrefix(doi, this._cacheDOIPrefixLength);
|
||||||
|
if (this._cachePrefixList.has(prefixStr)) {
|
||||||
|
this._queuedPrefixStrings.add(prefixStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var pmid = this._getItemPMID(item);
|
||||||
|
if (pmid) {
|
||||||
|
this._addItemKeyMapping(this.TYPE_PMID, pmid, item.id);
|
||||||
|
let prefixStr = this.TYPE_PMID + this._getPMIDPrefix(pmid, this._cachePMIDPrefixLength);
|
||||||
|
if (this._cachePrefixList.has(prefixStr)) {
|
||||||
|
this._queuedPrefixStrings.add(prefixStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.checkQueuedItems();
|
||||||
|
},
|
||||||
|
|
||||||
|
_loadCacheFile: async function () {
|
||||||
|
if (!await OS.File.exists(this._cacheFile)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var data = JSON.parse(await Zotero.File.getContentsAsync(this._cacheFile));
|
||||||
|
if (data) {
|
||||||
|
this._processCacheData(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_processCacheData: function (data) {
|
||||||
|
this._cacheETag = data.etag;
|
||||||
|
this._cacheDOIPrefixLength = data.doiPrefixLength;
|
||||||
|
this._cachePMIDPrefixLength = data.pmidPrefixLength;
|
||||||
|
this._cachePrefixList = new Set();
|
||||||
|
for (let row of data.data) {
|
||||||
|
this._cachePrefixList.add(row.split(' ')[0]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache prefix list in profile directory
|
||||||
|
*/
|
||||||
|
_saveCacheFile: async function (data, etag, doiPrefixLength, pmidPrefixLength) {
|
||||||
|
var cacheJSON = {
|
||||||
|
version: this._version,
|
||||||
|
etag,
|
||||||
|
doiPrefixLength,
|
||||||
|
pmidPrefixLength,
|
||||||
|
data
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await Zotero.File.putContentsAsync(this._cacheFile, JSON.stringify(cacheJSON));
|
||||||
|
this._processCacheData(cacheJSON);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
Zotero.logError("Error caching retractions data: " + e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// https://retractionwatch.com/retraction-watch-database-user-guide/retraction-watch-database-user-guide-appendix-b-reasons/
|
||||||
|
_reasonDescriptions: {
|
||||||
|
"Author Unresponsive": "Author(s) lack of communication after prior contact by Journal, Publisher or other original Authors",
|
||||||
|
"Breach of Policy by Author": "A violation of the Journal, Publisher or Institutional accepted practices by the author",
|
||||||
|
"Breach of Policy by Third Party": "A violation of the Journal, Publisher or Institutional accepted practices by a person or company/institution not the authors",
|
||||||
|
"Cites Prior Retracted Work": "A retracted item is used in citations or referencing",
|
||||||
|
"Civil Proceedings": "Non-criminal litigation arising from the publication of the original article or the related notice(s)",
|
||||||
|
"Complaints about Author": "Allegations made strictly about the author without respect to the original article",
|
||||||
|
"Complaints about Company/Institution": "Allegations made strictly about the author’s affiliation(s) without respect to the original article",
|
||||||
|
"Complaints about Third Party": "Allegations made strictly about those not the author or the author’s affiliation(s) without respect to the original article",
|
||||||
|
"Concerns/Issues About Authorship": "Any question, controversy or dispute over the rightful claim to authorship, excluding forged authorship",
|
||||||
|
"Concerns/Issues About Data": "Any question, controversy or dispute over the validity of the data",
|
||||||
|
"Concerns/Issues About Image": "Any question, controversy or dispute over the validity of the image",
|
||||||
|
"Concerns/Issues about Referencing/Attributions": "Any question, controversy or dispute over whether ideas, analyses, text or data are properly credited to the originator",
|
||||||
|
"Concerns/Issues About Results": "Any question, controversy or dispute over the validity of the results",
|
||||||
|
"Concerns/Issues about Third Party Involvement": "Any question, controversy or dispute over the rightful claim to authorship, excluding forged authorship",
|
||||||
|
"Conflict of Interest": "Authors having affiliations with companies, associations, or institutions that may serve to influence their belief about their findings",
|
||||||
|
"Contamination of Cell Lines/Tissues": "Impurities found within cell lines or tissues",
|
||||||
|
"Contamination of Materials (General)": "Impurities found within compounds or solutions used in experiments",
|
||||||
|
"Contamination of Reagents": "Impurities found within compounds or solutions used to drive experimental outcomes",
|
||||||
|
"Copyright Claims": "Dispute concerning right of ownership of a publication",
|
||||||
|
"Criminal Proceedings": "Court actions that may result in incarceration or fines arising from the publication of the original article or the related notice(s)",
|
||||||
|
"Date of Retraction/Other Unknown": "A lack of publishing date given on the notice – or the date on the notice is not representative of the actual notice date. Commonly found when Publishers overwrite the original article’s HTML page with the retraction notice, without changing the publication date to reflect such.",
|
||||||
|
"Doing the Right Thing": "An attribution made by co-founders of Retraction Watch indicating admirable behavior by one of the involved parties",
|
||||||
|
"Duplication of Article": "Also known as “self-plagiarism”. Used when an entire published item, or undefined sections of it, written by one or all authors of the original article, are repeated in the original article without appropriate citation.",
|
||||||
|
"Duplication of Data": "Also known as “self-plagiarism”. Used when the all or part of the data from an item written by one or all authors of the original article, are repeated in the original article without appropriate citation.",
|
||||||
|
"Duplication of Image": "Also known as “self-plagiarism”. Used when an image from an item written by one or all authors of the original article is repeated in the original article without appropriate citation.",
|
||||||
|
"Duplication of Text": "Also known as “self-plagiarism”. Used when sections of text from an item written by one or all authors of the original article, are repeated in the original article without appropriate citation.",
|
||||||
|
"Error by Journal/Publisher": "A mistake attributed to a Journal Editor or Publisher",
|
||||||
|
"Error by Third Party": "A mistake attributed to a person or other, who is not an author or representative of the Journal or Publisher",
|
||||||
|
"Error in Analyses": "A mistake made in the evaluation of the data or calculations",
|
||||||
|
"Error in Cell Lines/Tissues": "A mistake made in the identification of cell lines or tissues, or the choice of an incorrect cell line or tissue",
|
||||||
|
"Error in Data": "A mistake made in the data, either in data entry, gathering or identification",
|
||||||
|
"Error in Image": "A mistake made in the preparation or printing of an image",
|
||||||
|
"Error in Materials (General)": "A mistake made in the choice of materials in the performance of experiments (eg., reagents, mixing bowls, etc)",
|
||||||
|
"Error in Methods": "A mistake made in the experimental protocol, either in following the wrong protocol, or in erring during the performance of the protocol",
|
||||||
|
"Error in Results and/or Conclusions": "A mistake made in determining the results or establishing conclusions from an experiment or analysis",
|
||||||
|
"Error in Text": "A mistake made in the written portion of the item",
|
||||||
|
"Ethical Violations by Author": "When an author performs an action contrary to accepted standards of behavior. Generally used only when stated as such in the notice and no other specific reason (e.g., duplication of image) is given.",
|
||||||
|
"Ethical Violations by Third Party": "When any person not an author performs an action contrary to accepted standards of behavior. Generally used only when stated as such in the notice and no other specific reason (e.g., duplication of image) is given.",
|
||||||
|
"Euphemisms for Duplication": "The notice does not clearly state that the authors reused ideas, text, or images from one of their previously published items without suitable citation",
|
||||||
|
"Euphemisms for Misconduct": "The notice does not clearly state that the reason for the notice is due to fabrication, falsification, or plagiarism by one or all the authors, despite an institutional report stating such.",
|
||||||
|
"Euphemisms for Plagiarism": "The notice does not clearly state that the authors reused ideas, text, or images, without suitable citation, from items published by those not the authors",
|
||||||
|
"Fake Peer Review": "The peer review was intentionally not performed in accordance with the journal’s guidelines or ethical standards",
|
||||||
|
"Falsification/Fabrication of Data": "Intentional changes to data so that it is not representative of the actual finding",
|
||||||
|
"Falsification/Fabrication of Image": "Intentional changes to an image so that it is not representative of the actual data",
|
||||||
|
"Falsification/Fabrication of Results": "Intentional changes to results so that it is not representative of the actual finding",
|
||||||
|
"Forged Authorship": "The fraudulent use of an author name in submitting a manuscript for publication",
|
||||||
|
"Hoax Paper": "Paper intentionally drafted with fraudulent data or information with the specific intent of testing a journal’s or publisher’s manuscript acceptance policies",
|
||||||
|
"Informed/Patient Consent – None/Withdrawn": "When the full risks and benefits from being in an experiment are not provided to and accepted by the participant, or the participant chooses to later recant their approval",
|
||||||
|
"Investigation by Company/Institution": "An evaluation of allegations by the affiliations of one or all of the authors",
|
||||||
|
"Investigation by Journal/Publisher": "An evaluation of allegations by the Journal or Publisher",
|
||||||
|
"Investigation by ORI": "An evaluation of allegations by the United State Office of Research Integrity",
|
||||||
|
"Investigation by Third Party": "An evaluation of allegations by a person, company or institution not the Authors, Journal, Publisher or ORI",
|
||||||
|
"Lack of Approval from Author": "Failure to obtain agreement from original author(s)",
|
||||||
|
"Lack of Approval from Company/Institution": "Failure to obtain agreement from original author(s)",
|
||||||
|
"Lack of Approval from Third Party": "Failure to obtain agreement from original author(s)",
|
||||||
|
"Lack Of Balance/Bias Issues": "Failure to maintain objectivity in the presentation or analysis of information",
|
||||||
|
"Lack of IRB/IACUC Approval": "Failure to obtain consent from the institutional ethical review board overseeing human or animal experimentation",
|
||||||
|
"Legal Reasons/Legal Threats": "Actions taken to avoid or foster litigation",
|
||||||
|
"Manipulation of Images": "The changing of the presentation of an image by reversal, rotation or similar action",
|
||||||
|
"Manipulation of Results": "The changing of the presentation of results which may lead to conclusions not otherwise warranted",
|
||||||
|
"Miscommunication by Author": "Error in messaging from or to author",
|
||||||
|
"Miscommunication by Company/Institution": "Error in messaging from or to authors’ affiliations",
|
||||||
|
"Miscommunication by Journal/Publisher": "Error in messaging from or to Journal or Publisher",
|
||||||
|
"Miscommunication by Third Party": "Error in messaging from or to any party not the author, affiliations, journal of publisher",
|
||||||
|
"Misconduct – Official Investigation/Finding": "Finding of misconduct after investigation by incorporated company, institution of governmental agency",
|
||||||
|
"Misconduct by Author": "Statement Journal, Publisher, Company, Institution, Governmental Agency, or Author that author committed misconduct",
|
||||||
|
"Misconduct by Company/Institution": "Statement Journal, Publisher, Company, Institution, or Governmental Agency that Company/Institution committed misconduct",
|
||||||
|
"Misconduct by Third Party": "Statement Journal, Publisher, Company, Institution, or Governmental Agency that a third party committed misconduct",
|
||||||
|
"No Further Action": "Generally applicable to Expressions of Concern – Statement by Editor or Publisher that",
|
||||||
|
"Nonpayment of Fees/Refusal to Pay": "Lack of payment of full amount due for services rendered or for rights of access",
|
||||||
|
"Notice – Lack of": "No Notice was published by the Journal or Publisher, and the article is removed from the publishing platform.",
|
||||||
|
"Notice – Limited or No Information": "A notice provides minimal information as to the cause of the notice, or the original item is watermaked as retracted or corrected without explanation",
|
||||||
|
"Notice – Unable to Access via current resources": "The notice is paywalled, only in print, or in some form unavailable for inspection at this time.",
|
||||||
|
"Objections by Author(s)": "A complaint by any of the original authors or refusal to agree actions taken by the Journal or Publisher",
|
||||||
|
"Objections by Company/Institution": "A complaint by any of the original authors’ affiliation(s) or refusal by same to agree actions taken by the Journal or Publisher",
|
||||||
|
"Objections by Third Party": "A complaint by any person, company or institution not of the original authors, or refusal by same to agree actions taken by the Journal or Publisher",
|
||||||
|
"Plagiarism of Article": "Used when an entire published item, or undefined sections of it, and not written by one or all authors of the original article, are repeated in the original article without appropriate citation.",
|
||||||
|
"Plagiarism of Data": "Used when the all or part of the data from an item not written by one or all authors of the original article, are repeated in the original article without appropriate citation.",
|
||||||
|
"Plagiarism of Image": "Used when an image from an item not written by one or all authors of the original article is repeated in the original article without appropriate citation.",
|
||||||
|
"Plagiarism of Text": "Used when sections of text from an item not written by one or all authors of the original article, are repeated in the original article without appropriate citation.",
|
||||||
|
"Publishing Ban": "A Journal or Publisher states that no manuscripts will be acceptance from one or all the authors of the original article. It can be for a limited time, or indefinitely.",
|
||||||
|
"Results Not Reproducible": "Experiments conducted, using the same materials and methods, that fail to replicate the finding of the original article",
|
||||||
|
"Retract and Replace": "The permanent change of an item to a non-citable status, with a subsequent republication by the same journal after substantial changes to the item",
|
||||||
|
"Sabotage of Materials": "An intentional action to surreptitiously change or contaminate experimental ingredients in order to artificially change the experimental outcome",
|
||||||
|
"Sabotage of Methods": "An intentional action to surreptitiously change or contaminate experimental instruments or tools in order to artificially change the experimental outcome",
|
||||||
|
"Salami Slicing": "The publication of several articles by using the same (small) dataset, but by breaking it into sections, with the intent of exploiting a limited data set for the production of several published works This does not apply to large multi-group studies such as the Framingham Heart Study.",
|
||||||
|
"Temporary Removal": "An original article is removed from the Journal’s publishing platform for an undefined period of time, after which, if returned to the publishing platform, it appears with minimal or no substantial changes",
|
||||||
|
"Unreliable Data": "The accuracy or validity of the data is questionable",
|
||||||
|
"Unreliable Image": "The accuracy or validity of the image is questionable",
|
||||||
|
"Unreliable Results": "The accuracy or validity of the results is questionable",
|
||||||
|
"Updated to Correction": "A prior notice has been changed to the status of a Correction",
|
||||||
|
"Updated to Retraction": "A prior notice has been changed to the status of a Retraction",
|
||||||
|
"Upgrade/Update of Prior Notice": "Either a change to or affirmation of a prior notice",
|
||||||
|
Withdrawal: "The original article is removed from access on the Journal’s publishing platform."
|
||||||
|
}
|
||||||
|
};
|
|
@ -1127,6 +1127,8 @@ Zotero.Schema = new function(){
|
||||||
yield Zotero.DB.waitForTransaction();
|
yield Zotero.DB.waitForTransaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
yield Zotero.Retractions.updateFromServer();
|
||||||
|
|
||||||
// Get the last timestamp we got from the server
|
// Get the last timestamp we got from the server
|
||||||
var lastUpdated = yield this.getDBVersion('repository');
|
var lastUpdated = yield this.getDBVersion('repository');
|
||||||
var updated = false;
|
var updated = false;
|
||||||
|
@ -2506,6 +2508,10 @@ Zotero.Schema = new function(){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else if (i == 103) {
|
||||||
|
yield Zotero.DB.queryAsync("CREATE TABLE retractedItems (\n itemID INTEGER PRIMARY KEY,\n data TEXT,\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE\n);");
|
||||||
|
}
|
||||||
|
|
||||||
// If breaking compatibility or doing anything dangerous, clear minorUpdateFrom
|
// If breaking compatibility or doing anything dangerous, clear minorUpdateFrom
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -202,6 +202,35 @@ Zotero.Utilities.Internal = {
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Adapted from http://developer.mozilla.org/en/docs/nsICryptoHash
|
||||||
|
*
|
||||||
|
* @param {String} str
|
||||||
|
* @return {String}
|
||||||
|
*/
|
||||||
|
sha1: function (str) {
|
||||||
|
var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
|
||||||
|
.createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
|
||||||
|
converter.charset = "UTF-8";
|
||||||
|
var result = {};
|
||||||
|
var data = converter.convertToByteArray(str, result);
|
||||||
|
var ch = Components.classes["@mozilla.org/security/hash;1"]
|
||||||
|
.createInstance(Components.interfaces.nsICryptoHash);
|
||||||
|
ch.init(ch.SHA1);
|
||||||
|
ch.update(data, data.length);
|
||||||
|
var hash = ch.finish(false);
|
||||||
|
|
||||||
|
// Return the two-digit hexadecimal code for a byte
|
||||||
|
function toHexString(charCode) {
|
||||||
|
return ("0" + charCode.toString(16)).slice(-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the binary hash data to a hex string.
|
||||||
|
var s = Array.from(hash, (c, i) => toHexString(hash.charCodeAt(i))).join("");
|
||||||
|
return s;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
gzip: async function (data) {
|
gzip: async function (data) {
|
||||||
var deferred = Zotero.Promise.defer();
|
var deferred = Zotero.Promise.defer();
|
||||||
|
|
||||||
|
@ -590,15 +619,7 @@ Zotero.Utilities.Internal = {
|
||||||
let href = a.getAttribute('href');
|
let href = a.getAttribute('href');
|
||||||
a.setAttribute('tooltiptext', href);
|
a.setAttribute('tooltiptext', href);
|
||||||
a.onclick = function (event) {
|
a.onclick = function (event) {
|
||||||
try {
|
Zotero.launchURL(href);
|
||||||
let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
|
|
||||||
.getService(Components.interfaces.nsIWindowMediator);
|
|
||||||
let win = wm.getMostRecentWindow("navigator:browser");
|
|
||||||
win.ZoteroPane_Local.loadURI(href, options.linkEvent || event)
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
Zotero.logError(e);
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -745,6 +745,7 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
||||||
yield Zotero.Creators.init();
|
yield Zotero.Creators.init();
|
||||||
yield Zotero.Groups.init();
|
yield Zotero.Groups.init();
|
||||||
yield Zotero.Relations.init();
|
yield Zotero.Relations.init();
|
||||||
|
yield Zotero.Retractions.init();
|
||||||
|
|
||||||
// Load all library data except for items, which are loaded when libraries are first
|
// Load all library data except for items, which are loaded when libraries are first
|
||||||
// clicked on or if otherwise necessary
|
// clicked on or if otherwise necessary
|
||||||
|
|
|
@ -1198,3 +1198,9 @@ licenses.cc-by-sa = Creative Commons Attribution-ShareAlike 4.0 International Li
|
||||||
licenses.cc-by-nc = Creative Commons Attribution-NonCommercial 4.0 International License
|
licenses.cc-by-nc = Creative Commons Attribution-NonCommercial 4.0 International License
|
||||||
licenses.cc-by-nc-nd = Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License
|
licenses.cc-by-nc-nd = Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License
|
||||||
licenses.cc-by-nc-sa = Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License
|
licenses.cc-by-nc-sa = Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License
|
||||||
|
|
||||||
|
retraction.banner = This work has been retracted.
|
||||||
|
retraction.date = Retracted on %S
|
||||||
|
retraction.notice = Retraction Notice
|
||||||
|
retraction.details = More details:
|
||||||
|
retraction.credit = Data from %S
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 655 B After Width: | Height: | Size: 476 B |
BIN
chrome/skin/default/zotero/cross@1.5x.png
Normal file
BIN
chrome/skin/default/zotero/cross@1.5x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1,001 B |
BIN
chrome/skin/default/zotero/cross@2x.png
Normal file
BIN
chrome/skin/default/zotero/cross@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 932 B |
|
@ -45,6 +45,56 @@
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#retraction-banner {
|
||||||
|
padding: 1.5em 1em;
|
||||||
|
background: #e02a20;
|
||||||
|
width: 100%;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#retraction-details {
|
||||||
|
background: #fbf0f0;
|
||||||
|
padding: .5em 1.5em;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#retraction-details dt {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#retraction-details dt:not(:first-child) {
|
||||||
|
margin-top: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#retraction-details dd {
|
||||||
|
margin-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#retraction-details a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#retraction-links ul {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#retraction-links li {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#retraction-links li:not(:first-child) {
|
||||||
|
margin-top: .75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#retraction-credit {
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 1.5em;
|
||||||
|
margin-right: -.9em;
|
||||||
|
margin-bottom: .2em;
|
||||||
|
}
|
||||||
|
|
||||||
/* Buttons in trash and feed views */
|
/* Buttons in trash and feed views */
|
||||||
.zotero-item-pane-top-buttons {
|
.zotero-item-pane-top-buttons {
|
||||||
-moz-appearance: toolbar;
|
-moz-appearance: toolbar;
|
||||||
|
|
|
@ -110,6 +110,7 @@ const xpcomFilesLocal = [
|
||||||
'quickCopy',
|
'quickCopy',
|
||||||
'recognizePDF',
|
'recognizePDF',
|
||||||
'report',
|
'report',
|
||||||
|
'retractions',
|
||||||
'router',
|
'router',
|
||||||
'schema',
|
'schema',
|
||||||
'server',
|
'server',
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
-- 102
|
-- 103
|
||||||
|
|
||||||
-- Copyright (c) 2009 Center for History and New Media
|
-- Copyright (c) 2009 Center for History and New Media
|
||||||
-- George Mason University, Fairfax, Virginia, USA
|
-- George Mason University, Fairfax, Virginia, USA
|
||||||
|
@ -286,6 +286,12 @@ CREATE TABLE publicationsItems (
|
||||||
itemID INTEGER PRIMARY KEY
|
itemID INTEGER PRIMARY KEY
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE retractedItems (
|
||||||
|
itemID INTEGER PRIMARY KEY,
|
||||||
|
data TEXT,
|
||||||
|
FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE fulltextItems (
|
CREATE TABLE fulltextItems (
|
||||||
itemID INTEGER PRIMARY KEY,
|
itemID INTEGER PRIMARY KEY,
|
||||||
indexedPages INT,
|
indexedPages INT,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue