Edgewall Software

Ticket #153: last-steps-for-ticket153-r6139.diff

File last-steps-for-ticket153-r6139.diff, 19.4 KB (added by cboos, 4 years ago)

Finish the obfuscation of e-mails in the last places that needed it.

  • trac/ticket/api.py

     
    2626from trac.util import Ranges 
    2727from trac.util.compat import set, sorted 
    2828from trac.util.datefmt import utc 
    29 from trac.util.text import shorten_line 
     29from trac.util.text import shorten_line, obfuscate_email_address 
    3030from trac.util.translation import _ 
    3131from trac.wiki import IWikiSyntaxProvider, WikiParser 
    3232 
  • trac/ticket/web_ui.py

     
    1414# 
    1515# Author: Jonas Borgström <jonas@edgewall.com> 
    1616 
     17import csv 
    1718from datetime import datetime 
    1819import os 
    1920import pkg_resources 
     
    3132from trac.resource import Resource, get_resource_url, \ 
    3233                         render_resource_link, get_resource_shortname 
    3334from trac.search import ISearchSource, search_to_sql, shorten_result 
    34 from trac.ticket import Milestone, Ticket, TicketSystem, ITicketManipulator 
    35 from trac.ticket import ITicketActionController 
     35from trac.ticket.api import TicketSystem, ITicketManipulator, \ 
     36                            ITicketActionController 
     37from trac.ticket.model import Milestone, Ticket 
    3638from trac.ticket.notification import TicketNotifyEmail 
    3739from trac.timeline.api import ITimelineEventProvider, TimelineEvent 
    3840from trac.util import get_reporter_id 
     
    5254    title = "Invalid Ticket" 
    5355 
    5456 
    55 def cc_list(cc_field): 
    56     """Split a CC: value in a list of addresses. 
    57  
    58     TODO: will become `CcField.cc_list(value) 
    59     """ 
    60     if not cc_field: 
    61         return [] 
    62     return [cc.strip() for cc in cc_field.split(',') if cc] 
    63  
    64  
    6557class TicketModule(Component): 
    6658 
    6759    implements(IContentConverter, INavigationContributor, IRequestHandler, 
     
    111103 
    112104    def convert_content(self, req, mimetype, ticket, key): 
    113105        if key == 'csv': 
    114             return self.export_csv(ticket, mimetype='text/csv') 
     106            return self.export_csv(req, ticket, mimetype='text/csv') 
    115107        elif key == 'tab': 
    116             return self.export_csv(ticket, sep='\t', 
     108            return self.export_csv(req, ticket, sep='\t', 
    117109                                   mimetype='text/tab-separated-values') 
    118110        elif key == 'rss': 
    119111            return self.export_rss(req, ticket) 
     
    705697 
    706698        return 'diff_view.html', data, None 
    707699 
    708     def export_csv(self, ticket, sep=',', mimetype='text/plain'): 
     700    def export_csv(self, req, ticket, sep=',', mimetype='text/plain'): 
     701        # FIXME: consider dumping history of changes here as well 
     702        #        as one row of output doesn't seem to be terribly useful... 
    709703        content = StringIO() 
    710         content.write(sep.join(['id'] + [f['name'] for f in 
    711                                          ticket.fields]) 
    712                       + CRLF) 
    713         content.write(sep.join([unicode(ticket.id)] + 
    714                                 [ticket.values.get(f['name'], '') 
    715                                  .replace(sep, '_').replace('\\', '\\\\') 
    716                                  .replace('\n', '\\n').replace('\r', '\\r') 
    717                                  for f in ticket.fields]) + CRLF) 
     704        writer = csv.writer(content, delimiter=sep, quoting=csv.QUOTE_MINIMAL) 
     705        writer.writerow(['id'] + [unicode(f['name']) for f in ticket.fields]) 
     706 
     707        context = Context.from_request(req, ticket.resource) 
     708        cols = [unicode(ticket.id)] 
     709        for f in ticket.fields: 
     710            name = f['name'] 
     711            value = ticket.values.get(name, '') 
     712            if name in ('cc', 'reporter'): 
     713                value = Chrome(self.env).format_emails(context, value, ' ') 
     714            cols.append(value.encode('utf-8')) 
     715        writer.writerow(cols) 
    718716        return (content.getvalue(), '%s;charset=utf-8' % mimetype) 
    719717 
    720718    def export_rss(self, req, ticket): 
     
    928926            ticket[key] = field_changes[key]['new'] 
    929927 
    930928    def _prepare_fields(self, req, ticket): 
     929        context = Context.from_request(req, ticket.resource) 
    931930        fields = [] 
    932931        for field in ticket.fields: 
    933932            name = field['name'] 
     
    963962                    field['options'] = [opt for opt in field['options'] if not 
    964963                                        Milestone(self.env, opt).is_completed] 
    965964                milestone = Resource('milestone', ticket[name]) 
    966                 context = Context.from_request(req, ticket.resource) 
    967965                field['rendered'] = render_resource_link(self.env, context, 
    968966                                                         milestone, 'compact') 
    969967            elif name == 'cc': 
    970                 all_cc = cc_list(ticket[name]) 
    971                 if not (Chrome(self.env).show_email_addresses or \ 
    972                         'EMAIL_VIEW' in req.perm(ticket.resource)): 
    973                     all_cc = [obfuscate_email_address(cc) for cc in all_cc] 
    974                 field['rendered'] = ', '.join(all_cc) 
    975                      
     968                emails = Chrome(self.env).format_emails(context, ticket[name]) 
     969                field['rendered'] = emails 
    976970            # ensure sane defaults 
    977971            field.setdefault('optional', False) 
    978972            field.setdefault('options', []) 
     
    11481142        old_list, new_list = None, None 
    11491143        sep = ', ' 
    11501144        if field == 'cc': 
    1151             old_list, new_list = cc_list(old), cc_list(new) 
     1145            chrome = Chrome(self.env) 
     1146            old_list, new_list = chrome.cc_list(old), chrome.cc_list(new) 
    11521147            if not (Chrome(self.env).show_email_addresses or  
    11531148                    'EMAIL_VIEW' in req.perm(ticket.resource)): 
    11541149                old_list = [obfuscate_email_address(cc) 
  • trac/ticket/report.py

     
    2525 
    2626from trac.core import * 
    2727from trac.db import get_column_names 
     28from trac.mimeview import Context 
    2829from trac.perm import IPermissionRequestor 
    2930from trac.resource import Resource, ResourceNotFound 
    3031from trac.util import sorted 
     
    264265        if id > 0: 
    265266            title = '{%i} %s' % (id, title) 
    266267 
     268        report_resource = Resource('report', id) 
     269        context = Context.from_request(req, report_resource) 
    267270        data = {'action': 'view', 'title': title, 
    268                 'report': Resource('report', id), 
     271                'report': {'id': id, 'resource': report_resource}, 
     272                'context': context, 
    269273                'title': title, 'description': description, 
    270274                'args': args, 'message': None} 
    271275        try: 
    272276            cols, results = self.execute_report(req, db, id, sql, args) 
     277            results = [list(row) for row in results] 
    273278        except Exception, e: 
    274279            data['message'] = _('Report execution failed: %(error)s', 
    275280                                error=to_unicode(e)) 
     
    324329            cell_groups = [] 
    325330            row = {'cell_groups': cell_groups} 
    326331            realm = 'ticket' 
     332            email_cells = [] 
    327333            for header_group in header_groups: 
    328334                cell_group = [] 
    329335                for header in header_group: 
    330336                    value = unicode(result[col_idx]) 
     337                    cell = {'value': value, 'header': header, 'index': col_idx} 
     338                    col = header['col'] 
    331339                    col_idx += 1 
    332                     cell = {'value': value, 'header': header} 
    333                     col = header['col'] 
    334340                    # Detect and create new group 
    335341                    if col == '__group__' and value != prev_group_value: 
    336342                        prev_group_value = value 
     
    344350                        row['id'] = value 
    345351                    # Special casing based on column name 
    346352                    col = col.strip('_') 
    347                     if col == 'reporter': 
    348                         cell['author'] = value 
     353                    if col in ('reporter', 'cc'): 
     354                        email_cells.append(cell) 
    349355                    elif col == 'realm': 
    350356                        realm = value 
    351357                    cell_group.append(cell) 
     
    353359            resource = Resource(realm, row.get('id')) 
    354360            if 'view' not in req.perm(resource): 
    355361                continue 
     362            if email_cells: 
     363                for cell in email_cells: 
     364                    emails = Chrome(self.env).format_emails(context(resource), 
     365                                                            cell['value']) 
     366                    result[cell['index']] = cell['value'] = emails 
    356367            row['resource'] = resource 
    357368            if row_groups: 
    358369                row_group = row_groups[-1][1] 
     
    378389            self.add_alternate_links(req, args) 
    379390 
    380391        if format == 'rss': 
     392            data['context'] = Context.from_request(req, report_resource, 
     393                                                   absurls=True) 
    381394            return 'report.rss', data, 'application/rss+xml' 
    382395        elif format == 'csv': 
    383396            filename = id and 'report_%s.csv' % id or 'report.csv' 
  • trac/ticket/query.py

     
    787787        content = StringIO() 
    788788        cols = query.get_columns() 
    789789        writer = csv.writer(content, delimiter=sep) 
     790        writer = csv.writer(content, delimiter=sep, quoting=csv.QUOTE_MINIMAL) 
    790791        writer.writerow([unicode(c).encode('utf-8') for c in cols]) 
    791792 
     793        context = Context.from_request(req) 
    792794        results = query.execute(req, self.env.get_db_cnx()) 
    793795        for result in results: 
    794             if 'TICKET_VIEW' in req.perm('ticket', result['id']): 
    795                 writer.writerow([unicode(result[col]).encode('utf-8') 
    796                                  for col in cols]) 
     796            ticket = Resource('ticket', result['id']) 
     797            if 'TICKET_VIEW' in req.perm(ticket): 
     798                values = [] 
     799                for col in cols: 
     800                    value = result[col] 
     801                    if col in ('cc', 'reporter'): 
     802                        value = Chrome(self.env).format_emails(context(ticket), 
     803                                                               value) 
     804                    values.append(unicode(value).encode('utf-8')) 
     805                writer.writerow(values) 
    797806        return (content.getvalue(), '%s;charset=utf-8' % mimetype) 
    798807 
    799808    def export_rss(self, req, query): 
     
    806815                                                   or None), 
    807816                                        row=query.rows,  
    808817                                        **query.constraints) 
    809  
    810818        data = { 
    811819            'context': Context.from_request(req, 'query', absurls=True), 
    812820            'results': results, 
  • trac/ticket/templates/report.rss

     
    3030              <title>#$row.id: $cell.value</title> 
    3131            </py:when> 
    3232            <py:when test="col == 'description'"> 
    33               <description>${unicode(wiki_to_html(row.context, cell.value, href=abs_href))}</description> 
     33              <description>${unicode(wiki_to_html(context(row.resource), cell.value))}</description> 
    3434            </py:when> 
    3535          </py:choose> 
    3636        </py:with> 
  • trac/ticket/templates/report_view.html

     
    2727      </h1> 
    2828 
    2929      <div py:if="description" id="description" xml:space="preserve"> 
    30         ${wiki_to_html(context(report), description)} 
     30        ${wiki_to_html(context, description)} 
    3131      </div> 
    3232 
    3333      <div py:if="report.id != -1" class="buttons"> 
    34         <form py:if="'REPORT_MODIFY' in perm(report)" action="" method="get"> 
     34        <form py:if="'REPORT_MODIFY' in perm(report.resource)" action="" method="get"> 
    3535          <div> 
    3636            <input type="hidden" name="action" value="edit" /> 
    3737            <input type="submit" value="Edit report" accesskey="e" /> 
    3838          </div> 
    3939        </form> 
    40         <form py:if="'REPORT_CREATE' in perm(report)" action="" method="get"> 
     40        <form py:if="'REPORT_CREATE' in perm(report.resource)" action="" method="get"> 
    4141          <div> 
    4242            <input type="hidden" name="action" value="copy" /> 
    4343            <input type="submit" value="Copy report" /> 
    4444          </div> 
    4545        </form> 
    46         <form py:if="'REPORT_DELETE' in perm(report)" action="" method="get"> 
     46        <form py:if="'REPORT_DELETE' in perm(report.resource)" action="" method="get"> 
    4747          <div> 
    4848            <input type="hidden" name="action" value="delete" /> 
    4949            <input type="submit" value="Delete report" /> 
     
    156156        </table> 
    157157      </py:for> 
    158158 
    159       <div py:if="report.id == -1 and 'REPORT_CREATE' in perm(report)" class="buttons"> 
     159      <div py:if="report.id == -1 and 'REPORT_CREATE' in perm(report.resource)" class="buttons"> 
    160160        <form action="" method="get"> 
    161161          <div> 
    162162            <input type="hidden" name="action" value="new" /> 
  • trac/ticket/templates/query_results.html

     
    5353                      <a py:when="name == 'summary'" href="$result.href" title="View ticket">$value</a> 
    5454                      <span py:when="isinstance(value, datetime)">${format_datetime(value)}</span> 
    5555                      <span py:when="name == 'reporter'">${authorinfo(value)}</span> 
     56                      <span py:when="name == 'cc'">${format_emails(ticket_context, value)}</span> 
    5657                      <span py:when="name == 'owner' and value">${authorinfo(value)}</span> 
    5758                      <span py:otherwise="">$value</span> 
    5859                    </td> 
  • trac/wiki/tests/wiki-tests.txt

     
    306306<i>RFCs von <a class="ext-link" href="ftp://ftp.rfc-editor.org/in-notes/rfcXXXX.txt"><span class="icon">ftp://ftp.rfc-editor.org/in-notes/rfcXXXX.txt</span></a></i> 
    307307</p> 
    308308------------------------------ 
     309==============================  mailto: links 
     310Author: mailto:cboos@neuf.fr,  
     311i.e. [mailto:cboos@neuf.fr me] 
     312------------------------------ 
     313<p> 
     314Author: <a class="mail-link" href="mailto:cboos@neuf.fr"><span class="icon">mailto:cboos@neuf.fr</span></a>,  
     315i.e. <a class="mail-link" href="mailto:cboos@neuf.fr"><span class="icon">me</span></a> 
     316</p> 
     317------------------------------ 
     318============================== Arbitrary protocol Link 
     319''RFCs von ftp://ftp.rfc-editor.org/in-notes/rfcXXXX.txt'' 
     320------------------------------ 
     321<p> 
     322<i>RFCs von <a class="ext-link" href="ftp://ftp.rfc-editor.org/in-notes/rfcXXXX.txt"><span class="icon">ftp://ftp.rfc-editor.org/in-notes/rfcXXXX.txt</span></a></i> 
     323</p> 
     324------------------------------ 
    309325============================== Generic InterTrac links 
    310326th:roadmap 
    311327th:roadmap: 
     
    724740 * item 2 
    725741   item 2 line 2 
    726742Paragraph 
    727 ============================== Changelog sample 
     743============================== Changelog sample (and e-mail link) 
    7287442003-09-18 23:26  Joe Bar <joeb@gloogle.gom> 
    729745 
    730746        * src/code.py: Fix problem with obsolete use of 
     
    735751Paragraph 
    736752------------------------------ 
    737753<p> 
    738 2003-09-18 23:26  Joe Bar &lt;joeb@gloogle.gom&gt; 
     7542003-09-18 23:26  Joe Bar &lt;<a class="mail-link" href="mailto:joeb@gloogle.gom"><span class="icon">joeb@gloogle.gom</span></a>&gt; 
    739755</p> 
    740756<ul><li>src/code.py: Fix problem with obsolete use of 
    741757backslash in symbols. 
  • trac/wiki/parser.py

     
    7272    # Rules provided by IWikiSyntaxProviders will be inserted here 
    7373 
    7474    _post_rules = [ 
     75        # e-mails 
     76        r"(?P<email>\w[\w.]+@\w[\w.]+\w)", 
    7577        # > ... 
    7678        r"(?P<citation>^(?P<cdepth>>(?: *>)*))", 
    7779        # &, < and > to &amp;, &lt; and &gt; 
  • trac/wiki/formatter.py

     
    284284 
    285285    # -- Post- IWikiSyntaxProvider rules 
    286286 
     287    # E-mails 
     288 
     289    def _email_formatter(self, match, fullmatch): 
     290        from trac.web.chrome import Chrome 
     291        omatch = Chrome(self.env).format_emails(self.context, match) 
     292        if omatch == match: # not obfuscated, make a link 
     293            return self._make_mail_link('mailto:'+match, match) 
     294        else: 
     295            return omatch 
     296 
    287297    # HTML escape of &, < and > 
    288298 
    289299    def _htmlescape_formatter(self, match, fullmatch): 
     
    341351        elif target.startswith('//'): 
    342352            return self._make_ext_link(ns+':'+target, label) 
    343353        elif ns == "mailto": 
    344             return self._make_mail_link('mailto:'+target, label) 
     354            from trac.web.chrome import Chrome 
     355            otarget = Chrome(self.env).format_emails(self.context, target) 
     356            olabel = Chrome(self.env).format_emails(self.context, label) 
     357            if (otarget, olabel) == (target, label): 
     358                return self._make_mail_link('mailto:'+target, label) 
     359            else: 
     360                return olabel or otarget 
    345361        else: 
    346362            return self._make_intertrac_link(ns, target, label) or \ 
    347363                   self._make_interwiki_link(ns, target, label) or \ 
  • trac/web/chrome.py

     
    535535            'authname': req and req.authname or '<trac>', 
    536536            'show_email_addresses': show_email_addresses, 
    537537            'format_author': partial(self.format_author, req), 
     538            'format_emails': self.format_emails, 
    538539 
    539540            # Date/time formatting 
    540541            'dateinfo': dateinfo, 
     
    624625            req.chrome['scripts'] = scripts 
    625626            raise 
    626627 
     628    # E-mail formatting utilities 
     629 
     630    def cc_list(self, cc_field): 
     631        """Split a CC: value in a list of addresses.""" 
     632        if not cc_field: 
     633            return [] 
     634        return [cc.strip() for cc in cc_field.split(',') if cc] 
     635 
     636    def format_emails(self, context, value, sep=', '): 
     637        """Normalize a list of e-mails and obfuscate them if needed. 
     638 
     639        :param context: the context in which the check for obfuscation should 
     640                        be done 
     641        :param value: a string containing a comma-separated list of e-mails 
     642        :param sep: the separator to use when rendering the list again 
     643        """ 
     644        all_cc = self.cc_list(value) 
     645        if not (self.show_email_addresses or 'EMAIL_VIEW' in context.perm): 
     646            all_cc = [obfuscate_email_address(cc) for cc in all_cc] 
     647        return sep.join(all_cc) 
     648     
    627649    def format_author(self, req, author): 
    628650        if self.show_email_addresses or not req or 'EMAIL_VIEW' in req.perm: 
    629651            return author