diff --git a/chrome/content/zotero/debugViewer.js b/chrome/content/zotero/debugViewer.js index b9fb8b6339d..f85796f2988 100644 --- a/chrome/content/zotero/debugViewer.js +++ b/chrome/content/zotero/debugViewer.js @@ -38,7 +38,7 @@ function updateErrors() { .then(function (sysInfo) { if (stopping) return; - var errors = Zotero.getErrors(true); + var errors = Zotero.Errors.getErrors(true); var errorStr = errors.length ? errors.join('\n\n') + '\n\n' : ''; document.getElementById('errors').textContent = errorStr + sysInfo; diff --git a/chrome/content/zotero/errorReport.xul b/chrome/content/zotero/errorReport.xul index b4214dd92ce..b06929b3ec6 100644 --- a/chrome/content/zotero/errorReport.xul +++ b/chrome/content/zotero/errorReport.xul @@ -18,8 +18,6 @@ var Zotero = obj.Zotero; var data = obj.data; var msg = data.msg; - var errorData = data.errorData; - var extraData = data.extraData ? data.extraData : ''; var diagnosticInfo = false; @@ -29,13 +27,12 @@ continueButton.disabled = true; diagnosticInfo = yield Zotero.getSystemInfo(); + var errorData = Zotero.Errors.getErrors(); var errorDataText = errorData.length - ? data.errorData.join('\n\n') + ? errorData.join('\n\n') : Zotero.getString('errorReport.noErrorsLogged', Zotero.appName); - var logText = errorDataText + '\n\n' - + (extraData !== '' ? extraData + '\n\n' : '') - + diagnosticInfo; + var logText = diagnosticInfo + '\n\n' + errorDataText; if (document.getElementById('zotero-failure-message').hasChildNodes()) { var textNode = document.getElementById('zotero-failure-message').firstChild; @@ -57,89 +54,22 @@ var wizard = document.getElementById('zotero-error-report'); var continueButton = wizard.getButton('next'); continueButton.disabled = true; - - var parts = { - error: "true", - errorData: errorData.join('\n'), - extraData: extraData, - diagnostic: diagnosticInfo - }; - - var body = ''; - for (var key in parts) { - body += key + '=' + encodeURIComponent(parts[key]) + '&'; - } - body = body.substr(0, body.length - 1); - var req = yield Zotero.HTTP.request( - "POST", - ZOTERO_CONFIG.REPOSITORY_URL + "report", - { - body, - successCodes: false, - foreground: true - } - ); - _sendErrorReportCallback(req); - }); - - function _sendErrorReportCallback(xmlhttp) { - var wizard = document.getElementById('zotero-error-report'); - if (!wizard) { - return; - } - - var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] - .getService(Components.interfaces.nsIPromptService); - - if (!xmlhttp.responseXML){ - try { - if (xmlhttp.status>1000){ - ps.alert( - null, - Zotero.getString('general.error'), - Zotero.getString('errorReport.noNetworkConnection') - ); - } - else { - ps.alert( - null, - Zotero.getString('general.error'), - Zotero.getString('errorReport.invalidResponseRepository') - ); - } - } - catch (e){ - ps.alert( - null, - Zotero.getString('general.error'), - Zotero.getString('errorReport.repoCannotBeContacted') - ); - } - - wizard.rewind(); - return; - } - - - var reported = xmlhttp.responseXML.getElementsByTagName('reported'); - if (reported.length != 1) { - ps.alert( - null, - Zotero.getString('general.error'), - Zotero.getString('errorReport.invalidResponseRepository') - ); + var reportID; + try { + reportID = yield Zotero.Errors.submitToZotero(); + } catch (e) { + ps.alert(null, Zotero.getString('general.error'), e.message); wizard.rewind(); return; } wizard.advance(); - wizard.getButton('cancel').disabled = true;; + wizard.getButton('cancel').disabled = true; wizard.canRewind = false; - var reportID = reported[0].getAttribute('reportID'); document.getElementById('zotero-report-id').setAttribute('value', reportID); document.getElementById('zotero-report-result').hidden = false; - } + }); } ]]> diff --git a/chrome/content/zotero/standalone/standalone.js b/chrome/content/zotero/standalone/standalone.js index aa7f9201495..27e4026157c 100644 --- a/chrome/content/zotero/standalone/standalone.js +++ b/chrome/content/zotero/standalone/standalone.js @@ -240,7 +240,7 @@ ZoteroStandalone.DebugOutput = { toggleStore: function () { - Zotero.Debug.setStore(!Zotero.Debug.storing); + Zotero.Debug.setStore(!Zotero.Debug.storing, true); }, @@ -268,96 +268,21 @@ ZoteroStandalone.DebugOutput = { submit: function () { - // 'Zotero' isn't defined yet when this function is created, so do it inline return Zotero.Promise.coroutine(function* () { - Components.utils.import("resource://zotero/config.js"); - - var url = ZOTERO_CONFIG.REPOSITORY_URL + "report?debug=1"; - var output = yield Zotero.Debug.get( - Zotero.Prefs.get('debug.store.submitSize'), - Zotero.Prefs.get('debug.store.submitLineLength') - ); - Zotero.Debug.setStore(false); - var ps = Services.prompt; try { - var xmlhttp = yield Zotero.HTTP.request( - "POST", - url, - { - compressBody: true, - body: output, - logBodyLength: 30, - timeout: 15000, - requestObserver: function (req) { - // Don't fail during tests, with fake XHR - if (!req.channel) { - return; - } - req.channel.notificationCallbacks = { - onProgress: function (request, context, progress, progressMax) {}, - - // nsIInterfaceRequestor - getInterface: function (iid) { - try { - return this.QueryInterface(iid); - } - catch (e) { - throw Components.results.NS_NOINTERFACE; - } - }, - - QueryInterface: function(iid) { - if (iid.equals(Components.interfaces.nsISupports) || - iid.equals(Components.interfaces.nsIInterfaceRequestor) || - iid.equals(Components.interfaces.nsIProgressEventSink)) { - return this; - } - throw Components.results.NS_NOINTERFACE; - }, - - } - } - } - ); - } - catch (e) { - Zotero.logError(e); - let title = Zotero.getString('general.error'); - let msg; - if (e instanceof Zotero.HTTP.UnexpectedStatusException) { - msg = Zotero.getString('general.invalidResponseServer'); - } - else if (e instanceof Zotero.HTTP.BrowserOfflineException) { - msg = Zotero.getString('general.browserIsOffline', Zotero.appName); - } - else { - msg = Zotero.getString('zotero.debugOutputLogging.dialog.error'); - } - ps.alert(null, title, msg); + var debugID = yield Zotero.Debug.submitToZotero(); + } catch (e) { + ps.alert(null, Zotero.getString('general.error'), e.message); return false; } - Zotero.debug(xmlhttp.responseText); - - var reported = xmlhttp.responseXML.getElementsByTagName('reported'); - if (reported.length != 1) { - ps.alert( - null, - Zotero.getString('general.error'), - Zotero.getString('general.serverError') - ); - return false; - } - - var reportID = reported[0].getAttribute('reportID'); - var buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL; var index = ps.confirmEx( null, Zotero.getString('zotero.debugOutputLogging.dialog.title'), - Zotero.getString('zotero.debugOutputLogging.dialog.sent', [ZOTERO_CONFIG.DOMAIN_NAME, reportID]), + Zotero.getString('zotero.debugOutputLogging.dialog.sent', [ZOTERO_CONFIG.DOMAIN_NAME, debugID.substr(1)]), buttonFlags, Zotero.getString('general.copyToClipboard'), null, null, null, {} @@ -365,12 +290,10 @@ ZoteroStandalone.DebugOutput = { if (index == 0) { const helper = Components.classes["@mozilla.org/widget/clipboardhelper;1"] .getService(Components.interfaces.nsIClipboardHelper); - helper.copyString("D" + reportID); + helper.copyString(debugID); } - - Zotero.Debug.clear(); return true; - }.bind(this))(); + }).apply(this, arguments); }, @@ -381,13 +304,13 @@ ZoteroStandalone.DebugOutput = { submitted = true; }); doc.querySelector('#clear-button').addEventListener('click', function (event) { - Zotero.Debug.clear(); + Zotero.Debug.clear(true); }); // If output has been submitted, disable logging when window is closed doc.defaultView.addEventListener('unload', function (event) { if (submitted) { - Zotero.Debug.setStore(false); - Zotero.Debug.clear(); + Zotero.Debug.setStore(false, true); + Zotero.Debug.clear(true); } }); }); @@ -395,7 +318,7 @@ ZoteroStandalone.DebugOutput = { clear: function () { - Zotero.Debug.clear(); + Zotero.Debug.clear(true); }, diff --git a/chrome/content/zotero/xpcom/connector/server_connector.js b/chrome/content/zotero/xpcom/connector/server_connector.js index 409d216e71a..9723bdbe554 100644 --- a/chrome/content/zotero/xpcom/connector/server_connector.js +++ b/chrome/content/zotero/xpcom/connector/server_connector.js @@ -112,6 +112,47 @@ Zotero.Server.Connector.AttachmentProgressManager = new function() { } }; + +Zotero.Server.Connector.Reports = function() {}; +Zotero.Server.Connector.Reports.reports = []; +Zotero.Server.Endpoints["/connector/reports"] = Zotero.Server.Connector.Reports; +Zotero.Server.Connector.Reports.prototype = { + supportedMethods: ["POST"], + supportedDataTypes: ["application/json"], + permitBookmarklet: true, + + /** + * An endpoint to manage error/debug logging and reporting + */ + init: async function(options) { + let data = options.data; + if ('errors' in data && 'get' in data.errors) { + let sysInfo = await Zotero.getSystemInfo(); + let errors = Zotero.Errors.getErrors(); + return [200, "text/plain", `${sysInfo}\n\n${errors.join('\n\n')}`] + } + else if ('debug' in data) { + if ('get' in data.debug) { + let debug = await Zotero.Debug.get(); + return [200, "text/plain", debug] + } + else if ('store' in data.debug) { + Zotero.Debug.setStore(data.debug.store, false); + } + else if ('clear' in data.debug) { + Zotero.Debug.clear(false); + } + return 200; + } else if ('report' in data) { + Zotero.Server.Connector.Reports.reports.push(data.report); + return 200; + } + + return 400; + } +}; + + /** * Lists all available translators, including code for translators that should be run on every page * diff --git a/chrome/content/zotero/xpcom/debug.js b/chrome/content/zotero/xpcom/debug.js deleted file mode 100644 index 0e5468f591d..00000000000 --- a/chrome/content/zotero/xpcom/debug.js +++ /dev/null @@ -1,303 +0,0 @@ -/* - ***** 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 . - - ***** END LICENSE BLOCK ***** -*/ - - -Zotero.Debug = new function () { - var _console, _stackTrace, _store, _level, _lastTime, _output = []; - var _slowTime = false; - var _colorOutput = false; - var _consoleViewer = false; - var _consoleViewerQueue = []; - var _consoleViewerListener; - - /** - * Initialize debug logging - * - * Debug logging can be set in several different ways: - * - * - via the debug.log pref in the client or connector - * - by enabling debug output logging from the Help menu - * - by passing -ZoteroDebug or -ZoteroDebugText on the command line - * - * In the client, debug.log and -ZoteroDebugText enable logging via the terminal, while -ZoteroDebug - * enables logging via an in-app HTML-based window. - * - * @param {Integer} [forceDebugLog = 0] - Force output even if pref disabled - * 2: window (-ZoteroDebug) - * 1: text console (-ZoteroDebugText) - * 0: disabled - */ - this.init = function (forceDebugLog = 0) { - _console = Zotero.Prefs.get('debug.log') || forceDebugLog == 1; - _consoleViewer = forceDebugLog == 2; - // When logging to the text console from the client on Mac/Linux, colorize output - if (_console && Zotero.isFx && !Zotero.isBookmarklet) { - _colorOutput = true; - - // Time threshold in ms above which intervals should be colored red in terminal output - _slowTime = Zotero.Prefs.get('debug.log.slowTime'); - } - _store = Zotero.Prefs.get('debug.store'); - if (_store) { - Zotero.Prefs.set('debug.store', false); - } - _level = Zotero.Prefs.get('debug.level'); - _stackTrace = Zotero.Prefs.get('debug.stackTrace'); - - this.storing = _store; - this.updateEnabled(); - - if (Zotero.isStandalone) { - // Enable dump() from window (non-XPCOM) scopes when terminal or viewer logging is enabled. - // (These will always go to the terminal, even in viewer mode.) - Zotero.Prefs.set('browser.dom.window.dump.enabled', _console || _consoleViewer, true); - - if (_consoleViewer) { - setTimeout(function () { - Zotero.openInViewer("chrome://zotero/content/debugViewer.html"); - }, 1000); - } - } - } - - this.log = function (message, level, maxDepth, stack) { - if (!this.enabled) { - return; - } - - if (typeof message != 'string') { - message = Zotero.Utilities.varDump(message, 0, maxDepth); - } - - if (!level) { - level = 3; - } - - // If level above debug.level value, don't display - if (level > _level) { - return; - } - - var deltaStr = ''; - var deltaStrStore = ''; - var delta = 0; - var d = new Date(); - if (_lastTime) { - delta = d - _lastTime; - } - _lastTime = d; - var slowPrefix = ""; - var slowSuffix = ""; - if (_slowTime && delta > _slowTime) { - slowPrefix = "\x1b[31;40m"; - slowSuffix = "\x1b[0m"; - } - - delta = ("" + delta).padStart(7, "0") - - deltaStr = "(" + slowPrefix + "+" + delta + slowSuffix + ")"; - if (_store) { - deltaStrStore = "(+" + delta + ")"; - } - - if (stack === true) { - // Display stack starting from where this was called - stack = Components.stack.caller; - } else if (stack >= 0) { - let i = stack; - stack = Components.stack.caller; - while(stack && i--) { - stack = stack.caller; - } - } else if (_stackTrace) { - // Stack trace enabled globally - stack = Components.stack.caller; - } else { - stack = undefined; - } - - if (stack) { - message += '\n' + Zotero.Debug.stackToString(stack); - } - - if (_console || _consoleViewer) { - var output = '(' + level + ')' + deltaStr + ': ' + message; - if (Zotero.isFx && !Zotero.isBookmarklet) { - // Text console - if (_console) { - dump("zotero" + output + "\n\n"); - } - // Console window - if (_consoleViewer) { - // Remove ANSI color codes. We could replace this with HTML, but it's probably - // unnecessarily distracting/alarming to show the red in the viewer. Devs who care - // about times should just use a terminal. - if (slowPrefix) { - output = output.replace(slowPrefix, '').replace(slowSuffix, ''); - } - - // If there's a listener, pass line immediately - if (_consoleViewerListener) { - _consoleViewerListener(output); - } - // Otherwise add to queue - else { - _consoleViewerQueue.push(output); - } - } - } else if(window.console) { - window.console.log(output); - } - } - if (_store) { - if (Math.random() < 1/1000) { - // Remove initial lines if over limit - var overage = this.count() - Zotero.Prefs.get('debug.store.limit'); - if (overage > 0) { - _output.splice(0, Math.abs(overage)); - } - } - _output.push('(' + level + ')' + deltaStrStore + ': ' + message); - } - } - - - this.get = Zotero.Promise.method(function(maxChars, maxLineLength) { - var output = _output; - var total = output.length; - - if (total == 0) { - return ""; - } - - if (maxLineLength) { - for (var i=0, len=output.length; i maxLineLength) { - output[i] = Zotero.Utilities.ellipsize(output[i], maxLineLength, false, true); - } - } - } - - output = output.join('\n\n'); - - if (maxChars) { - output = output.substr(maxChars * -1); - // Cut at two newlines - let matches = output.match(/^[\n]*\n\n/); - if (matches) { - output = output.substr(matches[0].length); - } - } - - return Zotero.getSystemInfo().then(function(sysInfo) { - if (Zotero.isConnector) { - return Zotero.Errors.getErrors().then(function(errors) { - return errors.join('\n\n') + - "\n\n" + sysInfo + "\n\n" + - "=========================================================\n\n" + - output; - }); - } - else { - return Zotero.getErrors(true).join('\n\n') + - "\n\n" + sysInfo + "\n\n" + - "=========================================================\n\n" + - output; - } - }); - }); - - - this.getConsoleViewerOutput = function () { - var queue = _output.concat(_consoleViewerQueue); - _consoleViewerQueue = []; - return queue; - } - - - this.addConsoleViewerListener = function (listener) { - this.enabled = _consoleViewer = true; - _consoleViewerListener = listener; - }; - - - this.removeConsoleViewerListener = function () { - _consoleViewerListener = null; - // At least for now, stop logging once console viewer is closed - _consoleViewer = false; - this.updateEnabled(); - }; - - - this.setStore = function (enable) { - if (enable) { - this.clear(); - } - _store = enable; - this.updateEnabled(); - this.storing = _store; - } - - - this.updateEnabled = function () { - this.enabled = _console || _consoleViewer || _store; - }; - - - this.count = function () { - return _output.length; - } - - - this.clear = function () { - _output = []; - } - - /** - * Format a stack trace for output in the same way that Error.stack does - * @param {Components.stack} stack - * @param {Integer} [lines=5] Number of lines to format - */ - this.stackToString = function (stack, lines) { - if (!lines) lines = 5; - var str = ''; - while(stack && lines--) { - str += '\n ' + (stack.name || '') + '@' + stack.filename - + ':' + stack.lineNumber; - stack = stack.caller; - } - return str.substr(1); - }; - - - /** - * Strip Bluebird lines from a stack trace - * - * @param {String} stack - */ - this.filterStack = function (stack) { - return stack.split(/\n/).filter(line => line.indexOf('zotero/bluebird') == -1).join('\n'); - } -} diff --git a/chrome/content/zotero/xpcom/http.js b/chrome/content/zotero/xpcom/http.js index 0218c3953b4..df2a90c51e7 100644 --- a/chrome/content/zotero/xpcom/http.js +++ b/chrome/content/zotero/xpcom/http.js @@ -621,7 +621,7 @@ Zotero.HTTP = new function() { * through the error log and doing a fragile string comparison. */ _pacInstalled = function () { - return Zotero.getErrors(true).some(val => val.indexOf("PAC file installed") == 0) + return Zotero.Errors.getErrors(true).some(val => val.indexOf("PAC file installed") == 0) } diff --git a/chrome/content/zotero/xpcom/reports.js b/chrome/content/zotero/xpcom/reports.js new file mode 100644 index 00000000000..359bfaa2f40 --- /dev/null +++ b/chrome/content/zotero/xpcom/reports.js @@ -0,0 +1,545 @@ +/* + ***** 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 . + + ***** END LICENSE BLOCK ***** +*/ + +Zotero.Errors = new function() { + // Errors that were in the console at startup + var _startupErrors = []; + // Number of errors to maintain in the recent errors buffer + const ERROR_BUFFER_SIZE = 25; + // A rolling buffer of the last ERROR_BUFFER_SIZE errors + var _recentErrors = []; + + /** + * Observer for console messages + * @namespace + */ + var ConsoleListener = { + "QueryInterface":XPCOMUtils.generateQI([Components.interfaces.nsIConsoleMessage, + Components.interfaces.nsISupports]), + "observe":function(msg) { + if(!_shouldKeepError(msg)) return; + if(_recentErrors.length === ERROR_BUFFER_SIZE) _recentErrors.shift(); + _recentErrors.push(msg); + } + }; + + /** + * Determines whether to keep an error message so that it can (potentially) be reported later + */ + function _shouldKeepError(msg) { + const skip = ['CSS Parser', 'content javascript']; + + //Zotero.debug(msg); + try { + msg.QueryInterface(Components.interfaces.nsIScriptError); + //Zotero.debug(msg); + if (skip.indexOf(msg.category) != -1 || msg.flags & msg.warningFlag) { + return false; + } + } + catch (e) { } + + const blacklist = [ + "No chrome package registered for chrome://communicator", + '[JavaScript Error: "Components is not defined" {file: "chrome://nightly/content/talkback/talkback.js', + '[JavaScript Error: "document.getElementById("sanitizeItem")', + 'No chrome package registered for chrome://piggy-bank', + '[JavaScript Error: "[Exception... "\'Component is not available\' when calling method: [nsIHandlerService::getTypeFromExtension', + '[JavaScript Error: "this._uiElement is null', + 'Error: a._updateVisibleText is not a function', + '[JavaScript Error: "Warning: unrecognized command line flag ', + 'LibX:', + 'function skype_', + '[JavaScript Error: "uncaught exception: Permission denied to call method Location.toString"]', + 'CVE-2009-3555', + 'OpenGL', + 'trying to re-register CID', + 'Services.HealthReport', + '[JavaScript Error: "this.docShell is null"', + '[JavaScript Error: "downloadable font:', + '[JavaScript Error: "Image corrupt or truncated:', + '[JavaScript Error: "The character encoding of the', + 'nsLivemarkService.js', + 'Sync.Engine.Tabs', + 'content-sessionStore.js', + 'org.mozilla.appSessions', + 'bad script XDR magic number' + ]; + + for (var i=0; i messages[i]) + .filter(msg => _shouldKeepError(msg)); + } catch(e) { + Zotero.logError(e); + } + // Register error observer + Services.console.registerListener(ConsoleListener); + + // Add shutdown listener to remove quit-application observer and console listener + Zotero.addShutdownListener(function() { + Services.console.unregisterListener(ConsoleListener); + }); + }; + + this.getErrors = function (asStrings) { + var errors = []; + + for (let msg of _startupErrors.concat(_recentErrors)) { + let altMessage; + // Remove password in malformed XML errors + if (msg.category == 'malformed-xml') { + try { + // msg.message is read-only, so store separately + altMessage = msg.message.replace(/(https?:\/\/[^:]+:)([^@]+)(@[^"]+)/, "$1****$3"); + } + catch (e) {} + } + + if (asStrings) { + errors.push(altMessage || msg.message) + } + else { + errors.push(msg); + } + } + return errors; + }; + + this.showReportDialog = function() { + var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] + .getService(Components.interfaces.nsIWindowWatcher); + var data = { + msg: Zotero.getString('errorReport.followingReportWillBeSubmitted'), + }; + var io = { wrappedJSObject: { Zotero: Zotero, data: data } }; + var win = ww.openWindow(null, "chrome://zotero/content/errorReport.xul", + "zotero-error-report", "chrome,centerscreen,modal", io); + } + + + this.generateReport = async function () { + let sysInfo = await Zotero.getSystemInfo(); + return sysInfo + "\n\n" + (this.getErrors()).join('\n\n') + "\n\n" + } + + + this.fetchConnectorReports = async function(debug) { + if (!Zotero.ConnectorNotifier._listeners.length) { + return []; + } + if (debug) { + Zotero.ConnectorNotifier.notifyListeners('reports', {debug: {get: true}}); + } else { + Zotero.ConnectorNotifier.notifyListeners('reports', {errors: {get: true}}); + } + // We don't know if any of the listeners will respond and if they do, when, + // so we just wait a second. + await Zotero.Promise.delay(999); + let reports = Zotero.Server.Connector.Reports.reports; + Zotero.Server.Connector.Reports.reports = []; + return reports; + } + + + this.submitToZotero = async function(debug) { + var headers = {'Content-Type': 'text/plain'}; + var url; + var zoteroBody; + if (debug) { + url = ZOTERO_CONFIG.REPOSITORY_URL + "report?debug=1"; + zoteroBody = await Zotero.Debug.get( + Zotero.Prefs.get('debug.store.submitSize'), + Zotero.Prefs.get('debug.store.submitLineLength') + ); + } else { + // TODO: change to non-debug URL once that is supported + url = ZOTERO_CONFIG.REPOSITORY_URL + "report?debug=1"; + zoteroBody = await this.generateReport(); + } + var connectorBodies = await this.fetchConnectorReports(debug); + + let date = (new Date()).toUTCString(); + let type = debug ? "Debug" : "Report"; + let body = `----------------------------- Zotero ${type}: ${date} --------------------------------\n\n`; + body += zoteroBody; + for (let connectorBody of connectorBodies) { + body += `\n\n----------------------------- Connector ${type} --------------------------------\n\n`; + body += connectorBody; + } + + try { + var xmlhttp = await Zotero.HTTP.request( + "POST", + url, + { + compressBody: true, + body, + headers, + logBodyLength: 30, + timeout: 15000, + requestObserver: function (req) { + // Don't fail during tests, with fake XHR + if (!req.channel) { + return; + } + req.channel.notificationCallbacks = { + onProgress: function (request, context, progress, progressMax) {}, + + // nsIInterfaceRequestor + getInterface: function (iid) { + try { + return this.QueryInterface(iid); + } + catch (e) { + throw Components.results.NS_NOINTERFACE; + } + }, + + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsIInterfaceRequestor) || + iid.equals(Components.interfaces.nsIProgressEventSink)) { + return this; + } + throw Components.results.NS_NOINTERFACE; + }, + + } + } + } + ); + } + catch (e) { + Zotero.logError(e); + if (e instanceof Zotero.HTTP.BrowserOfflineException) { + throw new Error(Zotero.getString('general.browserIsOffline', Zotero.appName)); + } + throw new Error(Zotero.getString('general.invalidResponseServer')); + } + + Zotero.debug(xmlhttp.responseText); + + var reported = xmlhttp.responseXML.getElementsByTagName('reported'); + if (reported.length != 1) { + throw new Error(Zotero.getString('errorReport.invalidResponseServer')); + } + + var reportID = reported[0].getAttribute('reportID'); + + if (debug) { + return 'D' + reportID; + } + // TODO: remove the D once endpoint is updated for reports + return 'D' + reportID; + } +} + +Zotero.Debug = new function () { + var _console, _stackTrace, _store, _level, _lastTime, _output = []; + var _slowTime = false; + var _colorOutput = false; + var _consoleViewer = false; + var _consoleViewerQueue = []; + var _consoleViewerListener; + + /** + * Initialize debug logging + * + * Debug logging can be set in several different ways: + * + * - via the debug.log pref in the client or connector + * - by enabling debug output logging from the Help menu + * - by passing -ZoteroDebug or -ZoteroDebugText on the command line + * + * In the client, debug.log and -ZoteroDebugText enable logging via the terminal, while -ZoteroDebug + * enables logging via an in-app HTML-based window. + * + * @param {Integer} [forceDebugLog = 0] - Force output even if pref disabled + * 2: window (-ZoteroDebug) + * 1: text console (-ZoteroDebugText) + * 0: disabled + */ + this.init = function (forceDebugLog = 0) { + _console = Zotero.Prefs.get('debug.log') || forceDebugLog == 1; + _consoleViewer = forceDebugLog == 2; + // When logging to the text console from the client on Mac/Linux, colorize output + if (_console && Zotero.isFx && !Zotero.isBookmarklet) { + _colorOutput = true; + + // Time threshold in ms above which intervals should be colored red in terminal output + _slowTime = Zotero.Prefs.get('debug.log.slowTime'); + } + _store = Zotero.Prefs.get('debug.store'); + if (_store) { + Zotero.Prefs.set('debug.store', false); + } + _level = Zotero.Prefs.get('debug.level'); + _stackTrace = Zotero.Prefs.get('debug.stackTrace'); + + this.storing = _store; + this.updateEnabled(); + + if (Zotero.isStandalone) { + // Enable dump() from window (non-XPCOM) scopes when terminal or viewer logging is enabled. + // (These will always go to the terminal, even in viewer mode.) + Zotero.Prefs.set('browser.dom.window.dump.enabled', _console || _consoleViewer, true); + + if (_consoleViewer) { + setTimeout(function () { + Zotero.openInViewer("chrome://zotero/content/debugViewer.html"); + }, 1000); + } + } + } + + this.log = function (message, level, maxDepth, stack) { + if (!this.enabled) { + return; + } + + if (typeof message != 'string') { + message = Zotero.Utilities.varDump(message, 0, maxDepth); + } + + if (!level) { + level = 3; + } + + // If level above debug.level value, don't display + if (level > _level) { + return; + } + + var deltaStr = ''; + var deltaStrStore = ''; + var delta = 0; + var d = new Date(); + if (_lastTime) { + delta = d - _lastTime; + } + _lastTime = d; + var slowPrefix = ""; + var slowSuffix = ""; + if (_slowTime && delta > _slowTime) { + slowPrefix = "\x1b[31;40m"; + slowSuffix = "\x1b[0m"; + } + + delta = ("" + delta).padStart(7, "0") + + deltaStr = "(" + slowPrefix + "+" + delta + slowSuffix + ")"; + if (_store) { + deltaStrStore = "(+" + delta + ")"; + } + + if (stack === true) { + // Display stack starting from where this was called + stack = Components.stack.caller; + } else if (stack >= 0) { + let i = stack; + stack = Components.stack.caller; + while(stack && i--) { + stack = stack.caller; + } + } else if (_stackTrace) { + // Stack trace enabled globally + stack = Components.stack.caller; + } else { + stack = undefined; + } + + if (stack) { + message += '\n' + Zotero.Debug.stackToString(stack); + } + + if (_console || _consoleViewer) { + var output = '(' + level + ')' + deltaStr + ': ' + message; + if (Zotero.isFx && !Zotero.isBookmarklet) { + // Text console + if (_console) { + dump("zotero" + output + "\n\n"); + } + // Console window + if (_consoleViewer) { + // Remove ANSI color codes. We could replace this with HTML, but it's probably + // unnecessarily distracting/alarming to show the red in the viewer. Devs who care + // about times should just use a terminal. + if (slowPrefix) { + output = output.replace(slowPrefix, '').replace(slowSuffix, ''); + } + + // If there's a listener, pass line immediately + if (_consoleViewerListener) { + _consoleViewerListener(output); + } + // Otherwise add to queue + else { + _consoleViewerQueue.push(output); + } + } + } else if(window.console) { + window.console.log(output); + } + } + if (_store) { + if (Math.random() < 1/1000) { + // Remove initial lines if over limit + var overage = this.count() - Zotero.Prefs.get('debug.store.limit'); + if (overage > 0) { + _output.splice(0, Math.abs(overage)); + } + } + _output.push('(' + level + ')' + deltaStrStore + ': ' + message); + } + } + + + this.get = async function(maxChars, maxLineLength) { + var output = _output; + + if (maxLineLength) { + for (var i=0, len=output.length; i maxLineLength) { + output[i] = Zotero.Utilities.ellipsize(output[i], maxLineLength, false, true); + } + } + } + + output = output.join('\n\n'); + + if (maxChars) { + output = output.substr(maxChars * -1); + // Cut at two newlines + let matches = output.match(/^[\n]*\n\n/); + if (matches) { + output = output.substr(matches[0].length); + } + } + + let errors = await Zotero.Errors.generateReport(); + return errors + "\n\n" + + "=========================================================\n\n" + + output; + }; + + + this.getConsoleViewerOutput = function () { + var queue = _output.concat(_consoleViewerQueue); + _consoleViewerQueue = []; + return queue; + } + + + this.addConsoleViewerListener = function (listener) { + this.enabled = _consoleViewer = true; + _consoleViewerListener = listener; + }; + + + this.removeConsoleViewerListener = function () { + _consoleViewerListener = null; + // At least for now, stop logging once console viewer is closed + _consoleViewer = false; + this.updateEnabled(); + }; + + + this.setStore = function (enable, fetchFromConnectors) { + if (enable) { + this.clear(fetchFromConnectors); + } + _store = enable; + this.updateEnabled(); + this.storing = _store; + if (fetchFromConnectors) { + Zotero.ConnectorNotifier.notifyListeners('reports', {debug: {store: enable}}); + } + } + + + this.updateEnabled = function () { + this.enabled = _console || _consoleViewer || _store; + }; + + + this.count = function () { + return _output.length; + } + + + this.clear = function (fetchFromConnectors) { + _output = []; + if (fetchFromConnectors) { + Zotero.ConnectorNotifier.notifyListeners('reports', {debug: {clear: true}}); + } + } + + /** + * Format a stack trace for output in the same way that Error.stack does + * @param {Components.stack} stack + * @param {Integer} [lines=5] Number of lines to format + */ + this.stackToString = function (stack, lines) { + if (!lines) lines = 5; + var str = ''; + while(stack && lines--) { + str += '\n ' + (stack.name || '') + '@' + stack.filename + + ':' + stack.lineNumber; + stack = stack.caller; + } + return str.substr(1); + }; + + + /** + * Strip Bluebird lines from a stack trace + * + * @param {String} stack + */ + this.filterStack = function (stack) { + return stack.split(/\n/).filter(line => line.indexOf('zotero/bluebird') == -1).join('\n'); + } + + this.submitToZotero = async function () { + Zotero.Debug.setStore(false, true); + let debugID = await Zotero.Errors.submitToZotero(true); + Zotero.Debug.clear(true); + return debugID; + }; +} diff --git a/chrome/content/zotero/xpcom/server.js b/chrome/content/zotero/xpcom/server.js index 40c90e379ea..6f0db7b6420 100755 --- a/chrome/content/zotero/xpcom/server.js +++ b/chrome/content/zotero/xpcom/server.js @@ -559,7 +559,7 @@ Zotero.Server.DataListener.prototype._requestFinished = function(response) { intlStream.init(this.oStream, "UTF-8", 1024, "?".charCodeAt(0)); // write response - Zotero.debug(response, 5); + Zotero.debug(response.replace(/\r?\n/g, '\\n'), 5); intlStream.writeString(response); } finally { intlStream.close(); diff --git a/chrome/content/zotero/xpcom/sync/syncRunner.js b/chrome/content/zotero/xpcom/sync/syncRunner.js index cbd098a7fd2..fb1d1d87237 100644 --- a/chrome/content/zotero/xpcom/sync/syncRunner.js +++ b/chrome/content/zotero/xpcom/sync/syncRunner.js @@ -1322,7 +1322,7 @@ Zotero.Sync.Runner_Module = function (options = {}) { if (e.dialogButtonText === undefined) { var buttonText = Zotero.getString('errorReport.reportError'); var buttonCallback = function () { - doc.defaultView.ZoteroPane.reportErrors(); + Zotero.Errors.showReportDialog(); }; } else { diff --git a/chrome/content/zotero/xpcom/utilities_internal.js b/chrome/content/zotero/xpcom/utilities_internal.js index df74d48da84..07757fdb47f 100644 --- a/chrome/content/zotero/xpcom/utilities_internal.js +++ b/chrome/content/zotero/xpcom/utilities_internal.js @@ -369,7 +369,7 @@ Zotero.Utilities.Internal = { if (typeof buttonText == 'undefined') { buttonText = Zotero.getString('errorReport.reportError'); buttonCallback = function () { - win.ZoteroPane.reportErrors(); + Zotero.Errors.showReportDialog(); } } // If secondary button is explicitly null, just use an alert diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js index 60cd807716c..84ffcc462ca 100644 --- a/chrome/content/zotero/xpcom/zotero.js +++ b/chrome/content/zotero/xpcom/zotero.js @@ -130,12 +130,6 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js"); var _runningTimers = new Map(); var _startupTime = new Date(); - // Errors that were in the console at startup - var _startupErrors = []; - // Number of errors to maintain in the recent errors buffer - const ERROR_BUFFER_SIZE = 25; - // A rolling buffer of the last ERROR_BUFFER_SIZE errors - var _recentErrors = []; /** * Initialize the extension @@ -278,6 +272,7 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js"); Zotero.Prefs.init(); Zotero.Debug.init(options && options.forceDebugLog); + Zotero.Errors.init(); // Make sure that Zotero Standalone is not running as root if(Zotero.isStandalone && !Zotero.isWin) _checkRoot(); @@ -399,6 +394,9 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js"); // Register shutdown handler to call Zotero.shutdown() var _shutdownObserver = {observe:function() { Zotero.shutdown().done() }}; Services.obs.addObserver(_shutdownObserver, "quit-application", false); + Zotero.addShutdownListener(function() { + Services.obs.removeObserver(_shutdownObserver, "quit-application", false); + }); try { Zotero.IPC.init(); @@ -409,25 +407,7 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js"); } throw (e); } - - // Get startup errors - try { - var messages = {}; - Services.console.getMessageArray(messages, {}); - _startupErrors = Object.keys(messages.value).map(i => messages[i]) - .filter(msg => _shouldKeepError(msg)); - } catch(e) { - Zotero.logError(e); - } - // Register error observer - Services.console.registerListener(ConsoleListener); - - // Add shutdown listener to remove quit-application observer and console listener - this.addShutdownListener(function() { - Services.obs.removeObserver(_shutdownObserver, "quit-application", false); - Services.console.unregisterListener(ConsoleListener); - }); - + // Load additional info for connector or not if(Zotero.isConnector) { Zotero.debug("Loading in connector mode"); @@ -1303,31 +1283,6 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js"); } - this.getErrors = function (asStrings) { - var errors = []; - - for (let msg of _startupErrors.concat(_recentErrors)) { - let altMessage; - // Remove password in malformed XML errors - if (msg.category == 'malformed-xml') { - try { - // msg.message is read-only, so store separately - altMessage = msg.message.replace(/(https?:\/\/[^:]+:)([^@]+)(@[^"]+)/, "$1****$3"); - } - catch (e) {} - } - - if (asStrings) { - errors.push(altMessage || msg.message) - } - else { - errors.push(msg); - } - } - return errors; - } - - /** * Get versions, platform, etc. */ @@ -1344,12 +1299,7 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js"); var extensions = yield Zotero.getInstalledExtensions(); info.extensions = extensions.join(', '); - var str = ''; - for (var key in info) { - str += key + ' => ' + info[key] + ', '; - } - str = str.substr(0, str.length - 2); - return str; + return JSON.stringify(info, null, ' '); }); @@ -2026,59 +1976,6 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js"); handler.preferredAction = Components.interfaces.nsIHandlerInfo.useSystemDefault; handler.launchWithURI(uri, null); } - - /** - * Determines whether to keep an error message so that it can (potentially) be reported later - */ - function _shouldKeepError(msg) { - const skip = ['CSS Parser', 'content javascript']; - - //Zotero.debug(msg); - try { - msg.QueryInterface(Components.interfaces.nsIScriptError); - //Zotero.debug(msg); - if (skip.indexOf(msg.category) != -1 || msg.flags & msg.warningFlag) { - return false; - } - } - catch (e) { } - - const blacklist = [ - "No chrome package registered for chrome://communicator", - '[JavaScript Error: "Components is not defined" {file: "chrome://nightly/content/talkback/talkback.js', - '[JavaScript Error: "document.getElementById("sanitizeItem")', - 'No chrome package registered for chrome://piggy-bank', - '[JavaScript Error: "[Exception... "\'Component is not available\' when calling method: [nsIHandlerService::getTypeFromExtension', - '[JavaScript Error: "this._uiElement is null', - 'Error: a._updateVisibleText is not a function', - '[JavaScript Error: "Warning: unrecognized command line flag ', - 'LibX:', - 'function skype_', - '[JavaScript Error: "uncaught exception: Permission denied to call method Location.toString"]', - 'CVE-2009-3555', - 'OpenGL', - 'trying to re-register CID', - 'Services.HealthReport', - '[JavaScript Error: "this.docShell is null"', - '[JavaScript Error: "downloadable font:', - '[JavaScript Error: "Image corrupt or truncated:', - '[JavaScript Error: "The character encoding of the', - 'nsLivemarkService.js', - 'Sync.Engine.Tabs', - 'content-sessionStore.js', - 'org.mozilla.appSessions', - 'bad script XDR magic number' - ]; - - for (var i=0; i - + diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties index 06cbdead19e..1ac8ffc9127 100644 --- a/chrome/locale/en-US/zotero/zotero.properties +++ b/chrome/locale/en-US/zotero/zotero.properties @@ -102,9 +102,6 @@ errorReport.advanceMessage = Press %S to send the report to the Zotero develop errorReport.stepsToReproduce = Steps to Reproduce: errorReport.expectedResult = Expected result: errorReport.actualResult = Actual result: -errorReport.noNetworkConnection = No network connection -errorReport.invalidResponseRepository = Invalid response from repository -errorReport.repoCannotBeContacted = Repository cannot be contacted attachmentBasePath.selectDir = Choose Base Directory @@ -681,7 +678,6 @@ zotero.debugOutputLogging = Debug Output Logging zotero.debugOutputLogging.linesLogged = %1$S line logged;%1$S lines logged zotero.debugOutputLogging.dialog.title = Debug Output Submitted zotero.debugOutputLogging.dialog.sent = Debug output has been sent to %S.\n\nThe Debug ID is D%S. -zotero.debugOutputLogging.dialog.error = An error occurred sending debug output. zotero.debugOutputLogging.enabledAfterRestart = Debug output logging will be enabled after %S restarts. dragAndDrop.existingFiles = The following files already existed in the destination directory and were not copied: diff --git a/components/zotero-service.js b/components/zotero-service.js index 90761a4dbed..4c59eee78fa 100644 --- a/components/zotero-service.js +++ b/components/zotero-service.js @@ -35,7 +35,6 @@ const xpcomFilesAll = [ 'zotero', 'dataDirectory', 'date', - 'debug', 'error', 'file', 'http', @@ -45,6 +44,7 @@ const xpcomFilesAll = [ 'profile', 'progressWindow', 'proxy', + 'reports', 'translation/translate', 'translation/translate_firefox', 'translation/translator',