Merge lp:~mkanat/loggerhead/raw-controller into lp:loggerhead

Proposed by Max Kanat-Alexander
Status: Merged
Approved by: Max Kanat-Alexander
Approved revision: 439
Merged at revision: 439
Proposed branch: lp:~mkanat/loggerhead/raw-controller
Merge into: lp:loggerhead
Diff against target: 396 lines (+140/-60)
8 files modified
loggerhead/apps/branch.py (+2/-0)
loggerhead/controllers/__init__.py (+37/-6)
loggerhead/controllers/download_ui.py (+5/-14)
loggerhead/controllers/raw_ui.py (+60/-0)
loggerhead/controllers/view_ui.py (+16/-30)
loggerhead/static/css/files.css (+7/-0)
loggerhead/templates/inventory.pt (+9/-10)
loggerhead/templates/view.pt (+4/-0)
To merge this branch: bzr merge lp:~mkanat/loggerhead/raw-controller
Reviewer Review Type Date Requested Status
Martin Pool Approve
Review via email: mp+42675@code.launchpad.net

Description of the change

This addresses bug 605775 by adding a new "raw" controller that displays raw data. It's surprisingly fast--about 0.05s to display almost any file. It does its best to determine the correct MIME type to serve a file with, although it will often fall back to text/plain or application/octet-stream.

The only thing that this patch is missing is XSS protection (serving the raw content from a different domain), which is something I want to do in a separate bug and MP, because this MP is already complex enough as it is. There's also some preliminary refactoring involved in this patch that was necessary to get a "raw" controller that worked well without copying-and-pasting code. (After this, there's also some refactoring that can be done to make the "download" controller a subclass of RawUI, but that will involve changing the download URLs and how they work, so that's also something I wanted to do separately.)

To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 12/3/2010 1:41 PM, Max Kanat-Alexander wrote:
> Max Kanat-Alexander has proposed merging lp:~mkanat/loggerhead/raw-controller into lp:loggerhead.
>
> Requested reviews:
> Bazaar Developers (bzr)
>
>
> This addresses bug 605775 by adding a new "raw" controller that displays raw data. It's surprisingly fast--about 0.05s to display almost any file. It does its best to determine the correct MIME type to serve a file with, although it will often fall back to text/plain or application/octet-stream.
>
> The only thing that this patch is missing is XSS protection (serving the raw content from a different domain), which is something I want to do in a separate bug and MP, because this MP is already complex enough as it is. There's also some preliminary refactoring involved in this patch that was necessary to get a "raw" controller that worked well without copying-and-pasting code. (After this, there's also some refactoring that can be done to make the "download" controller a subclass of RawUI, but that will involve changing the download URLs and how they work, so that's also something I wanted to do separately.)

If we are worried about XSS, why not start by having raw just return
everything as application/octet-stream until we figure out how to
protect against it?

John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAkz5Y50ACgkQJdeBCYSNAAMItACfUNGl+AHZBIBTQpIfDr9lgiwO
wUkAni4f6kZji70Rq0D8/cRHssArTxBo
=JeEQ
-----END PGP SIGNATURE-----

Revision history for this message
Robert Collins (lifeless) wrote :

application/octet-stream is unsafe.

-Rob

Revision history for this message
Robert Collins (lifeless) wrote :

See http://msdn.microsoft.com/en-us/library/ms775147(VS.85).aspx for
*one* documented behaviour about content-sniffing.

Basically you need one domain per security context - and for us that
means pretty much one domain per file content. See the
publicrestrictedlibrarian, for instance.

-Rob

Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 12/3/2010 4:07 PM, Robert Collins wrote:
> application/octet-stream is unsafe.
>
> -Rob

We already have a way to get that, though. The 'download' urls do that.
Or are they somehow already protected against XSS, and new stuff
wouldn't be?

John
=:->

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAkz5arEACgkQJdeBCYSNAAOb7gCdH5Il4yjztErqf5GxdNVa7GvU
w5IAoJoQ/Y621aXMNFBRheD2fucpC/lE
=SKtw
-----END PGP SIGNATURE-----

Revision history for this message
Robert Collins (lifeless) wrote :

They probably set Content-Disposition: attachment, which (AFAIK) all
browsers honour, at least to the extent of not rendering the thing in
the source context.

-Rob

Revision history for this message
Max Kanat-Alexander (mkanat) wrote :

