1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4"""Methods for reporting bugs."""
5
6import subprocess, sys, os
7
8__all__ = ['ReportFailure', 'BugReport', 'getReporters']
9
10#
11
12class ReportFailure(Exception):
13    """Generic exception for failures in bug reporting."""
14    def __init__(self, value):
15        self.value = value
16
17# Collect information about a bug.
18
19class BugReport:
20    def __init__(self, title, description, files):
21        self.title = title
22        self.description = description
23        self.files = files
24
25# Reporter interfaces.
26
27import os
28
29import email, mimetypes, smtplib
30from email import encoders
31from email.message import Message
32from email.mime.base import MIMEBase
33from email.mime.multipart import MIMEMultipart
34from email.mime.text import MIMEText
35
36#===------------------------------------------------------------------------===#
37# ReporterParameter
38#===------------------------------------------------------------------------===#
39
40class ReporterParameter:
41  def __init__(self, n):
42    self.name = n
43  def getName(self):
44    return self.name
45  def getValue(self,r,bugtype,getConfigOption):
46     return getConfigOption(r.getName(),self.getName())
47  def saveConfigValue(self):
48    return True
49
50class TextParameter (ReporterParameter):
51  def getHTML(self,r,bugtype,getConfigOption):
52    return """\
53<tr>
54<td class="form_clabel">%s:</td>
55<td class="form_value"><input type="text" name="%s_%s" value="%s"></td>
56</tr>"""%(self.getName(),r.getName(),self.getName(),self.getValue(r,bugtype,getConfigOption))
57
58class SelectionParameter (ReporterParameter):
59  def __init__(self, n, values):
60    ReporterParameter.__init__(self,n)
61    self.values = values
62
63  def getHTML(self,r,bugtype,getConfigOption):
64    default = self.getValue(r,bugtype,getConfigOption)
65    return """\
66<tr>
67<td class="form_clabel">%s:</td><td class="form_value"><select name="%s_%s">
68%s
69</select></td>"""%(self.getName(),r.getName(),self.getName(),'\n'.join(["""\
70<option value="%s"%s>%s</option>"""%(o[0],
71                                     o[0] == default and ' selected="selected"' or '',
72                                     o[1]) for o in self.values]))
73
74#===------------------------------------------------------------------------===#
75# Reporters
76#===------------------------------------------------------------------------===#
77
78class EmailReporter:
79    def getName(self):
80        return 'Email'
81
82    def getParameters(self):
83        return map(lambda x:TextParameter(x),['To', 'From', 'SMTP Server', 'SMTP Port'])
84
85    # Lifted from python email module examples.
86    def attachFile(self, outer, path):
87        # Guess the content type based on the file's extension.  Encoding
88        # will be ignored, although we should check for simple things like
89        # gzip'd or compressed files.
90        ctype, encoding = mimetypes.guess_type(path)
91        if ctype is None or encoding is not None:
92            # No guess could be made, or the file is encoded (compressed), so
93            # use a generic bag-of-bits type.
94            ctype = 'application/octet-stream'
95        maintype, subtype = ctype.split('/', 1)
96        if maintype == 'text':
97            fp = open(path)
98            # Note: we should handle calculating the charset
99            msg = MIMEText(fp.read(), _subtype=subtype)
100            fp.close()
101        else:
102            fp = open(path, 'rb')
103            msg = MIMEBase(maintype, subtype)
104            msg.set_payload(fp.read())
105            fp.close()
106            # Encode the payload using Base64
107            encoders.encode_base64(msg)
108        # Set the filename parameter
109        msg.add_header('Content-Disposition', 'attachment', filename=os.path.basename(path))
110        outer.attach(msg)
111
112    def fileReport(self, report, parameters):
113        mainMsg = """\
114BUG REPORT
115---
116Title: %s
117Description: %s
118"""%(report.title, report.description)
119
120        if not parameters.get('To'):
121            raise ReportFailure('No "To" address specified.')
122        if not parameters.get('From'):
123            raise ReportFailure('No "From" address specified.')
124
125        msg = MIMEMultipart()
126        msg['Subject'] = 'BUG REPORT: %s'%(report.title)
127        # FIXME: Get config parameters
128        msg['To'] = parameters.get('To')
129        msg['From'] = parameters.get('From')
130        msg.preamble = mainMsg
131
132        msg.attach(MIMEText(mainMsg, _subtype='text/plain'))
133        for file in report.files:
134            self.attachFile(msg, file)
135
136        try:
137            s = smtplib.SMTP(host=parameters.get('SMTP Server'),
138                             port=parameters.get('SMTP Port'))
139            s.sendmail(msg['From'], msg['To'], msg.as_string())
140            s.close()
141        except:
142            raise ReportFailure('Unable to send message via SMTP.')
143
144        return "Message sent!"
145
146class BugzillaReporter:
147    def getName(self):
148        return 'Bugzilla'
149
150    def getParameters(self):
151        return map(lambda x:TextParameter(x),['URL','Product'])
152
153    def fileReport(self, report, parameters):
154        raise NotImplementedError
155
156
157class RadarClassificationParameter(SelectionParameter):
158  def __init__(self):
159    SelectionParameter.__init__(self,"Classification",
160            [['1', 'Security'], ['2', 'Crash/Hang/Data Loss'],
161             ['3', 'Performance'], ['4', 'UI/Usability'],
162             ['6', 'Serious Bug'], ['7', 'Other']])
163
164  def saveConfigValue(self):
165    return False
166
167  def getValue(self,r,bugtype,getConfigOption):
168    if bugtype.find("leak") != -1:
169      return '3'
170    elif bugtype.find("dereference") != -1:
171      return '2'
172    elif bugtype.find("missing ivar release") != -1:
173      return '3'
174    else:
175      return '7'
176
177class RadarReporter:
178    @staticmethod
179    def isAvailable():
180        # FIXME: Find this .scpt better
181        path = os.path.join(os.path.dirname(__file__),'../share/scan-view/GetRadarVersion.scpt')
182        try:
183          p = subprocess.Popen(['osascript',path],
184          stdout=subprocess.PIPE, stderr=subprocess.PIPE)
185        except:
186            return False
187        data,err = p.communicate()
188        res = p.wait()
189        # FIXME: Check version? Check for no errors?
190        return res == 0
191
192    def getName(self):
193        return 'Radar'
194
195    def getParameters(self):
196        return [ TextParameter('Component'), TextParameter('Component Version'),
197                 RadarClassificationParameter() ]
198
199    def fileReport(self, report, parameters):
200        component = parameters.get('Component', '')
201        componentVersion = parameters.get('Component Version', '')
202        classification = parameters.get('Classification', '')
203        personID = ""
204        diagnosis = ""
205        config = ""
206
207        if not component.strip():
208            component = 'Bugs found by clang Analyzer'
209        if not componentVersion.strip():
210            componentVersion = 'X'
211
212        script = os.path.join(os.path.dirname(__file__),'../share/scan-view/FileRadar.scpt')
213        args = ['osascript', script, component, componentVersion, classification, personID, report.title,
214                report.description, diagnosis, config] + map(os.path.abspath, report.files)
215#        print >>sys.stderr, args
216        try:
217          p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
218        except:
219            raise ReportFailure("Unable to file radar (AppleScript failure).")
220        data, err = p.communicate()
221        res = p.wait()
222
223        if res:
224            raise ReportFailure("Unable to file radar (AppleScript failure).")
225
226        try:
227            values = eval(data)
228        except:
229            raise ReportFailure("Unable to process radar results.")
230
231        # We expect (int: bugID, str: message)
232        if len(values) != 2 or not isinstance(values[0], int):
233            raise ReportFailure("Unable to process radar results.")
234
235        bugID,message = values
236        bugID = int(bugID)
237
238        if not bugID:
239            raise ReportFailure(message)
240
241        return "Filed: <a href=\"rdar://%d/\">%d</a>"%(bugID,bugID)
242
243###
244
245def getReporters():
246    reporters = []
247    if RadarReporter.isAvailable():
248        reporters.append(RadarReporter())
249    reporters.append(EmailReporter())
250    return reporters
251
252