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