> If we are worried about XSS, why not start by having raw just return
> everything as application/octet-stream until we figure out how to
> protect against it?

  Because we already have /download/ for that. This is about displaying content in the browser.

Revision history for this message
Martin Pool (mbp) wrote :

The diff itself looks reasonable to me. The main issue seems to be XSS.

I don't think it is a good idea to merge something into loggerhead trunk that makes it 'dangerous by default'. Some people run from trunk; someone else might make a release without realizing this was not yet finished. If there was an option to enable it, with a clear warning, that would be fine.

(Tangentially, I think we should have a discussion about how we're going to manage qa of the recent loggerhead changes before they're generally available, but that can be separate from this.)

It's great that this is so fast.

John's bug is really two bugs (as he acknowledges, but perhaps we should have split it before continuing.)

> 1) Something like the 'download' link that doesn't require including the file id.

It looks like that is fixed by this, which is great.

Really this is probably mostly independent of the download issue, and it's equally applicable to wanting to make up a url that gives you annotations or any other view.

If Loggerhead still generates urls containing file ids by default it may not be obvious to users they can be left out?

> 2) A way to view raw content in-the-browser, rather than always being an octet stream. So 'raw' vs 'download'.

This one seems to have inherent XSS-type problems, and we should perhaps narrow it down in the context of that bug. What do you want to see in the browser? What are the use cases? I would say almost certainly not arbitrary HTML.

To me the most obvious use case is to get plain text files from which you can easily copy/paste. Perhaps that can be accommodated well by escaping them into HTML?

The second most important case is to get a download URL that you could paste into a shell eg wget command. I think that c-d: attachment will still let this work?

The third might be to click through to png/jpg images within loggerhead. That seems problematic wrt abuse, and opens up to content sniffing.

I don't think we should merge this until we understand more clearly what the bug is trying to fix. If we do decide that a plain raw view is useful to some users, it should be off by default.

review: Needs Information
Revision history for this message
Max Kanat-Alexander (mkanat) wrote :

I've addressed all of the questions and comments on bug 605775 comment 5, I believe.

The only point that I didn't make there is that most loggerhead installations would not generally be able to do anything harmful to the user by injecting scripts into the page, I'm somewhat sure. I do agree that we shouldn't ship as dangerous by default, though.

Also, to be clear, loggerhead does not generate URLs that use file ids by default, with the only current exception being /download/. However, I was planning on fixing /download/ after this, which will be pretty simple since it can become a subclass of RawUI. But I didn't want to do two changes in one, particularly since one is a behavioral change for URLs.

I don't see why a plain raw view should be off by default--it seems like something that every VCS browser does. I do think that there should be a way for people to protect themselves against XSS, though. But the fix is complex, somewhat, so I wanted to separate it out so that we could do a good security review on just that fix, and not be distracted by the surrounding refactoring and new /raw/ code.

Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

> I don't see why a plain raw view should be off by default--it seems like something that every VCS browser does. I do think that there should be a way for people to protect themselves against XSS, though. But the fix is complex, somewhat, so I wanted to separate it out so that we could do a good security review on just that fix, and not be distracted by the surrounding refactoring and new /raw/ code.

I would say as long as something is "text/plain" then you could serve it
by default. Otherwise make it an attachment?

John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAkz+ef8ACgkQJdeBCYSNAAMWpgCcC1L/g+R6sDKShHxTKilDfzLm
k+UAn3TLhE5t3mQ8WJRZbKgam7nmtRC/
=YnD3
-----END PGP SIGNATURE-----

Revision history for this message
Martin Pool (mbp) wrote :

Let's get this unblocked.

> > I don't see why a plain raw view should be off by default--it seems like
> something that every VCS browser does. I do think that there should be a way
> for people to protect themselves against XSS, though. But the fix is complex,
> somewhat, so I wanted to separate it out so that we could do a good security
> review on just that fix, and not be distracted by the surrounding refactoring
> and new /raw/ code.

I think it's clear some people will want this on and some will want it off. Therefore we need an option. If there is an option that chooses between "invisibly dangerous" and "obviously broken" I think the second is a much better default.

> I would say as long as something is "text/plain" then you could serve it
> by default. Otherwise make it an attachment?

