Ticket #3332: mimeview_refactoring-r3507.diff
| File mimeview_refactoring-r3507.diff, 82.1 KB (added by cboos, 6 years ago) |
|---|
-
trac/attachment.py
26 26 from trac.config import BoolOption, IntOption 27 27 from trac.core import * 28 28 from trac.env import IEnvironmentSetupParticipant 29 from trac.mimeview import *29 from trac.mimeview.api import Mimeview, MimeType, FileMimeContent 30 30 from trac.util import get_reporter_id, create_unique_file 31 31 from trac.util.datefmt import format_datetime, pretty_timedelta 32 32 from trac.util.markup import Markup, html … … 124 124 def parent_href(self, req): 125 125 return req.href(self.parent_type, self.parent_id) 126 126 127 def _get_title(self): 127 def _get_title(self): # TODO: should extend to other `parent_type`s 128 128 return '%s%s: %s' % (self.parent_type == 'ticket' and '#' or '', 129 129 self.parent_id, self.filename) 130 130 title = property(_get_title) … … 227 227 228 228 select = classmethod(select) 229 229 230 def open(self): 230 def open(self): # deprecate? 231 231 self.env.log.debug('Trying to open attachment at %s', self.path) 232 232 try: 233 233 fd = open(self.path, 'rb') … … 505 505 'author': get_reporter_id(req)} 506 506 507 507 def _render_view(self, req, attachment): 508 # FIXME: perm_map should extend to other `parent_type`s 508 509 perm_map = {'ticket': 'TICKET_VIEW', 'wiki': 'WIKI_VIEW'} 509 510 req.perm.assert_permission(perm_map[attachment.parent_type]) 510 511 … … 522 523 if req.perm.has_permission(perm_map[attachment.parent_type]): 523 524 req.hdf['attachment.can_delete'] = 1 524 525 525 fd = attachment.open() 526 try: 527 mimeview = Mimeview(self.env) 526 format = req.args.get('format') 527 raw_href = attachment.href(req, format='raw') 528 528 529 # MIME type detection 530 str_data = fd.read(1000) 531 fd.seek(0) 532 533 binary = is_binary(str_data) 534 mime_type = mimeview.get_mimetype(attachment.filename, str_data) 529 mimecontent = FileMimeContent(self.env, attachment.path, raw_href, 530 'Attachment') 531 can_render_as_txt = self.render_unsafe_content and \ 532 not mimecontent.is_binary 535 533 536 # Eventually send the file directly 537 format = req.args.get('format') 538 if format in ('raw', 'txt'): 539 if not self.render_unsafe_content and not binary: 540 # Force browser to download HTML/SVG/etc pages that may 541 # contain malicious code enabling XSS attacks 542 req.send_header('Content-Disposition', 'attachment;' + 543 'filename=' + attachment.filename) 544 if not mime_type or (self.render_unsafe_content and \ 545 not binary and format == 'txt'): 546 mime_type = 'text/plain' 547 if 'charset=' not in mime_type: 548 charset = mimeview.get_charset(str_data, mime_type) 549 mime_type = mime_type + '; charset=' + charset 550 req.send_file(attachment.path, mime_type) 534 # Eventually send the file directly 535 if format in ('raw', 'txt'): 536 if not self.render_unsafe_content and not mimecontent.is_binary: 537 # Force browser to download HTML/SVG/etc pages that may 538 # contain malicious code enabling XSS attacks 539 req.send_header('Content-Disposition', 540 'attachment;filename=%s' % attachment.filename) 541 if not mimecontent.type.is_known or \ 542 (format == 'txt' and can_render_as_txt): 543 # Force the content to be displayed as text 544 type = MimeType('text/plain', mimecontent.encoding) 545 else: 546 type = mimecontent.type 547 req.send_file(attachment.path, type.mimetype_charset) 551 548 552 # add ''Plain Text'' alternate link if needed 553 if self.render_unsafe_content and not binary and \ 554 mime_type and not mime_type.startswith('text/plain'): 555 plaintext_href = attachment.href(req, format='txt') 556 add_link(req, 'alternate', plaintext_href, 'Plain Text', 557 mime_type) 549 mimetype = mimecontent.type.mimetype 558 550 559 # add ''Original Format'' alternate link (always) 560 raw_href = attachment.href(req, format='raw') 561 add_link(req, 'alternate', raw_href, 'Original Format', mime_type) 551 # add ''Plain Text'' alternate link if needed 552 if can_render_as_txt and mimecontent.type.is_known and \ 553 mimetype != 'text/plain': 554 add_link(req, 'alternate', attachment.href(req, format='txt'), 555 'Plain Text', 'text/plain') 562 556 563 self.log.debug("Rendering preview of file %s with mime-type %s"564 % (attachment.filename, mime_type))557 # add ''Original Format'' alternate link (always) 558 add_link(req, 'alternate', raw_href, 'Original Format', mimetype) 565 559 566 req.hdf['attachment'] = mimeview.preview_to_hdf( 567 req, fd, os.fstat(fd.fileno()).st_size, mime_type, 568 attachment.filename, raw_href, annotations=['lineno']) 569 finally: 570 fd.close() 560 self.log.debug("Rendering preview of file %s with mime-type %s" % \ 561 (attachment.filename, mimetype)) 571 562 563 req.hdf['attachment'] = Mimeview(self.env).preview_to_hdf( 564 req, mimecontent, annotations=['lineno']) 565 572 566 def _render_list(self, req, p_type, p_id): 573 567 self._parent_to_hdf(req, p_type, p_id) 574 568 req.hdf['attachment'] = { -
trac/mimeview/rst.py
28 28 import re 29 29 30 30 from trac.core import * 31 from trac.mimeview.api import IHTMLPreviewRenderer , content_to_unicode31 from trac.mimeview.api import IHTMLPreviewRenderer 32 32 from trac.web.href import Href 33 33 from trac.wiki.formatter import WikiProcessor 34 34 from trac.wiki import WikiSystem … … 223 223 224 224 _inliner = rst.states.Inliner() 225 225 _parser = rst.Parser(inliner=_inliner) 226 content = content_to_unicode(self.env, content, mimetype)226 content = unicode(content) 227 227 content = content.encode('utf-8') 228 228 html = publish_string(content, writer_name='html', parser=_parser, 229 229 settings_overrides={'halt_level': 6}) -
trac/mimeview/api.py
19 19 # Christian Boos <cboos@neuf.fr> 20 20 21 21 """ 22 The `trac.mimeview` module centralize the intelligence related to23 file metadata, principally concerning the `type` (MIME type) of the content24 and, if relevant, concerning the text encoding (charset) used by the content.22 The `trac.mimeview` module centralizes the intelligence related to file 23 metadata, principally concerning the `type` (MIME type) and the text 24 encoding (charset) used by the content, if the latter one is relevant. 25 25 26 26 There are primarily two approaches for getting the MIME type of a given file: 27 27 * taking advantage of existing conventions for the file name 28 28 * examining the file content and applying various heuristics 29 29 30 The module also knows how to convert the file content from one type31 to another type.30 The module also knows about conversions from one data type to another type, 31 like conversions to text/html (this is no more a special case). 32 32 33 In some cases, only the `url` pointing to the file's content is actually 34 needed, that's why we avoid to read the file's content when it's not needed. 35 36 The actual `content` to be converted might be a `unicode` object, 37 but it can also be the raw byte string (`str`) object, or simply 38 an object that can be `read()`. 33 In order to keep the API simple, we deal with a few classes, 34 each encapsulating a part of the knowledge related to content. 35 * the `MimeType`, for storing the mime type string, the charset, 36 but also eventually the name and the commonly used file extension 37 for that type 38 * the `MimeContent` and `FileMimeContent`, which provide a flexible 39 API for handling string and file content, respectively. 40 Those classes inherit from the abstract `MimeContentBase`, which 41 can be used to make new wrapper classes for any kind of content. 42 * the `Conversion` class, which is use to specify conversion from 43 one `MimeType` to another 39 44 """ 40 45 46 import os 41 47 import re 42 48 from StringIO import StringIO 43 49 44 50 from trac.config import IntOption, ListOption, Option 45 51 from trac.core import * 46 52 from trac.util import sorted 47 from trac.util.text import to_utf8, to_unicode 53 from trac.util.text import to_utf8, to_unicode, IDENTITY_CHARSET 48 54 from trac.util.markup import escape, Markup, Fragment, html 49 55 50 56 51 __all__ = ['get_mimetype', 'is_binary', 'detect_unicode', 'Mimeview', 52 'content_to_unicode'] 57 __all__ = ['get_mimetype', 'is_binary', 'detect_unicode', 58 'Mimeview', 'MimeType', 'Conversion', 59 'MimeContentBase', 'MimeContent', 'FileMimeContent', 60 'TEXT_PLAIN', 'TEXT_HTML', 'APPLICATION_OCTET_STREAM'] 53 61 54 62 55 63 # Some common MIME types and their associated keywords and/or file extensions 56 64 65 APPLICATION_OCTET_STREAM_STR = 'application/octet-stream' 66 57 67 KNOWN_MIME_TYPES = { 58 68 'application/pdf': ['pdf'], 59 69 'application/postscript': ['ps'], … … 114 124 for e in exts: 115 125 MIME_MAP[e] = t 116 126 117 # Simple builtin autodetection from the content using a regexp118 MODE_RE = re.compile(119 r"#!(?:[/\w.-_]+/)?(\w+)|" # look for shebang120 r"-\*-\s*(?:mode:\s*)?([\w+-]+)\s*-\*-|" # look for Emacs' -*- mode -*-121 r"vim:.*?syntax=(\w+)" # look for VIM's syntax=<n>122 )123 127 124 def get_mimetype(filename, content=None, mime_map=MIME_MAP): 125 """Guess the most probable MIME type of a file with the given name. 128 # -- a few functions for dealing with MIME types / binary / text content 129 # in a simple way (get_mimetype, is_binary, detect_unicode) 126 130 131 def get_mimetype_from_filename(filename, mime_map=MIME_MAP): 132 """Guess the most probable MIME type of file with the given `filename`. 133 127 134 `filename` is either a filename (the lookup will then use the suffix) 128 135 or some arbitrary keyword. 129 130 `content` is either a `str` or an `unicode` string. 136 `mime_map` maps keywords to MIME types. 137 138 Return the MIME type as a string, or `None` if not detected. 131 139 """ 132 140 suffix = filename.split('.')[-1] 133 141 if suffix in mime_map: … … 141 149 mimetype = mimetypes.guess_type(filename)[0] 142 150 except: 143 151 pass 144 if not mimetype and content:145 match = re.search(MODE_RE, content[:1000])146 if match:147 mode = match.group(1) or match.group(3) or \148 match.group(2).lower()149 if mode in mime_map:150 # 3) mimetype from the content, using the `MODE_RE`151 return mime_map[mode]152 else:153 if is_binary(content):154 # 4) mimetype from the content, using`is_binary`155 return 'application/octet-stream'156 152 return mimetype 157 153 154 # Simple builtin autodetection from the content using a regexp 155 MODE_RE = re.compile( 156 r"#!(?:[/\w.-_]+/)?(\w+)|" # look for shebang 157 r"-\*-\s*(?:mode:\s*)?([\w+-]+)\s*-\*-|" # look for Emacs' -*- mode -*- 158 r"vim:.*?syntax=(\w+)" # look for VIM's syntax=<n> 159 ) 160 161 def get_mimetype_from_content(content): 162 """Guess the most probable MIME type of file with the given `filename`. 163 164 `content` is either a `str` or an `unicode` string 165 166 Return the MIME type as a string, or `None` if not detected. 167 """ 168 match = re.search(MODE_RE, content[:1000]) 169 if match: 170 mode = match.group(1) or match.group(3) or \ 171 match.group(2).lower() 172 if mode in mime_map: 173 # 3) mimetype from the content, using the `MODE_RE` 174 return mime_map[mode] 175 else: 176 if is_binary(content): 177 # 4) mimetype from the content, using `is_binary` 178 return APPLICATION_OCTET_STREAM_STR 179 180 def get_mimetype(filename, content=None, mime_map=MIME_MAP): 181 """Auto-detect MIME type either from the `filename` or from the `content`. 182 """ 183 mimetype = get_mimetype_from_filename(filename, mime_map) 184 if not mimetype and content: 185 mimetype = get_mimetype_from_content(content) 186 return mimetype 187 158 188 def is_binary(data): 159 189 """Detect binary content by checking the first thousand bytes for zeroes. 160 190 … … 165 195 return '\0' in data[:1000] 166 196 167 197 def detect_unicode(data): 168 """Detect different unicode charsets by looking for B OMs (Byte Order Marks).198 """Detect different unicode charsets by looking for Byte Order Marks. 169 199 170 200 Operate obviously only on `str` objects. 171 201 """ … … 178 208 else: 179 209 return None 180 210 181 def content_to_unicode(env, content, mimetype):182 """Retrieve an `unicode` object from a `content` to be previewed"""183 mimeview = Mimeview(env)184 if hasattr(content, 'read'):185 content = content.read(mimeview.max_preview_size)186 return mimeview.to_unicode(content, mimetype)187 211 212 # -- Classes for mimetype, content and conversion 188 213 214 class MimeType(object): 215 """Representation of a MIME-Type. 216 217 If the MIME type correspond to text content, the object can also 218 store a `charset` information. 219 220 A MIME type has a `name` and has an `extension` 221 that can be used for storing the converted data in a file. 222 223 All the properties of this class are read-only. 224 """ 225 226 def __init__(self, mimetype, charset=None, name=None, extension=None): 227 """The `mimetype` string can eventually embed the `charset`.""" 228 self._mimetype = mimetype 229 # determine charset 230 self._charset = charset 231 if not self._charset and self._mimetype: 232 sep_idx = mimetype.find(';') 233 if sep_idx >= 0: 234 self._mimetype = mimetype[:sep_idx].strip() 235 charset_idx = mimetype.find('charset=', sep_idx) 236 if charset_idx >= 0: 237 self._charset = mimetype[charset_idx+8:].strip() 238 self._extension = extension 239 self._name = name 240 241 def __repr__(self): 242 return self.mimetype_charset 243 244 def _get_extension(self): 245 if not self._extension: 246 self._extension = KNOWN_MIME_TYPES.get(self.mimetype) 247 if not self._extension: 248 detail = self.mimetype.split('/', 1)[1] 249 if detail.startswith('x-'): 250 self._extension = detail[2:] 251 return self._extension 252 253 def _get_mimetype_charset(self): 254 """Combine the MIME type and charset information in a single string. 255 """ 256 if self._mimetype and self._charset: 257 return '%s; charset=%s' % (self.mimetype, self.charset) 258 else: 259 return self.mimetype 260 261 is_known = property(lambda x: x._mimetype is not None) 262 mimetype = property(lambda x: x._mimetype or APPLICATION_OCTET_STREAM_STR, 263 doc="MIME Type string (without charset information)") 264 charset = property(lambda x: x._charset, 265 doc="Eventual charset information") 266 name = property(lambda x: x._name or x._extension) 267 extension = property(_get_extension) 268 mimetype_charset = property(_get_mimetype_charset) 269 270 def match(self, other, regexp=False): 271 """Compare MIME type string only. 272 273 If `regexp` is set, `self.mimetype` is used as a regexp. 274 """ 275 if not isinstance(other, MimeType): 276 return False 277 if regexp: 278 return re.match(self.mimetype, other.mimetype) 279 else: 280 return self.mimetype == other.mimetype 281 282 283 TEXT_PLAIN = MimeType('text/plain', 'utf-8', 'Plain Text', 'txt') 284 TEXT_HTML = MimeType('text/html', 'utf-8', 'HTML', 'html') 285 APPLICATION_OCTET_STREAM = MimeType(APPLICATION_OCTET_STREAM_STR, 286 IDENTITY_CHARSET, 287 'Undefined (binary)', 'bin') 288 289 290 class MimeContentBase(object): 291 """An abstract MIME content, with an associated MimeType. 292 293 Such an object has means to auto-detect both the MIME Type and 294 the `encoding` of its content. 295 296 That `encoding` is more reliable than the `type.charset` information. 297 There are additional consistency checks that are performed, and it 298 can be `None` if the content is an `unicode` object. 299 300 The content itself can be accessed in various ways: through 301 the iterator protocol, the len() and unicode() operators... 302 """ 303 304 def __init__(self, env, mimetype=None, filename=None, url=None): 305 """ 306 `mimetype` can be specified as a `MimeType` object, 307 or as string, which will then be a hint about the content. 308 309 If the mimetype is not specified or equal to 310 "application/octet-stream", then it will be auto-detected when needed. 311 312 In case auto-detection fails, APPLICATION_OCTET_STREAM will be the 313 corresponding MIME type. 314 315 The `filename` is simply a suggested basename for that content. 316 317 The `url` is a link for retrieving the raw content directly 318 from the server. This can be useful for converters that can 319 provide links to objects, instead of having to expand the 320 content inline. 321 """ 322 self.env = env 323 if isinstance(mimetype, basestring): 324 mimetype = MimeType(mimetype) 325 self._type = mimetype 326 self._filename = filename 327 self._url = url 328 self._binary = None 329 self._encoding = False 330 331 def __repr__(self): 332 return '<%s %s "%s">' % (self.__class__.__name__, self._type, 333 self._filename or self._url) 334 335 def __unicode__(self): 336 """Return the `unicode` object corresponding to the content.""" 337 return to_unicode(self.content, self.encoding) 338 # Note: this does the right thing if the content is already `unicode` 339 340 341 def _is_binary(self): 342 """An heuristic to guess whether the content is binary data or not. 343 344 This will trigger the retrieval of an `excerpt` of the content. 345 """ 346 if self._binary is None: 347 mimetype = self.type.mimetype 348 self._binary = is_binary(self.excerpt) or \ 349 (self.type.is_known and 350 mimetype == APPLICATION_OCTET_STREAM_STR) or \ 351 mimetype in Mimeview(self.env).treat_as_binary 352 return self._binary 353 354 def _get_type(self): 355 """Get or determine the MimeType corresponding to this content. 356 357 An `excerpt` of the content will be examined if needed. 358 """ 359 if self._type is None: # not set 360 mimetype = None 361 if self.filename: 362 mimemap = Mimeview(self.env).mimemap 363 mimetype = get_mimetype_from_filename(self.filename, mimemap) 364 if not mimetype: 365 mimetype = get_mimetype_from_content(self.excerpt) 366 if not mimetype: 367 pass # TODO 0.11: go through IMimeTypeDetectors 368 self._type = MimeType(mimetype) 369 return self._type 370 371 def _set_type(self, type): 372 """Simply replace the existing `type` by the given `MimeType` object. 373 374 If `None` is given, this will force auto-detection the next time 375 `type` will be accessed. 376 377 Can be used for in-place conversion (e.g. ''any'' to text/plain). 378 """ 379 self._type = type 380 381 def _get_encoding(self): 382 """Get or determine the current encoding of that `content`. 383 384 The encoding will be determined using this order: 385 * from the charset information present in the mimetype information 386 * auto-detection of the charset from the `content` 387 * if nothing else worked, use the configured `default_charset` 388 389 If the `content` happens to be a genuine `unicode` object, then 390 this returns `None`. 391 If the `content` is binary, then the encoding will be the identity 392 charset (ISO Latin 1). 393 """ 394 if self._encoding is False: 395 charset = self.type.charset 396 if charset: 397 self._encoding = charset 398 elif isinstance(self.excerpt, str): 399 utf_encoding = detect_unicode(self.excerpt) 400 if utf_encoding is not None: 401 self._encoding = utf_encoding 402 elif self.is_binary: 403 self._encoding = IDENTITY_CHARSET 404 elif isinstance(self.excerpt, unicode): 405 self._encoding = None 406 if self._encoding is False: 407 pass # TODO 0.11: go through ICharsetDetectors here 408 if self._encoding is False: 409 self._encoding = Mimeview(self.env).default_charset 410 return self._encoding 411 412 def _get_content(self): 413 """Retrieve all the content. 414 415 Default implementation based on iterator. If the iterator itself 416 is implemented based on the content... reimplement this one! 417 """ 418 return "".join(self.__iter__()) 419 420 def read(self): # TODO: remove in 0.11 421 return self.content # (compatibility with IHTMLPreviewRenderer) 422 423 # Methods that need to be reimplemented by subclasses: 424 425 def __iter__(self): 426 """Iterate on chunks of raw content.""" 427 raise NotImplementedError 428 429 def __len__(self): 430 """Length of the raw content, in bytes.""" 431 raise NotImplementedError 432 433 def _get_excerpt(self, len=1000): 434 """Extracts the first `len` characters from the content.""" 435 raise NotImplementedError 436 437 type = property(fget=lambda x: x._get_type(), 438 fset=lambda x, y: x._set_type(y)) 439 is_binary = property(lambda x: x._is_binary()) 440 encoding = property(lambda x: x._get_encoding()) 441 excerpt = property(lambda x: x._get_excerpt()) 442 content = property(lambda x: x._get_content()) 443 filename = property(lambda x: x._filename) 444 url = property(lambda x: x._url) 445 446 447 class MimeContent(MimeContentBase): 448 """MIME-typed content wrapper for a basestring.""" 449 450 def __init__(self, env, content, mimetype, filename='file', url=None): 451 MimeContentBase.__init__(self, env, mimetype, filename, url) 452 self._content = content 453 454 # Reimplemented methods 455 456 def _get_content(self): 457 """Retrieve the wrapped content. 458 459 Note: therefore this *might* be an `unicode` object. 460 Remember that in this case, `encoding` will be `None`. 461 """ 462 return self._content 463 464 def __iter__(self): 465 """Iterate on chunks of content. 466 467 If the content `is_binary` property is `False`, those chunks will 468 be lines, with the line endings kept. 469 """ 470 if self.is_binary: 471 buf = StringIO(self.content) 472 chunk = buf.read(1000) 473 while chunk: 474 yield chunk 475 chunk = buf.read(1000) 476 else: 477 for line in self.content.splitlines(True): 478 yield line 479 480 def __len__(self): 481 """Length of the content, in characters.""" 482 return len(self.content) 483 484 def _get_excerpt(self, len=1000): 485 """Extracts the first `len` characters from the content.""" 486 return self._content[:len] 487 488 489 class FileMimeContent(MimeContentBase): 490 """MIME-typed content wrapper for a file.""" 491 492 def __init__(self, env, path, url=None, kind='File', mimetype=None): 493 self._fd = None 494 self._path = path 495 self._kind = kind 496 self._excerpt = None 497 MimeContentBase.__init__(self, env, mimetype, os.path.basename(path), 498 url) 499 def __del__(self): 500 if self._fd: 501 self._fd.close() 502 503 def _ensure_open(self): 504 if not self._fd: 505 try: 506 self._fd = open(self._path) 507 except IOError: 508 raise TracError('%s "%s" not found' % (self._kind, 509 self._filename)) 510 # Reimplemented methods 511 512 def __iter__(self): 513 """Iterate on chunks of raw content.""" 514 chunk = self.excerpt 515 while chunk: 516 yield chunk 517 chunk = self._fd.read(1000) 518 519 def __len__(self): 520 """Length of the raw content, in bytes.""" 521 if self._fd: 522 stat = os.fstat(self._fd.fileno()) 523 else: 524 stat = os.stat(self._path) 525 return stat.st_size 526 527 def _get_excerpt(self, len=1000): 528 """Extracts the `len` first bytes from the content.""" 529 if self._excerpt is None: 530 self._ensure_open() 531 self._excerpt = self._fd.read(1000) 532 return self._excerpt 533 534 535 class NoConversion(TracError): 536 def __init__(self, msg, from_, to): 537 self.msg = msg 538 self.from_ = from_ 539 self.to = to 540 541 def message(self): 542 return '%s, from %s to %s' % \ 543 (self.msg, self.from_.mimetype, self.to.mimetype) 544 545 class Conversion(object): 546 """A specification for performing a data conversion. 547 548 Each conversion is identified by a `key` and targets an output `mimetype`. 549 550 A conversion also specifies a `quality` ranking, which is a number 551 in the range 0 to 9, where 0 means no support and 9 means "perfect" 552 support (try to keep 9 available for user defined conversions, 553 though nothing will prevent them from using 10 or 100...) 554 555 Finally, `expand_tabs` indicates whether a tab expansion should precede 556 the conversion attempt. 557 558 e.g. Conversion(key='latex', quality=8, mimetype=MimeType('text/x-tex')) 559 """ 560 561 def __init__(self, key, quality=1, mimetype=TEXT_HTML, expand_tabs=False): 562 self.key = key 563 self.quality = quality 564 self.mimetype = mimetype 565 self.expand_tabs = expand_tabs 566 567 def __repr__(self): 568 return '<Conversion "%s" to %s>' % (self.key, self.mimetype) 569 570 571 # -- Deprecated (TODO: remove in 0.11) 572 189 573 class IHTMLPreviewRenderer(Interface): 190 574 """Extension point interface for components that add HTML renderers of 191 575 specific content types to the `Mimeview` component. 192 576 193 (Deprecated)577 Deprecated in 0.10. Implement `IContentConverter` instead. 194 578 """ 195 579 196 580 # implementing classes should set this property to True if they … … 198 582 expand_tabs = False 199 583 200 584 def get_quality_ratio(mimetype): 201 """Return the level of support this renderer provides for the `content` 202 of the specified MIME type. The return value must be a number between 203 0 and 9, where 0 means no support and 9 means "perfect" support. 204 """ 585 """Return the level of support this renderer provides""" 205 586 206 587 def render(req, mimetype, content, filename=None, url=None): 207 """Render an XHTML preview of the raw `content`. 588 """Render an XHTML preview of the raw `content`.""" 208 589 209 The `content` might be:210 * a `str` object211 * an `unicode` string212 * any object with a `read` method, returning one of the above213 590 214 It is assumed that the content will correspond to the given `mimetype`. 591 # -- Interfaces for the extension points 215 592 216 Besides the `content` value, the same content may eventually 217 be available through the `filename` or `url` parameters. 218 This is useful for renderers that embed objects, using <object> or 219 <img> instead of including the content inline. 220 221 Can return the generated XHTML text as a single string or as an 222 iterable that yields strings. In the latter case, the list will 223 be considered to correspond to lines of text in the original content. 593 class IContentConverter(Interface): 594 """An extension point interface for generic content conversion. 595 """ 596 597 def get_supported_conversions(mimetype): 598 """Check if conversion of `mimetype` is supported by this converter. 599 600 Return an iterable of `Conversion` objects for which this is 601 the case. 224 602 """ 225 603 604 def convert_content(context, conversion, content): 605 """Convert the given `content` using the specified `conversion`. 606 607 The conversion takes place in the given formatting `context`. 608 609 A `context` can provide at least a `req` property. 610 611 Return the converted content as a new `MimeContent` object. 612 """ 613 226 614 class IHTMLPreviewAnnotator(Interface): 227 615 """Extension point interface for components that can annotate an XHTML 228 616 representation of file contents with additional information.""" … … 240 628 annotation data.""" 241 629 242 630 243 class IContentConverter(Interface): 244 """An extension point interface for generic MIME based content 245 conversion.""" 631 # -- The main Mimeview component 246 632 247 def get_supported_conversions():248 """Return an iterable of tuples in the form (key, name, extension,249 in_mimetype, out_mimetype, quality) representing the MIME conversions250 supported and251 the quality ratio of the conversion in the range 0 to 9, where 0 means252 no support and 9 means "perfect" support. eg. ('latex', 'LaTeX', 'tex',253 'text/x-trac-wiki', 'text/plain', 8)"""254 255 def convert_content(req, mimetype, content, key):256 """Convert the given content from mimetype to the output MIME type257 represented by key. Returns a tuple in the form (content,258 output_mime_type) or None if conversion is not possible."""259 260 261 633 class Mimeview(Component): 262 634 """A generic class to prettify data, typically source code.""" 263 635 264 renderers = ExtensionPoint(IHTMLPreviewRenderer) 636 renderers = ExtensionPoint(IHTMLPreviewRenderer) # TODO: remove in 0.11 265 637 annotators = ExtensionPoint(IHTMLPreviewAnnotator) 266 638 converters = ExtensionPoint(IContentConverter) 267 639 … … 275 647 """Maximum file size for HTML preview. (''since 0.9'').""") 276 648 277 649 mime_map = ListOption('mimeviewer', 'mime_map', 278 'text/x-dylan:dylan,text/x-idl:ice,text/x-ada:ads:adb', 650 'text/x-dylan:dylan,text/x-idl:ice,text/x-ada:ads:adb', doc= 279 651 """List of additional MIME types and keyword mappings. 280 Mappings are comma-separated, and for each MIME type,281 there's a colon (":") separated list of associated keywords282 or file extensions. (''since 0.10'').""")283 652 653 Mappings are comma-separated. Each mapping starts with the mimetype, 654 followed by a colon (":") and the (colon separated) list of associated 655 keywords or file extensions. (''since 0.10'').""") 656 657 treat_as_binary = ListOption('mimeviewer', 'treat_as_binary', 658 'application/pdf,application/postscript,application/rtf', doc= 659 """List of MIME types that should always be treated as binary content. 660 661 Accounts for the fact that our binary detection heuristic can't 662 always work for some kind of binary data. (''since 0.10'').""") 663 284 664 def __init__(self): 285 665 self._mime_map = None 286 287 # Public API288 666 289 def get_supported_conversions(self, mimetype): 290 """Return a list of target MIME types in same form as 291 `IContentConverter.get_supported_conversions()`, but with the converter 292 component appended. Output is ordered from best to worst quality.""" 667 def _get_mimemap(self): 668 """Extend default extension to MIME type mappings""" 669 if not self._mime_map: 670 self._mime_map = {} 671 self._mime_map.update(MIME_MAP) 672 for mapping in self.config['mimeviewer'].getlist('mime_map'): 673 if ':' in mapping: 674 assocations = mapping.split(':') 675 mimetype = assocations[0] 676 for keyword in assocations: # mimetype->mimetype on purpose 677 self._mime_map[keyword] = mimetype 678 return self._mime_map 679 680 mimemap = property(_get_mimemap) 681 682 def lookup(self, keyword, charset=None): 683 mimetype = self.mimemap.get(keyword, APPLICATION_OCTET_STREAM_STR) 684 return MimeType(mimetype, charset, extension=keyword) 685 686 # -- MIME type conversion 687 688 def get_conversions(self, input): 689 """Return a list of possible conversions for the `input` MimeType. 690 691 The returned list contains pair of (conversion, converter) objects, 692 ordered from best to worst quality. 693 """ 694 # Build list of possible conversions 293 695 converters = [] 294 696 for converter in self.converters: 295 for k, n, e, im, om, q in converter.get_supported_conversions(): 296 if im == mimetype and q > 0: 297 converters.append((k, n, e, im, om, q, converter)) 298 converters = sorted(converters, key=lambda i: i[-1], reverse=True) 299 return converters 697 for conversion in converter.get_supported_conversions(input): 698 if conversion.quality > 0: 699 converters.append((conversion, converter)) 300 700 301 def convert_content(self, req, mimetype, content, key, filename=None, 302 url=None): 303 """Convert the given content to the target MIME type represented by 304 `key`, which can be either a MIME type or a key. Returns a tuple of 305 (content, output_mime_type, extension).""" 306 if not content: 307 return ('', 'text/plain;charset=utf-8') 701 # ---- Backward compatibility support for IHTMLPreviewRenderer 702 class IHTMLPreviewRendererWrapper(object): 703 def __init__(self, renderer): 704 self.renderer = renderer 705 def __repr__(self): 706 return repr(self.renderer) 707 def convert_content(self, context, conversion, mimecontent): 708 return self.renderer.render( 709 context.req, mimecontent.type.mimetype, 710 mimecontent, # which is read()able 711 mimecontent.filename, mimecontent.url) 712 for renderer in self.renderers: 713 qr = renderer.get_quality_ratio(input.mimetype) 714 if qr > 0: 715 expand_tabs = getattr(renderer, 'expand_tabs', False) 716 converters.append( 717 (Conversion(key='', quality=qr, mimetype=TEXT_HTML, 718 expand_tabs=expand_tabs), 719 IHTMLPreviewRendererWrapper(renderer))) 720 # ---- (to be removed in 0.11) 308 721 309 # Ensure we have a MIME type for this content 310 full_mimetype = mimetype 311 if not full_mimetype: 312 if hasattr(content, 'read'): 313 content = content.read(self.max_preview_size) 314 full_mimetype = self.get_mimetype(filename, content) 315 if full_mimetype: 316 mimetype = full_mimetype.split(';')[0].strip() # split off charset 317 else: 318 mimetype = full_mimetype = 'text/plain' # fallback if not binary 722 return sorted(converters, key=lambda c: c[0].quality, reverse=True) 319 723 320 # Choose best converter 321 candidates = list(self.get_supported_conversions(mimetype)) 322 candidates = [c for c in candidates if key in (c[0], c[4])] 724 def convert(self, context, selector, mimecontent): 725 """Convert the `mimecontent` to another MIME type. 726 727 The conversion to be done is determined by `selector`, 728 which can be either directly the desired output MIME type or 729 a key identifying the Conversion object. 730 731 Returns a new MimeContent. 732 """ 733 # Ensure we have the mimetype and the charset information 734 output = self.lookup(selector) 735 if not output: 736 self.log.debug('No output MIME type selected by "%s"' % selector) 737 738 # Get all available conversions for the input 739 candidates = self.get_conversions(mimecontent.type) 740 # Filter those which are matching `selector` 741 candidates = [cn_cr for cn_cr in candidates 742 if selector in (cn_cr[0].key, output.mimetype)] 323 743 if not candidates: 324 raise TracError('No available MIME conversions from %s to %s' %325 (mimetype, key))744 raise NoConversion('No available MIME conversions', 745 mimecontent.type, output) 326 746 327 # First successful conversion wins 328 for ck, name, ext, input_mimettype, output_mimetype, quality, \ 329 converter in candidates: 330 output = converter.convert_content(req, mimetype, content, ck) 331 if not output: 332 continue 333 return (output[0], output[1], ext) 334 raise TracError('No available MIME conversions from %s to %s' % 335 (mimetype, key)) 747 tab_expanded = None # we don't want to expand tabs more than once. 336 748 749 # First candidate which converts successfully wins. 750 for conversion, converter in candidates: 751 if conversion.expand_tabs and not tab_expanded: 752 tab_expanded = unicode(mimecontent).expandtabs(self.tab_width) 753 mimecontent = MimeContent(self.env, tab_expanded, 754 mimecontent.type) 755 self.log.debug('tab expansion performed.') 756 try: 757 self.log.debug('Attempt conversion %s using %s' % 758 (conversion, converter)) 759 res = converter.convert_content(context, conversion, 760 mimecontent) 761 if not res: 762 continue 763 return res 764 except Exception, e: 765 self.log.warning('MIME conversion using %s failed (%s)' 766 % (converter, e), exc_info=True) 767 raise NoConversion('No MIME conversions succeeded', 768 mimecontent.type, output) 769 770 # -- XHTML rendering and annotations (based on the conversion API) 771 337 772 def get_annotation_types(self): 338 773 """Generator that returns all available annotation types.""" 339 774 for annotator in self.annotators: 340 775 yield annotator.get_annotation_type() 341 776 342 def render(self, req, mimetype, content, filename=None, url=None, 343 annotations=None): 344 """Render an XHTML preview of the given `content`. 777 def render(self, req, mimecontent, annotations=None): 778 """Render an XHTML preview of the given `mimecontent`. 345 779 346 `content` is the same as an `IHTMLPreviewRenderer.render`'s 347 `content` argument. 348 349 The specified `mimetype` will be used to select the most appropriate 350 `IHTMLPreviewRenderer` implementation available for this MIME type. 351 If not given, the MIME type will be infered from the filename or the 352 content. 353 354 Return a string containing the XHTML text. 780 Some `annotations` might be requested as well. 355 781 """ 356 if not content: 357 return '' 782 class ToplevelContext(object): 783 def __init__(self, req): 784 self.req = req 785 result = self.convert(ToplevelContext(req), 'text/html', mimecontent) 358 786 359 # Ensure we have a MIME type for this content 360 full_mimetype = mimetype 361 if not full_mimetype: 362 if hasattr(content, 'read'): 363 content = content.read(self.max_preview_size) 364 full_mimetype = self.get_mimetype(filename, content) 365 if full_mimetype: 366 mimetype = full_mimetype.split(';')[0].strip() # split off charset 367 else: 368 mimetype = full_mimetype = 'text/plain' # fallback if not binary 787 if isinstance(result, Fragment): 788 return result # might be processed further 789 elif isinstance(result, basestring): 790 self.log.warning('HTML rendering: got %s' % 791 result.__class__.__name__) 792 return Markup(to_unicode(result)) # needed for compatibility 369 793 370 # Determine candidate `IHTMLPreviewRenderer`s 371 candidates = [] 372 for renderer in self.renderers: 373 qr = renderer.get_quality_ratio(mimetype) 374 if qr > 0: 375 candidates.append((qr, renderer)) 376 candidates.sort(lambda x,y: cmp(y[0], x[0])) 794 # otherwise, it's an iterable yielding lines 795 if annotations: 796 return Markup(self._annotate(result, annotations)) 377 797 378 # First candidate which renders successfully wins. 379 # Also, we don't want to expand tabs more than once. 380 expanded_content = None 381 for qr, renderer in candidates: 382 try: 383 self.log.debug('Trying to render HTML preview using %s' 384 % renderer.__class__.__name__) 385 # check if we need to perform a tab expansion 386 rendered_content = content 387 if getattr(renderer, 'expand_tabs', False): 388 if expanded_content is None: 389 content = content_to_unicode(self.env, content, 390 full_mimetype) 391 expanded_content = content.expandtabs(self.tab_width) 392 rendered_content = expanded_content 393 result = renderer.render(req, full_mimetype, rendered_content, 394 filename, url) 395 if not result: 396 continue 397 elif isinstance(result, Fragment): 398 return result 399 elif isinstance(result, basestring): 400 return Markup(to_unicode(result)) 401 elif annotations: 402 return Markup(self._annotate(result, annotations)) 403 else: 404 buf = StringIO() 405 buf.write('<div class="code"><pre>') 406 for line in result: 407 buf.write(line + '\n') 408 buf.write('</pre></div>') 409 return Markup(buf.getvalue()) 410 except Exception, e: 411 self.log.warning('HTML preview using %s failed (%s)' 412 % (renderer, e), exc_info=True) 798 return html.DIV(html.PRE(Markup(''.join(result))), class_="code") 413 799 414 800 def _annotate(self, lines, annotations): 801 """Add requested `annotations` to the lines' content.""" 415 802 buf = StringIO() 416 803 buf.write('<table class="code"><thead><tr>') 417 804 annotators = [] … … 445 832 buf.write('</tbody></table>') 446 833 return buf.getvalue() 447 834 835 # -- Deprecated API (TODO: remove in 0.11) 836 448 837 def get_max_preview_size(self): 449 838 """Deprecated: use `max_preview_size` attribute directly.""" 450 839 return self.max_preview_size 451 840 452 def get_charset(self, content='', mimetype=None):453 """Infer the character encoding from the `content` or the `mimetype`.454 455 `content` is either a `str` or an `unicode` object.456 457 The charset will be determined using this order:458 * from the charset information present in the `mimetype` argument459 * auto-detection of the charset from the `content`460 * the configured `default_charset`461 """462 if mimetype:463 ctpos = mimetype.find('charset=')464 if ctpos >= 0:465 return mimetype[ctpos + 8:].strip()466 if isinstance(content, str):467 utf = detect_unicode(content)468 if utf is not None:469 return utf470 return self.default_charset471 472 def get_mimetype(self, filename, content=None):473 """Infer the MIME type from the `filename` or the `content`.474 475 `content` is either a `str` or an `unicode` object.476 477 Return the detected MIME type, augmented by the478 charset information (i.e. "<mimetype>; charset=..."),479 or `None` if detection failed.480 """481 # Extend default extension to MIME type mappings with configured ones482 if not self._mime_map:483 self._mime_map = MIME_MAP484 for mapping in self.config['mimeviewer'].getlist('mime_map'):485 if ':' in mapping:486 assocations = mapping.split(':')487 for keyword in assocations: # Note: [0] kept on purpose488 self._mime_map[keyword] = assocations[0]489 490 mimetype = get_mimetype(filename, content, self._mime_map)491 charset = None492 if mimetype:493 charset = self.get_charset(content, mimetype)494 if mimetype and charset and not 'charset' in mimetype:495 mimetype += '; charset=' + charset496 return mimetype497 498 841 def to_utf8(self, content, mimetype=None): 499 842 """Convert an encoded `content` to utf-8. 500 843 501 844 ''Deprecated in 0.10. You should use `unicode` strings only.'' 502 845 """ 503 return to_utf8(content , self.get_charset(content, mimetype))846 return to_utf8(content) 504 847 505 def to_unicode(self, content, mimetype=None, charset=None): 506 """Convert `content` (an encoded `str` object) to an `unicode` object. 848 # -- Utilities 507 849 508 This calls `trac.util.to_unicode` with the `charset` provided, 509 or the one obtained by `Mimeview.get_charset()`. 850 def configured_modes_mapping(self, renderer): 851 """Utility for configurable custom converters 852 853 Return a MIME type to `(mode,quality)` mapping for given `option`, 854 assuming a format of comma-separated <mimetype>:<mode>:<quality> 855 associations. 856 857 See EnscriptConverter and SilverCityConverter. 510 858 """ 511 if not charset:512 charset = self.get_charset(content, mimetype)513 return to_unicode(content, charset)514 515 def configured_modes_mapping(self, renderer):516 """Return a MIME type to `(mode,quality)` mapping for given `option`"""517 859 types, option = {}, '%s_modes' % renderer 518 860 for mapping in self.config['mimeviewer'].getlist(option): 519 861 if not mapping: … … 526 868 "option." % (mapping, option)) 527 869 return types 528 870 529 def preview_to_hdf(self, req, content, length, mimetype, filename, 530 url=None, annotations=None): 531 """Prepares a rendered preview of the given `content`. 532 533 Note: `content` will usually be an object with a `read` method. 534 """ 535 if length >= self.max_preview_size: 871 def preview_to_hdf(self, req, mimecontent, annotations=None): 872 """Prepares a rendered preview of the given `mimecontent`.""" 873 if len(mimecontent) >= self.max_preview_size: 536 874 return {'max_file_size_reached': True, 537 875 'max_file_size': self.max_preview_size, 538 'raw_href': url}876 'raw_href': mimecontent.url} 539 877 else: 540 return {'preview': self.render(req, mimetype, content, filename, 541 url, annotations), 542 'raw_href': url} 878 preview_error = preview = None 879 try: 880 preview = self.render(req, mimecontent, annotations) 881 except NoConversion, e: 882 preview_error = e.message() 883 return {'preview': preview, 884 'preview_error': preview_error, 885 'raw_href': mimecontent.url} 543 886 544 def send_converted(self, req, in_type, content, selector, filename='file'):545 """Helper method for converting ` content` and sending it directly.887 def send_converted(self, req, selector, mimecontent): 888 """Helper method for converting `mimecontent` and sending it directly. 546 889 547 `selector` can be either a key or a MIME Type.""" 890 `selector` can be either a key or the expected output MIME Type. 891 """ 548 892 from trac.web import RequestDone 549 content, output_type, ext = self.convert_content(req, in_type, 550 content, selector) 893 result = self.convert(req, selector, mimecontent) 551 894 req.send_response(200) 552 req.send_header('Content-Type', output_type)553 req.send_header('Content-Disposition', 'filename=%s.%s' % (filename,554 ext))895 req.send_header('Content-Type', self.get_content_type(result)) 896 req.send_header('Content-Disposition', 897 self.get_content_disposition(result)) 555 898 req.end_headers() 556 req.write(content) 557 raise RequestDone 899 900 self.send_content(req, result) 901 raise RequestDone 902 903 def get_content_type(self, result): 904 if isinstance(result, unicode): 905 return 'utf-8' 906 elif isinstance(result, str): 907 return IDENTITY_CHARSET # FIXME: could probably do better... 908 else: # MimeContentBase 909 return result.encoding or 'utf-8' 910 911 def get_content_disposition(self, result): 912 if isinstance(result, MimeContentBase): 913 return 'filename=%s.%s' % (result.filename, result.type.extension) 914 else: 915 return 'file.'+(is_binary(result) and 'bin' or 'txt') 916 917 def send_content(self, req, result): 918 if isinstance(result, unicode): 919 return result.encode('utf-8') 920 elif isinstance(result, str): 921 return result 922 else: # MimeContentBase 923 if result.encoding: 924 req.write(result.content) 925 else: 926 req.write(unicode(result).encode('utf-8')) 927 # or: for chunk in result: req.write(chunk) 928 # TODO: check what's the fastest solution 558 929 559 930 931 # utility for Mimeview._annotate 560 932 def _html_splitlines(lines): 561 933 """Tracks open and close tags in lines of HTML text and yields lines that 562 934 have no tags spanning more than one line.""" … … 604 976 def annotate_line(self, number, content): 605 977 return '<th id="L%s"><a href="#L%s">%s</a></th>' % (number, number, 606 978 number) 979 # return html.TH(html.A(number, href="#L%s" % number), id=number) 607 980 608 981 609 # -- Default renderers982 # -- Default TEXT_HTML converters (previously ''IHTMLPreviewRenderer'') 610 983 611 984 class PlainTextRenderer(Component): 612 """HTML preview renderer for plain text, and fallback for any kind of text 613 for which no more specific renderer is available. 985 """Convert text to HTML-escaped text. 986 987 Will be used as a fallback for any kind of text 988 for which no more specific HTML converter is available. 614 989 """ 615 implements(I HTMLPreviewRenderer)990 implements(IContentConverter) 616 991 617 expand_tabs = True 618 619 TREAT_AS_BINARY = [ 620 'application/pdf', 621 'application/postscript', 622 'application/rtf' 623 ] 624 625 def get_quality_ratio(self, mimetype): 626 if mimetype in self.TREAT_AS_BINARY: 627 return 0 628 return 1 629 630 def render(self, req, mimetype, content, filename=None, url=None): 631 if is_binary(content): 992 def get_supported_conversions(self, input): 993 if input.mimetype in Mimeview(self.env).treat_as_binary: 994 return 995 yield Conversion(key='default', 996 quality=TEXT_PLAIN.match(input) and 8 or 1, 997 mimetype=TEXT_HTML, expand_tabs=True) 998 999 def convert_content(self, context, conversion, mimecontent): 1000 if mimecontent.is_binary: 632 1001 self.env.log.debug("Binary data; no preview available") 633 return 1002 else: 1003 if not TEXT_PLAIN.match(mimecontent.type): 1004 self.env.log.debug("Fallback to plain text renderer.") 1005 mimecontent.type = MimeType('text/plain', mimecontent.encoding) 1006 return mimecontent 634 1007 635 self.env.log.debug("Using default plain text mimeviewer")636 content = content_to_unicode(self.env, content, mimetype)637 for line in content.splitlines():638 yield escape(line)639 1008 640 641 1009 class ImageRenderer(Component): 642 """Inline image display. Here we don't need the `content` at all.""" 643 implements(IHTMLPreviewRenderer) 1010 """Inline image display. 644 1011 645 def get_quality_ratio(self, mimetype): 646 if mimetype.startswith('image/'): 647 return 8 648 return 0 1012 This renderer doesn't need the actual data at all, only the url. 1013 """ 1014 implements(IContentConverter) 649 1015 650 def render(self, req, mimetype, content, filename=None, url=None): 651 if url: 652 return html.DIV(html.IMG(src=url,alt=filename), 1016 def get_supported_conversions(self, input): 1017 if MimeType('^image/').match(input, regexp=True): 1018 yield Conversion(key='image', quality=8, mimetype=TEXT_HTML) 1019 1020 def convert_content(self, context, conversion, mimecontent): 1021 if mimecontent.url: 1022 return html.DIV(html.IMG(src=mimecontent.url, 1023 alt=mimecontent.filename), 653 1024 class_="image-file") 654 1025 655 1026 656 1027 class WikiTextRenderer(Component): 657 1028 """Render files containing Trac's own Wiki formatting markup.""" 658 implements(IHTMLPreviewRenderer)659 1029 660 def get_quality_ratio(self, mimetype): 661 if mimetype in ('text/x-trac-wiki', 'application/x-trac-wiki'): 662 return 8 663 return 0 1030 implements(IContentConverter) 664 1031 665 def render(self, req, mimetype, content, filename=None, url=None): 1032 def get_supported_conversions(self, input): 1033 from trac.wiki import TEXT_X_TRAC_WIKI, APPLICATION_X_TRAC_WIKI 1034 if TEXT_X_TRAC_WIKI.match(input) or \ 1035 APPLICATION_X_TRAC_WIKI.match(input): 1036 yield Conversion(key='wiki', quality=8, mimetype=TEXT_HTML) 1037 1038 def convert_content(self, context, conversion, mimecontent): 666 1039 from trac.wiki import wiki_to_html 667 return wiki_to_html(content_to_unicode(self.env, content, mimetype), 668 self.env, req) 1040 return MimeContent(self.env, wiki_to_html(unicode(mimecontent), 1041 self.env, context.req), 1042 TEXT_HTML) -
trac/mimeview/silvercity.py
108 108 raise Exception, err 109 109 110 110 # SilverCity does not like unicode strings 111 content = content.read() 111 112 content = content.encode('utf-8') 112 113 113 114 # SilverCity generates extra empty line against some types of -
trac/mimeview/patch.py
16 16 # Ludvig Strigeus 17 17 18 18 from trac.core import * 19 from trac.mimeview.api import content_to_unicode,IHTMLPreviewRenderer, Mimeview19 from trac.mimeview.api import IHTMLPreviewRenderer, Mimeview 20 20 from trac.util.markup import escape, Markup 21 21 from trac.web.chrome import add_stylesheet 22 22 … … 65 65 def render(self, req, mimetype, content, filename=None, rev=None): 66 66 from trac.web.clearsilver import HDFWrapper 67 67 68 content = content_to_unicode(self.env, content, mimetype)68 content = unicode(content) 69 69 d = self._diff_to_hdf(content.splitlines(), 70 70 Mimeview(self.env).tab_width) 71 71 if not d: -
trac/mimeview/enscript.py
135 135 cmdline += ' --color -h -q --language=html -p - -E%s' % mode 136 136 self.env.log.debug("Enscript command line: %s" % cmdline) 137 137 138 content = unicode(content) 138 139 np = NaivePopen(cmdline, content.encode('utf-8'), capturestderr=1) 139 140 if np.errorlevel or np.err: 140 141 err = 'Running (%s) failed: %s, %s.' % (cmdline, np.errorlevel, -
trac/mimeview/php.py
20 20 21 21 from trac.core import * 22 22 from trac.config import Option 23 from trac.mimeview.api import IHTMLPreviewRenderer , content_to_unicode23 from trac.mimeview.api import IHTMLPreviewRenderer 24 24 from trac.util import NaivePopen 25 25 from trac.util.markup import Deuglifier 26 26 … … 79 79 cmdline += ' -sn' 80 80 self.env.log.debug("PHP command line: %s" % cmdline) 81 81 82 content = content_to_unicode(self.env, content, mimetype)82 content = unicode(content) 83 83 content = content.encode('utf-8') 84 84 np = NaivePopen(cmdline, content, capturestderr=1) 85 85 if np.errorlevel or np.err: -
trac/versioncontrol/web_ui/util.py
16 16 # Author: Jonas Borgström <jonas@edgewall.com> 17 17 # Christian Boos <cboos@neuf.fr> 18 18 19 import posixpath 19 20 import re 20 21 import urllib 21 22 22 23 from trac.core import TracError 24 from trac.mimeview.api import MimeContentBase 23 25 from trac.util.datefmt import format_datetime, pretty_timedelta 24 26 from trac.util.text import shorten_line 25 27 from trac.util.markup import escape, html, Markup … … 27 29 from trac.wiki import wiki_to_html, wiki_to_oneliner 28 30 29 31 __all__ = ['get_changes', 'get_path_links', 'get_path_rev_line', 30 'get_existing_node', 'render_node_property'] 32 'get_existing_node', 'render_node_property', 33 'NodeMimeContent'] 31 34 35 36 # MimeContent wrapper for repository files 37 # 38 # -- This could be part of trac.versioncontrol.api itself, 39 # if introducing a dependency on trac.mimeview.api is 40 # considered to be OK. 41 42 CHUNK_SIZE = 4096 43 44 class NodeMimeContent(MimeContentBase): 45 """Encapsulation of the content of a file in the repository.""" 46 47 def __init__(self, env, node, url=None): 48 self.node = node 49 MimeContentBase.__init__(self, env, node.content_type, 50 posixpath.basename(node.path), url) 51 self._excerpt = None 52 53 def __iter__(self): 54 """Get chunks of the byte content""" 55 chunk = self.excerpt 56 self._excerpt = None 57 while True: 58 if not chunk: 59 return 60 yield chunk 61 chunk = self._content.read(CHUNK_SIZE) 62 63 def __len__(self): 64 return self.node.get_content_length() 65 66 def _get_excerpt(self): 67 if not self._excerpt: 68 self._content = self.node.get_content() 69 self._excerpt = self._content.read(CHUNK_SIZE) 70 return self._excerpt 71 72 32 73 def get_changes(env, repos, revs, full=None, req=None, format=None): 33 74 db = env.get_db_cnx() 34 75 changes = {} -
trac/versioncontrol/web_ui/changeset.py
26 26 from trac import util 27 27 from trac.config import BoolOption, IntOption 28 28 from trac.core import * 29 from trac.mimeview import Mimeview, is_binary30 29 from trac.perm import IPermissionRequestor 31 30 from trac.Search import ISearchSource, search_to_sql, shorten_result 32 31 from trac.Timeline import ITimelineEventProvider … … 36 35 from trac.versioncontrol import Changeset, Node 37 36 from trac.versioncontrol.diff import get_diff_options, hdf_diff, unified_diff 38 37 from trac.versioncontrol.svn_authz import SubversionAuthorizer 39 from trac.versioncontrol.web_ui.util import render_node_property38 from trac.versioncontrol.web_ui.util import * 40 39 from trac.web import IRequestHandler 41 40 from trac.web.chrome import INavigationContributor, add_link, add_stylesheet 42 41 from trac.wiki import wiki_to_html, wiki_to_oneliner, IWikiSyntaxProvider, \ … … 439 438 The list is empty when no differences between comparable files 440 439 are detected, but the return value is None for non-comparable files. 441 440 """ 442 old_content = old_node.get_content().read()443 if is_binary(old_content):441 old_content = NodeMimeContent(self.env, old_node) 442 if old_content.is_binary: 444 443 return None 445 444 446 new_content = new_node.get_content().read()447 if is_binary(new_content):445 new_content = NodeMimeContent(self.env, new_content) 446 if new_content.is_binary: 448 447 return None 449 448 450 mview = Mimeview(self.env) 451 old_content = mview.to_unicode(old_content, old_node.content_type) 452 new_content = mview.to_unicode(new_content, new_node.content_type) 449 old_content = unicode(old_content) 450 new_content = unicode(new_content) 453 451 454 452 if old_content != new_content: 455 453 context = 3 … … 526 524 'filename=%s.diff' % filename) 527 525 req.end_headers() 528 526 529 mimeview = Mimeview(self.env)530 527 for old_node, new_node, kind, change in repos.get_changes(**diff): 531 528 # TODO: Property changes 532 529 … … 536 533 537 534 new_content = old_content = '' 538 535 new_node_info = old_node_info = ('','') 539 mimeview = Mimeview(self.env)540 536 541 537 if old_node: 542 old_content = old_node.get_content().read()543 if is_binary(old_content):538 old_content = NodeMimeContent(self.env, old_node) 539 if old_content.is_binary: 544 540 continue 545 541 old_node_info = (old_node.path, old_node.rev) 546 old_content = mimeview.to_unicode(old_content, 547 old_node.content_type) 542 old_content = unicode(old_content) 548 543 if new_node: 549 new_content = new_node.get_content().read()550 if is_binary(new_content):544 new_content = NodeMimeContent(self.env, new_node) 545 if new_content.is_binary: 551 546 continue 552 547 new_node_info = (new_node.path, new_node.rev) 553 548 new_path = new_node.path 554 new_content = mimeview.to_unicode(new_content, 555 new_node.content_type) 549 new_content = unicode(new_content) 556 550 else: 557 551 old_node_path = repos.normalize_path(old_node.path) 558 552 diff_old_path = repos.normalize_path(diff.old_path) -
trac/versioncontrol/web_ui/browser.py
23 23 from trac import util 24 24 from trac.config import ListOption, Option 25 25 from trac.core import * 26 from trac.mimeview import Mimeview, is_binary, get_mimetype26 from trac.mimeview import Mimeview, MimeType 27 27 from trac.perm import IPermissionRequestor 28 28 from trac.util import sorted, embedded_numbers 29 29 from trac.util.datefmt import http_date, format_datetime, pretty_timedelta … … 36 36 from trac.versioncontrol.web_ui.util import * 37 37 38 38 39 CHUNK_SIZE = 409640 41 42 39 class BrowserModule(Component): 43 40 44 41 implements(INavigationContributor, IPermissionRequestor, IRequestHandler, … … 199 196 def _render_file(self, req, repos, node, rev=None): 200 197 req.perm.assert_permission('FILE_VIEW') 201 198 202 mimeview = Mimeview(self.env)199 format = req.args.get('format') 203 200 204 # MIME type detection 205 content = node.get_content() 206 chunk = content.read(CHUNK_SIZE) 207 mime_type = node.content_type 208 if not mime_type or mime_type == 'application/octet-stream': 209 mime_type = mimeview.get_mimetype(node.name, chunk) or \ 210 mime_type or 'text/plain' 201 raw_href = req.href.browser(node.path, rev=rev, format='raw') 211 202 203 mimecontent = NodeMimeContent(self.env, node, raw_href) 204 212 205 # Eventually send the file directly 213 format = req.args.get('format') 214 if format in ['raw', 'txt']: 206 if format in ('raw', 'txt'): 207 if format == 'txt': 208 type = MimeType('text/plain', mimecontent.encoding) 209 else: 210 type = mimecontent.type 215 211 req.send_response(200) 216 req.send_header('Content-Type', 217 format == 'txt' and 'text/plain' or mime_type) 218 req.send_header('Content-Length', node.content_length) 212 req.send_header('Content-Type', type.mimetype_charset) 213 req.send_header('Content-Length', len(mimecontent)) 219 214 req.send_header('Last-Modified', http_date(node.last_modified)) 220 215 req.end_headers() 221 216 222 while 1: 223 if not chunk: 224 raise RequestDone 217 for chunk in mimecontent: 225 218 req.write(chunk) 226 chunk = content.read(CHUNK_SIZE)219 raise RequestDone 227 220 else: 228 221 # The changeset corresponding to the last change on `node` 229 222 # is more interesting than the `rev` changeset. 230 changeset = repos.get_changeset(node. rev)223 changeset = repos.get_changeset(node.created_rev) 231 224 232 225 message = changeset.message or '--' 233 226 if self.config['changeset'].getbool('wiki_format_messages'): … … 238 231 239 232 req.hdf['file'] = { 240 233 'rev': node.rev, 241 'changeset_href': req.href.changeset(node. rev),234 'changeset_href': req.href.changeset(node.created_rev), 242 235 'date': format_datetime(changeset.date), 243 236 'age': pretty_timedelta(changeset.date), 244 237 'size': pretty_size(node.content_length), … … 246 239 'message': message 247 240 } 248 241 242 mimetype = mimecontent.type.mimetype 243 249 244 # add ''Plain Text'' alternate link if needed 250 if not is_binary(chunk) and mime_type != 'text/plain': 245 if not mimecontent.is_binary and mimecontent.type.is_known and \ 246 mimetype != 'text/plain': 251 247 plain_href = req.href.browser(node.path, rev=rev, format='txt') 252 248 add_link(req, 'alternate', plain_href, 'Plain Text', 253 249 'text/plain') 254 250 255 251 # add ''Original Format'' alternate link (always) 256 raw_href = req.href.browser(node.path, rev=rev, format='raw') 257 add_link(req, 'alternate', raw_href, 'Original Format', mime_type) 252 add_link(req, 'alternate', raw_href, 'Original Format', mimetype) 258 253 259 254 self.log.debug("Rendering preview of node %s@%s with mime-type %s" 260 % (node.name, str(rev), mime _type))255 % (node.name, str(rev), mimetype)) 261 256 262 del content # the remainder of that content is not needed 257 req.hdf['file'] = Mimeview(self.env).preview_to_hdf( 258 req, mimecontent, annotations=['lineno']) 263 259 264 req.hdf['file'] = mimeview.preview_to_hdf(265 req, node.get_content(), node.get_content_length(), mime_type,266 node.created_path, raw_href, annotations=['lineno'])267 268 260 add_stylesheet(req, 'common/css/code.css') 269 261 270 262 # IWikiSyntaxProvider methods -
trac/wiki/web_ui.py
34 34 from trac.web import HTTPNotFound, IRequestHandler 35 35 from trac.wiki.api import IWikiPageManipulator, WikiSystem 36 36 from trac.wiki.model import WikiPage 37 from trac.wiki.formatter import wiki_to_html, wiki_to_oneliner 38 from trac.mimeview.api import Mimeview, IContentConverter 37 from trac.wiki.formatter import wiki_to_html, wiki_to_oneliner, \ 38 TEXT_X_TRAC_WIKI, APPLICATION_X_TRAC_WIKI 39 from trac.mimeview.api import Mimeview, IContentConverter, Conversion, \ 40 MimeContent, TEXT_PLAIN 39 41 40 42 41 43 class InvalidWikiPage(TracError): … … 50 52 page_manipulators = ExtensionPoint(IWikiPageManipulator) 51 53 52 54 # IContentConverter methods 53 def get_supported_conversions(self):54 yield ('txt', 'Plain Text', 'txt', 'text/x-trac-wiki', 'text/plain', 9)55 55 56 def convert_content(self, req, mimetype, content, key): 57 return (content, 'text/plain;charset=utf-8') 56 def get_supported_conversions(self, input): 57 if TEXT_X_TRAC_WIKI.match(input) or \ 58 APPLICATION_X_TRAC_WIKI.match(input): 59 yield Conversion('txt', quality=8, mimetype=TEXT_PLAIN) 58 60 61 def convert_content(self, context, conversion, content): 62 return content # identity transform 63 59 64 # INavigationContributor methods 60 65 61 66 def get_active_navigation_item(self, req): … … 128 133 else: 129 134 format = req.args.get('format') 130 135 if format: 131 Mimeview(self.env).send_converted(req, 'text/x-trac-wiki', 132 page.text, format, page.name) 136 wiki_content = MimeContent(self.env, page.text, 137 TEXT_X_TRAC_WIKI, 138 filename=page.name) 139 Mimeview(self.env).send_converted(req, format, wiki_content) 133 140 self._render_view(req, db, page) 134 141 135 142 req.hdf['wiki.action'] = action … … 429 436 req.hdf['html.norobots'] = 1 430 437 431 438 # Add registered converters 432 for conversion in Mimeview(self.env).get_supported_conversions( 433 'text/x-trac-wiki'): 439 for conv, _ in Mimeview(self.env).get_conversions(TEXT_X_TRAC_WIKI): 440 if conv.key in ('wiki', 'default'): 441 continue 434 442 conversion_href = req.href.wiki(page.name, version=version, 435 format=conv ersion[0])436 add_link(req, 'alternate', conversion_href, conversion[1],437 conv ersion[3])443 format=conv.key) 444 add_link(req, 'alternate', conversion_href, 445 conv.mimetype.name, conv.mimetype.mimetype_charset) 438 446 439 447 latest_page = WikiPage(self.env, page.name) 440 448 req.hdf['wiki'] = {'exists': page.exists, -
trac/wiki/formatter.py
31 31 from trac.util.markup import escape, Markup, Element, html 32 32 33 33 __all__ = ['wiki_to_html', 'wiki_to_oneliner', 'wiki_to_outline', 34 'wiki_to_link', 'Formatter' ] 34 'wiki_to_link', 'Formatter', 35 'TEXT_X_TRAC_WIKI', 'APPLICATION_X_TRAC_WIKI'] 35 36 36 37 38 TEXT_X_TRAC_WIKI = MimeType('text/x-trac-wiki', 39 name='Trac Wiki Text', extension='txt') 40 41 APPLICATION_X_TRAC_WIKI = MimeType('application/x-trac-wiki', 42 name='Trac Wiki Text', extension='txt') 43 44 37 45 def system_message(msg, text=None): 38 46 return html.DIV(html.STRONG(msg), text and html.PRE(text), 39 47 class_="system-message") … … 43 51 44 52 _code_block_re = re.compile('^<div(?:\s+class="([^"]+)")?>(.*)</div>$') 45 53 46 def __init__(self, env, name):47 # TODO: transmit `formatter` argument48 self.env = env54 def __init__(self, formatter, name): 55 self.formatter = formatter 56 self.env = formatter.env 49 57 self.name = name 50 58 self.error = None 51 59 self.macro_provider = None 60 self.mimetype = None 52 61 53 62 builtin_processors = {'html': self._html_processor, 54 63 'default': self._default_processor, … … 65 74 break 66 75 if not self.processor: 67 76 # Find a matching mimeview renderer 68 from trac.mimeview.api import Mimeview 69 mimetype = Mimeview(self.env).get_mimetype(self.name) 70 if mimetype: 71 self.name = mimetype 77 mimetype = Mimeview(self.env).lookup(self.name) 78 if not APPLICATION_OCTET_STREAM.match(mimetype): 79 self.mimetype = mimetype 72 80 self.processor = self._mimeview_processor 73 81 else: 74 82 self.processor = self._default_processor … … 94 102 # generic processors 95 103 96 104 def _macro_processor(self, req, text): 97 # TODO: macro should take a `formatter` argument 105 # TODO: macro should take a `formatter` argument (0.11) 98 106 self.env.log.debug('Executing Wiki macro %s by provider %s' 99 107 % (self.name, self.macro_provider)) 100 108 return self.macro_provider.render_macro(req, self.name, text) 101 109 102 110 def _mimeview_processor(self, req, text): 103 # TODO: transmit context from `formatter` 104 return Mimeview(self.env).render(req, self.name, text) 111 class FormatterContext(object): 112 def __init__(self, req): 113 self.req = req 114 blockcontent = MimeContent(self.env, text, self.mimetype) 115 return Mimeview(self.env).convert(self.formatter, 'text/html', 116 blockcontent) 117 105 118 106 119 def process(self, req, text, in_paragraph=False): 107 120 if self.error: … … 429 442 return '<br />' 430 443 args = fullmatch.group('macroargs') 431 444 try: 432 macro = WikiProcessor(self .env, name)445 macro = WikiProcessor(self, name) 433 446 return macro.process(self.req, args, True) 434 447 except Exception, e: 435 448 self.env.log.error('Macro %s(%s) failed' % (name, args), … … 714 727 else: 715 728 self.code_text += line + os.linesep 716 729 if not self.code_processor: 717 self.code_processor = WikiProcessor(self .env, 'default')730 self.code_processor = WikiProcessor(self, 'default') 718 731 elif line.strip() == Formatter.ENDBLOCK: 719 732 self.in_code_block -= 1 720 733 if self.in_code_block == 0 and self.code_processor: … … 728 741 match = Formatter._processor_re.search(line) 729 742 if match: 730 743 name = match.group(1) 731 self.code_processor = WikiProcessor(self .env, name)744 self.code_processor = WikiProcessor(self, name) 732 745 else: 733 746 self.code_text += line + os.linesep 734 self.code_processor = WikiProcessor(self .env, 'default')747 self.code_processor = WikiProcessor(self, 'default') 735 748 else: 736 749 self.code_text += line + os.linesep 737 750 -
trac/web/chrome.py
17 17 import os 18 18 import re 19 19 20 from trac import mimeview21 20 from trac.config import * 22 21 from trac.core import * 23 22 from trac.env import IEnvironmentSetupParticipant 23 from trac.mimeview import get_mimetype 24 24 from trac.util.markup import html 25 25 from trac.web.api import IRequestHandler, HTTPNotFound 26 26 from trac.web.href import Href … … 288 288 icon = href.chrome(icon) 289 289 else: 290 290 icon = href.chrome('common', icon) 291 mimetype = mimeview.get_mimetype(icon)291 mimetype = get_mimetype(icon) 292 292 add_link(req, 'icon', icon, mimetype=mimetype) 293 293 add_link(req, 'shortcut icon', icon, mimetype=mimetype) 294 294 -
trac/util/text.py
25 25 26 26 27 27 CRLF = '\r\n' 28 IDENTITY_CHARSET = 'iso-8859-1' # not iso-8859-15 28 29 29 30 # -- Unicode 30 31 … … 40 41 If the `lossy` argument is `True`, which is the default, then 41 42 we use the 'replace' mode: 42 43 43 If the `lossy` argument is `False`, we fallback to the 'iso-8859-1 5'44 charset in case of an error (encoding a `str` using 'iso-8859-1 5'44 If the `lossy` argument is `False`, we fallback to the 'iso-8859-1' 45 charset in case of an error (encoding a `str` using 'iso-8859-1' 45 46 will always work, as there's one Unicode character for each byte of 46 47 the input). 47 48 """ … … 65 66 except UnicodeError: 66 67 return unicode(text, locale.getpreferredencoding(), errors) 67 68 except UnicodeError: 68 return unicode(text, 'iso-8859-15')69 return unicode(text, IDENTITY_CHARSET) 69 70 70 71 def unicode_quote(value): 71 72 """A unicode aware version of urllib.quote""" … … 85 86 return urlencode([(k, isinstance(v, unicode) and v.encode('utf-8') or v) 86 87 for k, v in params]) 87 88 88 def to_utf8(text, charset= 'iso-8859-15'):89 def to_utf8(text, charset=IDENTITY_CHARSET): 89 90 """Convert a string to UTF-8, assuming the encoding is either UTF-8, ISO 90 91 Latin-1, or as specified by the optional `charset` parameter. 91 92 … … 101 102 u = unicode(text, charset) 102 103 except UnicodeError: 103 104 # This should always work 104 u = unicode(text, 'iso-8859-15')105 u = unicode(text, IDENTITY_CHARSET) 105 106 return u.encode('utf-8') 106 107 107 108 -
templates/browser.cs
111 111 </table><?cs 112 112 /if ?><?cs 113 113 114 if:!browser.is_dir ?> 115 <div id="preview"><?cs 116 if:file.preview ?><?cs 117 var:file.preview ?><?cs 118 elif:file.max_file_size_reached ?> 119 <strong>HTML preview not available</strong>, since the file size exceeds 120 <?cs var:file.max_file_size ?> bytes. Try <a href="<?cs 121 var:file.raw_href ?>">downloading</a> the file instead.<?cs 122 else ?><strong>HTML preview not available</strong>. To view, <a href="<?cs 123 var:file.raw_href ?>">download</a> the file.<?cs 124 /if ?> 125 </div><?cs 114 if:!browser.is_dir ?><?cs 115 call:html_preview(file) ?><?cs 126 116 /if ?> 127 117 128 118 <div id="help"> -
templates/macros.cs
194 194 195 195 def:plural(base, count) ?><?cs 196 196 var:base ?><?cs if:count != 1 ?>s<?cs /if ?><?cs 197 /def ?><?cs 198 199 def:html_preview(base) ?> 200 <div id="preview"><?cs 201 if:base.preview ?> 202 <?cs var:base.preview ?><?cs 203 elif:base.max_file_size_reached ?> 204 <strong>HTML preview not available</strong>, since the file size exceeds 205 <?cs var:base.max_file_size ?> bytes. You may <a href="<?cs 206 var:base.raw_href ?>">download the file</a> instead.<?cs 207 else ?> 208 <strong>HTML preview not available</strong>. To view the file, 209 <a href="<?cs var:base.raw_href ?>">download the file</a>.<?cs 210 /if ?> 211 </div><?cs 197 212 /def ?> -
templates/attachment.cs
66 66 </th></tr><tr> 67 67 <td class="message"><?cs var:attachment.description ?></td> 68 68 </tr> 69 </tbody></table> 70 <div id="preview"><?cs 71 if:attachment.preview ?> 72 <?cs var:attachment.preview ?><?cs 73 elif:attachment.max_file_size_reached ?> 74 <strong>HTML preview not available</strong>, since the file size exceeds 75 <?cs var:attachment.max_file_size ?> bytes. You may <a href="<?cs 76 var:attachment.raw_href ?>">download the file</a> instead.<?cs 77 else ?> 78 <strong>HTML preview not available</strong>. To view the file, 79 <a href="<?cs var:attachment.raw_href ?>">download the file</a>.<?cs 80 /if ?> 81 </div> 69 </tbody></table><?cs 70 call:html_preview(attachment) ?> 82 71 <?cs if:attachment.can_delete ?><div class="buttons"> 83 72 <form method="get" action=""><div id="delete"> 84 73 <input type="hidden" name="action" value="delete" />
