diff options
-rwxr-xr-x | bin/rapporter-feil | 452 | ||||
-rw-r--r-- | rapporter-feil.cfg | 87 |
2 files changed, 539 insertions, 0 deletions
diff --git a/bin/rapporter-feil b/bin/rapporter-feil new file mode 100755 index 0000000..6e710c8 --- /dev/null +++ b/bin/rapporter-feil @@ -0,0 +1,452 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright 2009-2011 Linnea Skogtvedt <linnea@linuxavdelingen.no> +# Copyright 2016 Mike Gabriel <mike.gabriel@das-netzwerkteam.de> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the +# Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + +import locale +import re +import sys +import configparser +from optparse import OptionParser + +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import QApplication, QDialog, QLabel, QPlainTextEdit, QDialogButtonBox, QGridLayout, QLineEdit, QCheckBox, QMessageBox + +class ConfigException(BaseException): + pass + +class RapporterFeilDialog(QDialog): + def __init__(self, title, userMsg, urlOpener, idleTimeout, customFields, onAccept, onTimeout): + super(RapporterFeilDialog, self).__init__(None) + + def openLink(link): + import shlex + argv = shlex.split(urlOpener) + [link] + QProcess.startDetached(argv[0], argv[1:]) + + def updateTitle(): + assert isinstance(title, str) + self.setWindowTitle(u'%s (%d...)' % (title, self.idleTimeout-self.idleCount)) + + def updateIdleCount(): + self.idleCount += 1 + updateTitle() + if self.idleCount == self.idleTimeout: + onTimeout() + + def resetIdleCount(ignored=None): + """ + Called when the user edits anything. Stops the idle timer and starts + a timer to restart the idle timer after idleTimeout seconds. + """ + if idleTimeout == 0: + return + self.idleCount = 0 + self.timer.stop() + self.resetTimer.stop() + self.resetTimer.start(self.idleTimeout*1000) + self.setWindowTitle(title) + + msgLabel = QLabel(userMsg) + msgLabel.setTextFormat(Qt.RichText) + msgLabel.linkActivated.connect(openLink) + + self.msgTextEdit = QPlainTextEdit() + self.msgTextEdit.textChanged.connect(resetIdleCount) + + buttonBox = QDialogButtonBox() + buttonBox.addButton('Send', QDialogButtonBox.AcceptRole) + buttonBox.addButton(QDialogButtonBox.Cancel) + + layout = QGridLayout() + # w, row, col, rowspan, colspan + layout.addWidget(msgLabel, 0, 0, 1, 2) + + self.idleTimeout = idleTimeout + self.idleCount = 0 + self.timer = QTimer() + self.timer.setInterval(1000) + self.timer.timeout.connect(updateIdleCount) + if self.idleTimeout != 0: + self.timer.start() + self.resetTimer = QTimer() + self.resetTimer.setSingleShot(True) + self.resetTimer.timeout.connect(self.timer.start) + + row = 1 + self.customFieldWidgets = [] # [ [label, widget] ] + for label, initValue in customFields: + if initValue in ('0|1', '1|0'): # checkbox + checkbox = QCheckBox(label) + checkbox.setChecked(initValue.split('|')[0] == '1') + checkbox.stateChanged.connect(resetIdleCount) + self.customFieldWidgets.append([checkbox, checkbox]) + layout.addWidget(checkbox, row, 0, 1, 2) # span 1 row and 2 columns + else: + label, lineEdit = QLabel(label), QLineEdit(initValue) + lineEdit.textChanged.connect(resetIdleCount) + self.customFieldWidgets.append([label, lineEdit]) + layout.addWidget(label, row, 0) + layout.addWidget(lineEdit, row, 1) + row += 1 + + layout.addWidget(self.msgTextEdit, row, 0, 5, 2) + row += 5 + layout.addWidget(buttonBox, row, 0, 1, 2) + self.setLayout(layout) + + buttonBox.accepted.connect(onAccept) + buttonBox.rejected.connect(self.reject) + + self.setWindowTitle(title) + + def get(self): + ''' + Return tuple (msg, customFields) where + customFields is a list of (label, text) tuples. + ''' + def custom(): + for label, widget in self.customFieldWidgets: + if isinstance(widget, QCheckBox): + yield label.text(), int(widget.isChecked()) + else: + yield label.text(), widget.text() + return self.msgTextEdit.toPlainText(), list(custom()) + + def showConfirmation(self, title, msg): + QMessageBox.information(self, title, msg) + + def showError(self, title, msg, exc): + msg = '%s\n%s: %s' % (msg, type(exc).__name__, exc) + QMessageBox.critical(self, title, msg) + +class LocalConfigParser(configparser.RawConfigParser): + """ + RawConfigParser subclass which expands shell $() syntax in values. + In addition, option names are not case sensitive. + + >>> p = LocalConfigParser() + >>> p.add_section('test') + >>> p.set('test', 'Test æ', '$(echo "æ") $(echo "ø")') + >>> p.set('test', 'test \xff', '') + >>> p.items('test') + [(u'Test \\xe6', u'\\xe6 \\xf8'), (u'test \ufffd', u'')] + """ + optionxform = str + + def _getoutput(self, cmd): + import subprocess + p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) + output = p.stdout.read().decode().rstrip() + p.wait() + return output + + def _expand(self, value): + def replace(match): + return self._getoutput(match.group(1)) + assert isinstance(value, str) + return re.sub('\$\((.*?)\)', replace, value) + + def get(self, section, option): + return self._expand(configparser.RawConfigParser.get(self, section, option)) + + def items(self, section): + return [ (k, self._expand(v)) for (k, v) in configparser.RawConfigParser.items(self, section) ] + +def createEmail(fromAddr, toAddrs, ccAddrs, subject, msg, custom, info): + """ + Create email message, returned in plain text and MIME encoded. + + >>> t, m = createEmail(u'from@example.org', [u'to@example.org'], None, u'Test', u'', + ... [(u'a', u'\xe5')], [(u'c', u'd')]) + >>> assert 'a: å' in t + >>> assert 'From: from@example.org' in m + """ + assert isinstance(msg, str) + + emailText = msg + '\n\n' + \ + '\n'.join(['%s: %s' % (f, v) for (f,v) in info]) + '\n' + \ + '\n'.join(['%s: %s' % (f, v) for (f,v) in custom]) + + import email.mime.text + emailText = emailText + m = email.mime.text.MIMEText(emailText, 'plain', 'utf-8') + m.add_header('From', fromAddr) + m.add_header('To', ', '.join(toAddrs)) + if ccAddrs: + m.add_header('Cc', ', '.join(ccAddrs)) + m.add_header('Subject', subject) + return (emailText, m.as_string()) + +def sendEmail(fromAddr, toAddrs, smtpServer, emailMime): + """ + Send the email. + + >>> from minimock import Mock, restore + >>> import smtplib + >>> smtplib.SMTP = Mock('smtplib.SMTP') + >>> smtplib.SMTP.mock_returns = Mock('smtp_connection') + >>> sendEmail(u'from@example.org', [u'to@example.org'], u'smtp.example.org', 'body') + Called smtplib.SMTP('smtp.example.org') + Called smtp_connection.sendmail('from@example.org', ['to@example.org'], 'body') + Called smtp_connection.quit() + >>> + """ + fromAddr = fromAddr + toAddrs = toAddrs + smtpServer = smtpServer + + import smtplib + server = smtplib.SMTP(smtpServer) + server.sendmail(fromAddr, toAddrs, emailMime) + server.quit() + +def exportFields(fields): + """ + Export the fields to the environment, prepending 'RF_' to the variable name + and encoding variable and value to utf-8. Everything not matching [A-Z] is + stripped from the variable name. + + >>> exportFields([[u'Te09_.\xe5st', u'\xe5']]) + >>> import os + >>> os.environ['RF_TEST'] + '\\xc3\\xa5' + >>> + """ + import os + import re + for k, v in fields: + k = 'RF_' + re.sub(r'[^A-Z]', '', k.upper()) + os.environ[k] = str(v) + +def runCallbacks(callbacks, input): + """ + Run commands, writing input to their stdin. + + >>> import os + >>> pipe_r, pipe_w = os.pipe() + >>> runCallbacks(['cat >&%d' % pipe_w], 'å') + >>> os.read(pipe_r, 4096) + '\\xc3\\xa5' + """ + def runCallback(cmd, input): + assert isinstance(cmd, str) + import subprocess + print ("CALLBACK: ", cmd, input) + p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE) + try: + p.stdin.write(bytes(input, 'UTF-8')) + except(IOError): + pass + p.stdin.close() + p.wait() + assert isinstance(input, str) + + for cmd in callbacks: + runCallback(cmd, input) + +class RapporterFeil(object): + + dialogcls = RapporterFeilDialog + + def __init__(self): + self.config_tips = LocalConfigParser() + self.config_rapporter = LocalConfigParser() + self.dialog = None + import sys + self.app = QApplication(sys.argv) + locale = QLocale.system().name() + self.qtTranslator = QTranslator() + if self.qtTranslator.load('qt_%s' % locale, '/usr/share/qt5/translations'): + self.app.installTranslator(self.qtTranslator) + + def main(self): + parser = OptionParser(usage="usage: %prog [options]") + parser.add_option('-t', '--timeout', help='idle timeout in seconds', metavar='SECONDS', type='int', default=0) + parser.add_option('-c', '--cfg-tips', help='configuration file for user hints', metavar='FILE', type='string', default='/etc/tips.cfg') + parser.add_option('-r', '--cfg-rapporter', help='configuration file for error reporting', metavar='FILE', type='string', default='/etc/rapporter-feil.cfg') + options, args = parser.parse_args() + if args: + parser.error('no positional arguments allowed') + + self.config_tips.readfp(open(options.cfg_tips, 'r')) + self.config_rapporter.readfp(open(options.cfg_rapporter, 'r')) + + lang, encoding = locale.getlocale() + lang, region = lang.split("_") + + title_options = [ + 'title[{lang}_{region}]'.format(lang=lang, region=region), + 'title[{lang}]'.format(lang=lang), + 'title', + ] + for option in title_options: + if self.config_rapporter.has_option('rapporter-feil', option): + title = self.config_rapporter.get('rapporter-feil', option) + break + else: + raise ConfigException + + msg_options = [ + 'msg[{lang}_{region}]'.format(lang=lang, region=region), + 'msg[{lang}]'.format(lang=lang), + 'msg', + ] + for option in msg_options: + if self.config_tips.has_option('tips', option): + msg = self.config_tips.get('tips', option) + break + else: + raise ConfigException + + custom_fields_section = 'custom_fields:{lang}_{region}'.format(lang=lang, region=region) + + if not self.config_rapporter.has_section(custom_fields_section): + custom_fields_section = 'custom_fields:{lang}'.format(lang=lang) + if not self.config_rapporter.has_section(custom_fields_section): + custom_fields_section = 'custom_fields' + + customFields = self.config_rapporter.items(custom_fields_section) + + try: + urlOpener = self.config_tips.get('tips', 'open_urls_with') + except(configparser.NoOptionError): + urlOpener = 'xdg-open' + self.dialog = self.dialogcls(title, msg, urlOpener, options.timeout, customFields, self.onAccept, self.onTimeout) + self.dialog.show() + self.app.exec_() + + def onAccept(self): + + lang, encoding = locale.getlocale() + lang, region = lang.split("_") + + msg, customFields = self.dialog.get() + if not msg: + return + exportFields(customFields) + + info_section = 'info:{lang}_{region}'.format(lang=lang, region=region) + if not self.config_rapporter.has_section(info_section): + info_section = 'info:{lang}'.format(lang=lang) + if not self.config_rapporter.has_section(info_section): + info_section = 'info' + + exportFields(self.config_rapporter.items(info_section)) + + fromAddr = self.config_rapporter.get('email', 'from_address') + toAddrs = self.config_rapporter.get('email', 'to_addresses').split() + try: + ccAddrs = self.config_rapporter.get('email', 'cc_addresses').split() + except(configparser.NoOptionError): + ccAddrs = [] + try: + bccAddrs = self.config_rapporter.get('email', 'bcc_addresses').split() + except configparser.NoOptionError: + bccAddrs = [] + + title_options = [ + 'title[{lang}_{region}]'.format(lang=lang, region=region), + 'title[{lang}]'.format(lang=lang), + 'title', + ] + for option in title_options: + if self.config_rapporter.has_option('rapporter-feil', option): + title = self.config_rapporter.get('rapporter-feil', option) + break + else: + raise ConfigException + + errormsg_options = [ + 'error_msg[{lang}_{region}]'.format(lang=lang, region=region), + 'error_msg[{lang}]'.format(lang=lang), + 'error_msg', + ] + for option in errormsg_options: + if self.config_rapporter.has_option('rapporter-feil', option): + error_msg = self.config_rapporter.get('rapporter-feil', option) + break + else: + raise ConfigException + + confirmmsg_options = [ + 'confirmation_msg[{lang}_{region}]'.format(lang=lang, region=region), + 'confirmation_msg[{lang}]'.format(lang=lang), + 'confirmation_msg', + ] + for option in confirmmsg_options: + if self.config_rapporter.has_option('rapporter-feil', option): + confirmation_msg = self.config_rapporter.get('rapporter-feil', option) + break + else: + raise ConfigException + + subject_options = [ + 'subject[{lang}_{region}]'.format(lang=lang, region=region), + 'subject[{lang}]'.format(lang=lang), + 'subject', + ] + for option in subject_options: + if self.config_rapporter.has_option('email', option): + subject = self.config_rapporter.get('email', option) + break + else: + raise ConfigException + + + smtpServer = self.config_rapporter.get('email', 'smtp_server') + + print (info_section) + emailText, emailMime = createEmail(fromAddr, toAddrs, ccAddrs, subject, + msg, customFields, self.config_rapporter.items(info_section)) + + plain_callbacks_section = 'plain_callbacks:{lang}_{region}'.format(lang=lang, region=region) + if not self.config_rapporter.has_section(plain_callbacks_section): + plain_callbacks_section = 'plain_callbacks:{lang}'.format(lang=lang) + if not self.config_rapporter.has_section(plain_callbacks_section): + plain_callbacks_section = 'plain_callbacks' + + mime_callbacks_section = 'mime_callbacks:{lang}_{region}'.format(lang=lang, region=region) + if not self.config_rapporter.has_section(mime_callbacks_section): + mime_callbacks_section = 'mime_callbacks:{lang}'.format(lang=lang) + if not self.config_rapporter.has_section(mime_callbacks_section): + mime_callbacks_section = 'mime_callbacks' + + for section, data in ((plain_callbacks_section, emailText), (mime_callbacks_section, emailMime)): + commands = [ cmd for (ign, cmd) in self.config_rapporter.items(section)] + runCallbacks(commands, data) + + if smtpServer: + try: + sendEmail(fromAddr, toAddrs + ccAddrs + bccAddrs, smtpServer, emailMime) + except Exception as e: + self.dialog.showError(title, + error_msg, + e) + return + self.dialog.showConfirmation(title, + confirmation_msg) + self.app.exit() + + def onTimeout(self): + self.app.exit() + +if __name__ == '__main__': + RapporterFeil().main() diff --git a/rapporter-feil.cfg b/rapporter-feil.cfg new file mode 100644 index 0000000..2c1f997 --- /dev/null +++ b/rapporter-feil.cfg @@ -0,0 +1,87 @@ +[rapporter-feil] +title = Report an Error +title[de] = Fehlerbericht erstellen +title[nb_NO] = Feilrapportering + +error_msg = An error occurred while sending the message. +error_msg[nb_NO] = En feil oppsto ved sending av beskjed. +error_msg[de] = Ein Fehler ist beim Versenden der Nachricht aufgetreten. + +confirmation_msg = Message sent. You will receive response to your email account. +confirmation_msg[de] = Nachricht wurde gesendet. Rückmeldung auf Ihre Anfrage wird an Ihr e-Mail Konto gesendet. +confirmation_msg[nb_NO] = Beskjed sendt. Videre oppfølging og svar kommer til din epostadresse. + +# SMTP server kan settes blank for å disable innebygd sending +# av mail. +[email] +smtp_server = tjener.intern +subject = Error report (school: $(cat /etc/skole)) +subject[de] = Fehlermeldung (Schule: $(cat /etc/skole)) +subject[nb_NO] = Feilrapport (skole: $(cat /etc/skole)) +from_address = no-reply@example.com +to_addresses = support@your-provider.com +#cc_addresses = ... +#bcc_addresses = ... + +[custom_fields] +Your email address (optional) = $(cat ~/.local/rf-mail 2>/dev/null) +You are allowed to take over my desktop from remote to fix the problem = 0|1 + +[custom_fields:de] +Ihre E-Mailadresse (optional) = $(cat ~/.local/rf-mail 2>/dev/null) +Ich gestatte, meine Arbeitsoverfläche fernzusteuern, um das Problem zu beheben = 0|1 + +[custom_fields:nb_NO] +Din epostadresse (valgfritt)= $(cat ~/.local/rf-mail 2>/dev/null) +Dere kan ta over skrivebordet for å løse problemet = 0|1 + +[info] +Username = $(echo $USER) +Full name = $(getent passwd $USER | cut -d: -f5) +School = $(cat /etc/skole) +Load = $(cat /proc/loadavg) +Groups = $(groups) +Host = $(hostname) + +[info:de] +Benutzername = $(echo $USER) +Vor- und Nachname = $(getent passwd $USER | cut -d: -f5) +Schule = $(cat /etc/skole) +Systemlast = $(cat /proc/loadavg) +Gruppen = $(groups) +Rechner = $(hostname) + +[info:nb_NO] +Brukernavn = $(echo $USER) +Navn = $(getent passwd $USER | cut -d: -f5) +Skole = $(cat /etc/skole) +Last = $(cat /proc/loadavg) +Grupper = $(groups) +Maskin = $(hostname) + +# Det brukeren skrev inn (pluss ekstra info) blir skrevet til stdin. +# All values from [custom fields] and [info] get exported to the environment: +# e.g. $RF_DINEPOSTADRESSE, $RF_BRUKERNAVN; however all characters not matching [a-zA-Z] +# will get removed from the variable name. +# The text before and after "=" can be any kind of text. + +[plain_callbacks] +syslog = logger -t "rapporter-feil" + +[plain_callbacks:en] +syslog = logger -t "rapporter-feil" +#local_email = mail -a "From: $USER@intern" -s "Error report (school: $RF_SCHOOL)" support@intern +save_mailaddress = echo $RF_YOUREMAILADDRESSOPTIONAL > ~/.local/rf-mail + +[plain_callbacks:de] +syslog = logger -t "rapporter-feil" +#local_email = mail -a "From: $USER@intern" -s "Fehlerbericht (Schule: $RF_SCHULE)" support@intern +save_mailaddress = echo $RF_IHREEMAILADRESSEOPTIONAL > ~/.local/rf-mail + +[plain_callbacks:nb_NO] +syslog = logger -t "rapporter-feil" +#local_email = mail -a "From: $USER@intern" -s "Feilrapport (skole: $RF_SKOLE)" feil@intern +save_mailaddress = echo $RF_DINEPOSTADRESSEVALGFRITT > ~/.local/rf-mail + +[mime_callbacks] +#send_email = /usr/lib/sendmail feil@intern |