This is the whole problem with xss and content-sniffing: the server can mark it as text/plain, but some browsers will still interpret it as something else if it looks like it's not actually text. They may interpret it as something that lets malicious content steal people's credentials. See Robert's link.

Can you please add an option?

Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 12/14/2010 9:31 PM, Martin Pool wrote:
> Let's get this unblocked.
>
>>> I don't see why a plain raw view should be off by default--it seems like
>> something that every VCS browser does. I do think that there should be a way
>> for people to protect themselves against XSS, though. But the fix is complex,
>> somewhat, so I wanted to separate it out so that we could do a good security
>> review on just that fix, and not be distracted by the surrounding refactoring
>> and new /raw/ code.
>
> I think it's clear some people will want this on and some will want it off. Therefore we need an option. If there is an option that chooses between "invisibly dangerous" and "obviously broken" I think the second is a much better default.
>
>> I would say as long as something is "text/plain" then you could serve it
>> by default. Otherwise make it an attachment?
>
> This is the whole problem with xss and content-sniffing: the server can mark it as text/plain, but some browsers will still interpret it as something else if it looks like it's not actually text. They may interpret it as something that lets malicious content steal people's credentials. See Robert's link.
>
> Can you please add an option?

Matthew Nuzum just put up a test, and I confirmed,
 http://people.canonical.com/~mnuzum/count.html.txt

Is displayed as a text file in my copies of Firefox and Chrome, but
shows up as an HTML page (showing the js content) from Internet Explorer.

John
=:->

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAk0IaQcACgkQJdeBCYSNAAO92wCgpQAL8CSmnAuL3xG9eVJa0mGm
xrwAoKmJJMJRXYS1FJkEZFpc+zUMZifu
=7rJs
-----END PGP SIGNATURE-----

Revision history for this message
Martin Pool (mbp) wrote :

Ok, so in another branch max has added an option that seems suitable for use on Launchpad.

Max makes a case that this is in fact safe to be on by default, because most Loggerhead instances either don't serve untrusted content, or don't have anything that could be exploited by XSS. He has handled something similar in bugzilla in <https://bugzilla.mozilla.org/show_bug.cgi?id=38862>.

I think this ought to be mentioned in README but aside from that I'm ok with merging this.

review: Approve
Revision history for this message
Max Kanat-Alexander (mkanat) wrote :

Accidentally pushed instead of merged, but the end result should be the same.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'loggerhead/apps/branch.py'
2--- loggerhead/apps/branch.py 2010-11-30 10:18:40 +0000
3+++ loggerhead/apps/branch.py 2010-12-03 19:41:25 +0000
4@@ -36,6 +36,7 @@
5 from loggerhead.controllers.download_ui import DownloadUI
6 from loggerhead.controllers.filediff_ui import FileDiffUI
7 from loggerhead.controllers.inventory_ui import InventoryUI
8+from loggerhead.controllers.raw_ui import RawUI
9 from loggerhead.controllers.revision_ui import RevisionUI
10 from loggerhead.controllers.revlog_ui import RevLogUI
11 from loggerhead.controllers.search_ui import SearchUI
12@@ -113,6 +114,7 @@
13 'diff': DiffUI,
14 'download': DownloadUI,
15 'files': InventoryUI,
16+ 'raw': RawUI,
17 'revision': RevisionUI,
18 'search': SearchUI,
19 'view': ViewUI,
20
21=== modified file 'loggerhead/controllers/__init__.py'
22--- loggerhead/controllers/__init__.py 2010-04-26 19:37:26 +0000
23+++ loggerhead/controllers/__init__.py 2010-12-03 19:41:25 +0000
24@@ -17,6 +17,7 @@
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
27
28+import os
29 import time
30
31 from paste.request import path_info_pop, parse_querystring
32@@ -66,7 +67,7 @@
33 return self.__history
34
35 def __call__(self, environ, start_response):
36- z = time.time()
37+ self._call_start = time.time()
38 kwargs = dict(parse_querystring(environ))
39 util.set_context(kwargs)
40 args = []
41@@ -75,12 +76,15 @@
42 if arg is None:
43 break
44 args.append(arg)
45-
46+ self.args = args
47+
48 path = None
49 if len(args) > 1:
50 path = unicode('/'.join(args[1:]), 'utf-8')
51- self.args = args
52-
53+
54+ return self.get_output(path, kwargs, start_response)
55+
56+ def get_output(self, path, kwargs, start_response):
57 vals = {
58 'static_url': self._branch.static_url,
59 'branch': self._branch,
60@@ -93,7 +97,7 @@
61 vals.update(self.get_values(path, kwargs, headers))
62
63 self.log.info('Getting information for %s: %.3f secs' % (
64- self.__class__.__name__, time.time() - z))
65+ self.__class__.__name__, time.time() - self._call_start))
66 if 'Content-Type' not in headers:
67 headers['Content-Type'] = 'text/html'
68 writer = start_response("200 OK", headers.items())
69@@ -105,8 +109,31 @@
70 self.log.info(
71 'Rendering %s: %.3f secs, %s bytes' % (
72 self.__class__.__name__, time.time() - z, w.bytes))
73- return []
74+ return []
75+
76+ def file_info(self, path, kwargs):
77+ history = self._history
78+ revid = self.get_revid()
79+ revid = history.fix_revid(revid)
80+ file_id = kwargs.get('file_id', None)
81+ if (file_id is None) and (path is None):
82+ raise HTTPBadRequest('No file_id or filename provided')
83
84+ if file_id is None:
85+ file_id = history.get_file_id(revid, path)
86+
87+ if path is None:
88+ path = history.get_path(revid, file_id)
89+
90+ filename = os.path.basename(path)
91+
92+ return {
93+ 'file_id': file_id,
94+ 'revid': revid,
95+ 'filename': filename,
96+ 'path': path,
97+ }
98+
99 def get_revid(self):
100 h = self._history
101 if h is None:
102@@ -115,3 +142,7 @@
103 return h.fix_revid(self.args[0])
104 else:
105 return h.last_revid
106+
107+ def tree_for(self, file_id, revid):
108+ file_revid = self._history.get_inventory(revid)[file_id].revision
109+ return self._history._branch.repository.revision_tree(file_revid)
110\ No newline at end of file
111
112=== modified file 'loggerhead/controllers/download_ui.py'
113--- loggerhead/controllers/download_ui.py 2010-05-05 19:03:40 +0000
114+++ loggerhead/controllers/download_ui.py 2010-12-03 19:41:25 +0000
115@@ -26,23 +26,14 @@
116
117 from loggerhead.controllers import TemplatedBranchView
118
119-log = logging.getLogger("loggerhead.controllers")
120-
121-
122-class DownloadUI (TemplatedBranchView):
123-
124- def __call__(self, environ, start_response):
125+class DownloadUI(TemplatedBranchView):
126+
127+ def get_output(self, path, kwargs, start_response):
128 # /download/<rev_id>/<file_id>/[filename]
129
130 h = self._history
131-
132- args = []
133- while True:
134- arg = path_info_pop(environ)
135- if arg is None:
136- break
137- args.append(arg)
138-
139+ args = self.args
140+
141 if len(args) < 2:
142 raise httpexceptions.HTTPMovedPermanently(
143 self._branch.absolute_url('/changes'))
144
145=== added file 'loggerhead/controllers/raw_ui.py'
146--- loggerhead/controllers/raw_ui.py 1970-01-01 00:00:00 +0000
147+++ loggerhead/controllers/raw_ui.py 2010-12-03 19:41:25 +0000
148@@ -0,0 +1,60 @@
149+#
150+# Copyright (C) 2010 Canonical Ltd.
151+#
152+# This program is free software; you can redistribute it and/or modify
153+# it under the terms of the GNU General Public License as published by
154+# the Free Software Foundation; either version 2 of the License, or
155+# (at your option) any later version.
156+#
157+# This program is distributed in the hope that it will be useful,
158+# but WITHOUT ANY WARRANTY; without even the implied warranty of
159+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
160+# GNU General Public License for more details.
161+#
162+# You should have received a copy of the GNU General Public License
163+# along with this program; if not, write to the Free Software
164+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
165+#
166+
167+import mimetypes
168+import time
169+
170+import bzrlib.errors
171+import bzrlib.textfile
172+
173+from loggerhead.controllers import TemplatedBranchView
174+
175+class RawUI(TemplatedBranchView):
176+
177+ def get_output(self, path, kwargs, start_response):
178+ # /raw/<revno>/[path]
179+ file_info = self.file_info(path, kwargs)
180+ revid = file_info['revid']
181+ file_id = file_info['file_id']
182+ filename = file_info['filename']
183+
184+ h = self._history
185+ tree = self.tree_for(file_id, revid)
186+ content = tree.get_file_lines(file_id)
187+ size = tree.inventory[file_id].text_size
188+
189+ mime_type, encoding = mimetypes.guess_type(filename)
190+ if mime_type is None:
191+ try:
192+ bzrlib.textfile.check_text_lines(content)
193+ except bzrlib.errors.BinaryFile:
194+ mime_type = 'application/octet-stream'
195+ else:
196+ mime_type = 'text/plain'
197+
198+ headers = [
199+ ('Content-Type', mime_type),
200+ ('Content-Length', str(size)),
201+ ('X-Content-Type-Options', 'nosniff'),
202+ ]
203+
204+ self.log.info('/raw %s @ %s (%d bytes): %.3f secs',
205+ file_info['path'], revid, size, time.time() - self._call_start)
206+
207+ start_response('200 OK', headers)
208+ return content
209
210=== modified file 'loggerhead/controllers/view_ui.py'
211--- loggerhead/controllers/view_ui.py 2010-12-01 07:13:52 +0000
212+++ loggerhead/controllers/view_ui.py 2010-12-03 19:41:25 +0000
213@@ -18,7 +18,6 @@
214 #
215
216 import cgi
217-import os
218 import time
219
220 import bzrlib.errors
221@@ -39,12 +38,9 @@
222
223 template_path = 'loggerhead.templates.view'
224
225- def tree_for(self, file_id, revid):
226- file_revid = self._history.get_inventory(revid)[file_id].revision
227- return self._history._branch.repository.revision_tree(file_revid)
228-
229- def text_lines(self, file_id, revid):
230- file_name = os.path.basename(self._history.get_path(revid, file_id))
231+ def text_lines(self, file_info):
232+ file_id = file_info['file_id']
233+ revid = file_info['revid']
234
235 tree = self.tree_for(file_id, revid)
236 file_text = tree.get_file_text(file_id)
237@@ -60,7 +56,7 @@
238 bzrlib.textfile.check_text_lines(file_lines)
239
240 if highlight is not None:
241- hl_lines = highlight(file_name, file_text, encoding)
242+ hl_lines = highlight(file_info['filename'], file_text, encoding)
243 # highlight strips off extra newlines at the end of the file.
244 extra_lines = len(file_lines) - len(hl_lines)
245 hl_lines.extend([u''] * extra_lines)
246@@ -69,9 +65,9 @@
247
248 return hl_lines;
249
250- def file_contents(self, file_id, revid):
251+ def file_contents(self, file_info):
252 try:
253- file_lines = self.text_lines(file_id, revid)
254+ file_lines = self.text_lines(file_info)
255 except bzrlib.errors.BinaryFile:
256 # bail out; this isn't displayable text
257 return ['(This is a binary file.)']
258@@ -79,25 +75,15 @@
259 return file_lines
260
261 def get_values(self, path, kwargs, headers):
262+ file_info = self.file_info(path, kwargs)
263+ path = file_info['path']
264+ revid = file_info['revid']
265+
266+ # no navbar for revisions
267+ navigation = util.Container()
268+
269 history = self._history
270 branch = history._branch
271- revid = self.get_revid()
272- revid = history.fix_revid(revid)
273- file_id = kwargs.get('file_id', None)
274- if (file_id is None) and (path is None):
275- raise HTTPBadRequest('No file_id or filename '
276- 'provided to view')
277-
278- if file_id is None:
279- file_id = history.get_file_id(revid, path)
280-
281- # no navbar for revisions
282- navigation = util.Container()
283-
284- if path is None:
285- path = history.get_path(revid, file_id)
286- filename = os.path.basename(path)
287-
288 change = history.get_changes([ revid ])[0]
289 # If we're looking at the tip, use head: in the URL instead
290 if revid == branch.last_revision():
291@@ -126,12 +112,12 @@
292 # true or not before using it, so we have to define it here also.
293 'annotated': False,
294 'revno_url': revno_url,
295- 'file_id': file_id,
296+ 'file_id': file_info['file_id'],
297 'file_path': path,
298- 'filename': filename,
299+ 'filename': file_info['filename'],
300 'navigation': navigation,
301 'change': change,
302- 'contents': self.file_contents(file_id, revid),
303+ 'contents': self.file_contents(file_info),
304 'fileview_active': True,
305 'directory_breadcrumbs': directory_breadcrumbs,
306 'branch_breadcrumbs': branch_breadcrumbs,
307
308=== modified file 'loggerhead/static/css/files.css'
309--- loggerhead/static/css/files.css 2009-06-17 20:28:20 +0000
310+++ loggerhead/static/css/files.css 2010-12-03 19:41:25 +0000
311@@ -48,3 +48,10 @@
312 .bug {
313 background:url(../images/ico_bug.png) no-repeat top left;
314 }
315+#inventory .expcell {
316+ width: 28px;
317+ text-align: left;
318+}
319+.view_link, .raw_link {
320+ margin-right: 10px;
321+}
322\ No newline at end of file
323
324=== modified file 'loggerhead/templates/inventory.pt'
325--- loggerhead/templates/inventory.pt 2010-11-30 10:18:40 +0000
326+++ loggerhead/templates/inventory.pt 2010-12-03 19:41:25 +0000
327@@ -27,7 +27,7 @@
328 </tal:block>
329 </h1>
330
331- <div metal:fill-slot="content">
332+ <div metal:fill-slot="content" id="inventory">
333 <tal:branch-info replace="structure python:branchinfo(branch)" />
334
335 <p tal:condition="python:not change">
336@@ -52,7 +52,6 @@
337 <th class="datecell"><a tal:attributes="href python:url(['/files', revid], sort='date')">Last Changed</a></th>
338 <th class="timedate"><a tal:attributes="href python:url(['/files', revid], sort='size')">Size</a></th>
339 <th class="expandcell"></th>
340- <th class="expandcell"></th>
341 </tr>
342
343 <tr class="blueRow0" tal:condition="python:updir is not None">
344@@ -84,7 +83,6 @@
345 <img tal:attributes="src python:branch.static_url('/static/images/ico_planilla.gif')" alt="Diff" />
346 </a>
347 </td>
348- <td class="expcell"></td>
349 </tr>
350
351 <!-- Show this if it's a symlink -->
352@@ -108,7 +106,6 @@
353 title string:Show revision ${file/change/revno}" />
354 </a>
355 </td>
356- <td class="expcell"></td>
357 </tr>
358
359 <!-- Show this if it's a regular file -->
360@@ -131,13 +128,15 @@
361 </td>
362 <td class="date" tal:content="python:util.date_time(file.change.date)"></td>
363 <td class="timedate2" tal:content="python:util.human_size(file.size)"></td>
364- <td class="expcell"><a tal:attributes="href python:url(['/view', revno_url, file.absolutepath]);
365- title string:View ${file/filename}">
366- <img tal:attributes="src python:branch.static_url('/static/images/ico_planilla.gif')" alt="Diff" />
367- </a>
368- </td>
369 <td class="expcell">
370- <a tal:attributes="href python:url(['/download', file.revid, file.file_id, file.filename]);
371+ <a class="raw_link"
372+ tal:attributes="href python:url(['/raw', revno_url, file.absolutepath]);
373+ title string:Raw content of ${file/filename}">
374+ <img tal:attributes="src python:branch.static_url('/static/images/ico_file.gif')"
375+ alt="View raw file" />
376+ </a>
377+ <a class="download_link"
378+ tal:attributes="href python:url(['/download', file.revid, file.file_id, file.filename]);
379 title string:Download ${file/filename} at revision ${file/change/revno}">
380 <img tal:attributes="src python:branch.static_url('/static/images/ico_file_download.gif')" alt="Download File" />
381 </a>
382
383=== modified file 'loggerhead/templates/view.pt'
384--- loggerhead/templates/view.pt 2010-12-01 08:10:46 +0000
385+++ loggerhead/templates/view.pt 2010-12-03 19:41:25 +0000
386@@ -51,6 +51,10 @@
387 <a tal:attributes="href python:url(['/changes'], clear=1, filter_file_id=file_id)"
388 >view changes to this file</a>
389 </li>
390+ <li>
391+ <a tal:attributes="href python:url(['/raw', revno_url, file_path], clear=1)"
392+ >view raw content</a>
393+ </li>
394 <li id="last">
395 <a tal:attributes="href python:url(['/download',
396 revno_url, file_id, filename])"

Subscribers

People subscribed via source and target branches