39
|
1 # -*- coding: utf-8 -*-
|
|
2 #
|
|
3 # Copyright (C) 2003-2006 Edgewall Software
|
|
4 # Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
|
|
5 # Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
|
|
6 # All rights reserved.
|
|
7 #
|
|
8 # This software is licensed as described in the file COPYING, which
|
|
9 # you should have received as part of this distribution. The terms
|
|
10 # are also available at http://trac.edgewall.com/license.html.
|
|
11 #
|
|
12 # This software consists of voluntary contributions made by many
|
|
13 # individuals. For the exact contribution history, see the revision
|
|
14 # history and logs, available at http://projects.edgewall.com/trac/.
|
|
15 #
|
|
16 # Author: Jonas Borgström <jonas@edgewall.com>
|
|
17 # Christopher Lenz <cmlenz@gmx.de>
|
|
18
|
|
19 import os
|
|
20 import re
|
|
21 import StringIO
|
|
22
|
|
23 from trac.attachment import attachments_to_hdf, Attachment, AttachmentModule
|
|
24 from trac.core import *
|
|
25 from trac.perm import IPermissionRequestor
|
|
26 from trac.Search import ISearchSource, search_to_sql, shorten_result
|
|
27 from trac.Timeline import ITimelineEventProvider
|
|
28 from trac.util import get_reporter_id
|
|
29 from trac.util.datefmt import format_datetime, pretty_timedelta
|
|
30 from trac.util.text import shorten_line
|
|
31 from trac.util.markup import html, Markup
|
|
32 from trac.versioncontrol.diff import get_diff_options, hdf_diff
|
|
33 from trac.web.chrome import add_link, add_stylesheet, INavigationContributor
|
|
34 from trac.web import HTTPNotFound, IRequestHandler
|
|
35 from trac.wiki.api import IWikiPageManipulator, WikiSystem
|
|
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
|
|
39
|
|
40
|
|
41 class InvalidWikiPage(TracError):
|
|
42 """Exception raised when a Wiki page fails validation."""
|
|
43
|
|
44
|
|
45 class WikiModule(Component):
|
|
46
|
|
47 implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
|
|
48 ITimelineEventProvider, ISearchSource, IContentConverter)
|
|
49
|
|
50 page_manipulators = ExtensionPoint(IWikiPageManipulator)
|
|
51
|
|
52 # IContentConverter methods
|
|
53 def get_supported_conversions(self):
|
|
54 yield ('txt', 'Plain Text', 'txt', 'text/x-trac-wiki', 'text/plain', 9)
|
|
55
|
|
56 def convert_content(self, req, mimetype, content, key):
|
|
57 return (content, 'text/plain;charset=utf-8')
|
|
58
|
|
59 # INavigationContributor methods
|
|
60
|
|
61 def get_active_navigation_item(self, req):
|
|
62 return 'wiki'
|
|
63
|
|
64 def get_navigation_items(self, req):
|
|
65 if not req.perm.has_permission('WIKI_VIEW'):
|
|
66 return
|
|
67 yield ('mainnav', 'wiki',
|
|
68 html.A('Wiki', href=req.href.wiki(), accesskey=1))
|
|
69 yield ('metanav', 'help',
|
|
70 html.A('Help/Guide', href=req.href.wiki('TracGuide'),
|
|
71 accesskey=6))
|
|
72
|
|
73 # IPermissionRequestor methods
|
|
74
|
|
75 def get_permission_actions(self):
|
|
76 actions = ['WIKI_CREATE', 'WIKI_DELETE', 'WIKI_MODIFY', 'WIKI_VIEW']
|
|
77 return actions + [('WIKI_ADMIN', actions)]
|
|
78
|
|
79 # IRequestHandler methods
|
|
80
|
|
81 def match_request(self, req):
|
|
82 match = re.match(r'^/wiki(?:/(.*))?', req.path_info)
|
|
83 if match:
|
|
84 if match.group(1):
|
|
85 req.args['page'] = match.group(1)
|
|
86 return 1
|
|
87
|
|
88 def process_request(self, req):
|
|
89 action = req.args.get('action', 'view')
|
|
90 pagename = req.args.get('page', 'WikiStart')
|
|
91 version = req.args.get('version')
|
|
92
|
|
93 if pagename.endswith('/'):
|
|
94 req.redirect(req.href.wiki(pagename.strip('/')))
|
|
95
|
|
96 db = self.env.get_db_cnx()
|
|
97 page = WikiPage(self.env, pagename, version, db)
|
|
98
|
|
99 add_stylesheet(req, 'common/css/wiki.css')
|
|
100
|
|
101 if req.method == 'POST':
|
|
102 if action == 'edit':
|
|
103 latest_version = WikiPage(self.env, pagename, None, db).version
|
|
104 if req.args.has_key('cancel'):
|
|
105 req.redirect(req.href.wiki(page.name))
|
|
106 elif int(version) != latest_version:
|
|
107 action = 'collision'
|
|
108 self._render_editor(req, db, page)
|
|
109 elif req.args.has_key('preview'):
|
|
110 action = 'preview'
|
|
111 self._render_editor(req, db, page, preview=True)
|
|
112 else:
|
|
113 self._do_save(req, db, page)
|
|
114 elif action == 'delete':
|
|
115 self._do_delete(req, db, page)
|
|
116 elif action == 'diff':
|
|
117 get_diff_options(req)
|
|
118 req.redirect(req.href.wiki(page.name, version=page.version,
|
|
119 action='diff'))
|
|
120 elif action == 'delete':
|
|
121 self._render_confirm(req, db, page)
|
|
122 elif action == 'edit':
|
|
123 self._render_editor(req, db, page)
|
|
124 elif action == 'diff':
|
|
125 self._render_diff(req, db, page)
|
|
126 elif action == 'history':
|
|
127 self._render_history(req, db, page)
|
|
128 else:
|
|
129 format = req.args.get('format')
|
|
130 if format:
|
|
131 Mimeview(self.env).send_converted(req, 'text/x-trac-wiki',
|
|
132 page.text, format, page.name)
|
|
133 self._render_view(req, db, page)
|
|
134
|
|
135 req.hdf['wiki.action'] = action
|
|
136 req.hdf['wiki.current_href'] = req.href.wiki(page.name)
|
|
137 return 'wiki.cs', None
|
|
138
|
|
139 # ITimelineEventProvider methods
|
|
140
|
|
141 def get_timeline_filters(self, req):
|
|
142 if req.perm.has_permission('WIKI_VIEW'):
|
|
143 yield ('wiki', 'Wiki changes')
|
|
144
|
|
145 def get_timeline_events(self, req, start, stop, filters):
|
|
146 if 'wiki' in filters:
|
|
147 wiki = WikiSystem(self.env)
|
|
148 format = req.args.get('format')
|
|
149 href = format == 'rss' and req.abs_href or req.href
|
|
150 db = self.env.get_db_cnx()
|
|
151 cursor = db.cursor()
|
|
152 cursor.execute("SELECT time,name,comment,author,version "
|
|
153 "FROM wiki WHERE time>=%s AND time<=%s",
|
|
154 (start, stop))
|
|
155 for t,name,comment,author,version in cursor:
|
|
156 title = Markup('<em>%s</em> edited by %s',
|
|
157 wiki.format_page_name(name), author)
|
|
158 diff_link = html.A('diff', href=href.wiki(name, action='diff',
|
|
159 version=version))
|
|
160 if format == 'rss':
|
|
161 comment = wiki_to_html(comment or '--', self.env, req, db,
|
|
162 absurls=True)
|
|
163 else:
|
|
164 comment = wiki_to_oneliner(comment, self.env, db,
|
|
165 shorten=True)
|
|
166 if version > 1:
|
|
167 comment = Markup('%s (%s)', comment, diff_link)
|
|
168 yield 'wiki', href.wiki(name), title, t, author, comment
|
|
169
|
|
170 # Attachments
|
|
171 def display(id):
|
|
172 return Markup('ticket ', html.EM('#', id))
|
|
173 att = AttachmentModule(self.env)
|
|
174 for event in att.get_timeline_events(req, db, 'wiki', format,
|
|
175 start, stop,
|
|
176 lambda id: html.EM(id)):
|
|
177 yield event
|
|
178
|
|
179 # Internal methods
|
|
180
|
|
181 def _set_title(self, req, page, action):
|
|
182 title = name = WikiSystem(self.env).format_page_name(page.name)
|
|
183 if action:
|
|
184 title += ' (%s)' % action
|
|
185 req.hdf['wiki.page_name'] = name
|
|
186 req.hdf['title'] = title
|
|
187 return title
|
|
188
|
|
189 def _do_delete(self, req, db, page):
|
|
190 if page.readonly:
|
|
191 req.perm.assert_permission('WIKI_ADMIN')
|
|
192 else:
|
|
193 req.perm.assert_permission('WIKI_DELETE')
|
|
194
|
|
195 if req.args.has_key('cancel'):
|
|
196 req.redirect(req.href.wiki(page.name))
|
|
197
|
|
198 version = int(req.args.get('version', 0)) or None
|
|
199 old_version = int(req.args.get('old_version', 0)) or version
|
|
200
|
|
201 if version and old_version and version > old_version:
|
|
202 # delete from `old_version` exclusive to `version` inclusive:
|
|
203 for v in range(old_version, version):
|
|
204 page.delete(v + 1, db)
|
|
205 else:
|
|
206 # only delete that `version`, or the whole page if `None`
|
|
207 page.delete(version, db)
|
|
208 db.commit()
|
|
209
|
|
210 if not page.exists:
|
|
211 req.redirect(req.href.wiki())
|
|
212 else:
|
|
213 req.redirect(req.href.wiki(page.name))
|
|
214
|
|
215 def _do_save(self, req, db, page):
|
|
216 if page.readonly:
|
|
217 req.perm.assert_permission('WIKI_ADMIN')
|
|
218 elif not page.exists:
|
|
219 req.perm.assert_permission('WIKI_CREATE')
|
|
220 else:
|
|
221 req.perm.assert_permission('WIKI_MODIFY')
|
|
222
|
|
223 page.text = req.args.get('text')
|
|
224 if req.perm.has_permission('WIKI_ADMIN'):
|
|
225 # Modify the read-only flag if it has been changed and the user is
|
|
226 # WIKI_ADMIN
|
|
227 page.readonly = int(req.args.has_key('readonly'))
|
|
228
|
|
229 # Give the manipulators a pass at post-processing the page
|
|
230 for manipulator in self.page_manipulators:
|
|
231 for field, message in manipulator.validate_wiki_page(req, page):
|
|
232 if field:
|
|
233 raise InvalidWikiPage("The Wiki page field %s is invalid: %s"
|
|
234 % (field, message))
|
|
235 else:
|
|
236 raise InvalidWikiPage("Invalid Wiki page: %s" % message)
|
|
237
|
|
238 page.save(get_reporter_id(req, 'author'), req.args.get('comment'),
|
|
239 req.remote_addr)
|
|
240 req.redirect(req.href.wiki(page.name))
|
|
241
|
|
242 def _render_confirm(self, req, db, page):
|
|
243 if page.readonly:
|
|
244 req.perm.assert_permission('WIKI_ADMIN')
|
|
245 else:
|
|
246 req.perm.assert_permission('WIKI_DELETE')
|
|
247
|
|
248 version = None
|
|
249 if req.args.has_key('delete_version'):
|
|
250 version = int(req.args.get('version', 0))
|
|
251 old_version = int(req.args.get('old_version', 0)) or version
|
|
252
|
|
253 self._set_title(req, page, 'delete')
|
|
254 req.hdf['wiki'] = {'mode': 'delete'}
|
|
255 if version is not None:
|
|
256 num_versions = 0
|
|
257 for v,t,author,comment,ipnr in page.get_history():
|
|
258 if v >= old_version:
|
|
259 num_versions += 1;
|
|
260 if num_versions > 1:
|
|
261 break
|
|
262 req.hdf['wiki'] = {'version': version, 'old_version': old_version,
|
|
263 'only_version': num_versions == 1}
|
|
264
|
|
265 def _render_diff(self, req, db, page):
|
|
266 req.perm.assert_permission('WIKI_VIEW')
|
|
267
|
|
268 if not page.exists:
|
|
269 raise TracError("Version %s of page %s does not exist" %
|
|
270 (req.args.get('version'), page.name))
|
|
271
|
|
272 add_stylesheet(req, 'common/css/diff.css')
|
|
273
|
|
274 self._set_title(req, page, 'diff')
|
|
275
|
|
276 # Ask web spiders to not index old versions
|
|
277 req.hdf['html.norobots'] = 1
|
|
278
|
|
279 old_version = req.args.get('old_version')
|
|
280 if old_version:
|
|
281 old_version = int(old_version)
|
|
282 if old_version == page.version:
|
|
283 old_version = None
|
|
284 elif old_version > page.version: # FIXME: what about reverse diffs?
|
|
285 old_version, page = page.version, \
|
|
286 WikiPage(self.env, page.name, old_version)
|
|
287 latest_page = WikiPage(self.env, page.name)
|
|
288 new_version = int(page.version)
|
|
289 info = {
|
|
290 'version': new_version,
|
|
291 'latest_version': latest_page.version,
|
|
292 'history_href': req.href.wiki(page.name, action='history')
|
|
293 }
|
|
294
|
|
295 num_changes = 0
|
|
296 old_page = None
|
|
297 prev_version = next_version = None
|
|
298 for version,t,author,comment,ipnr in latest_page.get_history():
|
|
299 if version == new_version:
|
|
300 if t:
|
|
301 info['time'] = format_datetime(t)
|
|
302 info['time_delta'] = pretty_timedelta(t)
|
|
303 info['author'] = author or 'anonymous'
|
|
304 info['comment'] = wiki_to_html(comment or '--',
|
|
305 self.env, req, db)
|
|
306 info['ipnr'] = ipnr or ''
|
|
307 else:
|
|
308 if version < new_version:
|
|
309 num_changes += 1
|
|
310 if not prev_version:
|
|
311 prev_version = version
|
|
312 if (old_version and version == old_version) or \
|
|
313 not old_version:
|
|
314 old_page = WikiPage(self.env, page.name, version)
|
|
315 info['num_changes'] = num_changes
|
|
316 info['old_version'] = version
|
|
317 break
|
|
318 else:
|
|
319 next_version = version
|
|
320 req.hdf['wiki'] = info
|
|
321
|
|
322 # -- prev/next links
|
|
323 if prev_version:
|
|
324 add_link(req, 'prev', req.href.wiki(page.name, action='diff',
|
|
325 version=prev_version),
|
|
326 'Version %d' % prev_version)
|
|
327 if next_version:
|
|
328 add_link(req, 'next', req.href.wiki(page.name, action='diff',
|
|
329 version=next_version),
|
|
330 'Version %d' % next_version)
|
|
331
|
|
332 # -- text diffs
|
|
333 diff_style, diff_options = get_diff_options(req)
|
|
334
|
|
335 oldtext = old_page and old_page.text.splitlines() or []
|
|
336 newtext = page.text.splitlines()
|
|
337 context = 3
|
|
338 for option in diff_options:
|
|
339 if option.startswith('-U'):
|
|
340 context = int(option[2:])
|
|
341 break
|
|
342 if context < 0:
|
|
343 context = None
|
|
344 changes = hdf_diff(oldtext, newtext, context=context,
|
|
345 ignore_blank_lines='-B' in diff_options,
|
|
346 ignore_case='-i' in diff_options,
|
|
347 ignore_space_changes='-b' in diff_options)
|
|
348 req.hdf['wiki.diff'] = changes
|
|
349
|
|
350 def _render_editor(self, req, db, page, preview=False):
|
|
351 req.perm.assert_permission('WIKI_MODIFY')
|
|
352
|
|
353 if req.args.has_key('text'):
|
|
354 page.text = req.args.get('text')
|
|
355 if preview:
|
|
356 page.readonly = req.args.has_key('readonly')
|
|
357
|
|
358 author = get_reporter_id(req, 'author')
|
|
359 comment = req.args.get('comment', '')
|
|
360 editrows = req.args.get('editrows')
|
|
361 if editrows:
|
|
362 pref = req.session.get('wiki_editrows', '20')
|
|
363 if editrows != pref:
|
|
364 req.session['wiki_editrows'] = editrows
|
|
365 else:
|
|
366 editrows = req.session.get('wiki_editrows', '20')
|
|
367
|
|
368 self._set_title(req, page, 'edit')
|
|
369 info = {
|
|
370 'page_source': page.text,
|
|
371 'version': page.version,
|
|
372 'author': author,
|
|
373 'comment': comment,
|
|
374 'readonly': page.readonly,
|
|
375 'edit_rows': editrows,
|
|
376 'scroll_bar_pos': req.args.get('scroll_bar_pos', '')
|
|
377 }
|
|
378 if page.exists:
|
|
379 info['history_href'] = req.href.wiki(page.name,
|
|
380 action='history')
|
|
381 info['last_change_href'] = req.href.wiki(page.name,
|
|
382 action='diff',
|
|
383 version=page.version)
|
|
384 if preview:
|
|
385 info['page_html'] = wiki_to_html(page.text, self.env, req, db)
|
|
386 info['comment_html'] = wiki_to_oneliner(comment, self.env, req, db)
|
|
387 info['readonly'] = int(req.args.has_key('readonly'))
|
|
388 req.hdf['wiki'] = info
|
|
389
|
|
390 def _render_history(self, req, db, page):
|
|
391 """Extract the complete history for a given page and stores it in the
|
|
392 HDF.
|
|
393
|
|
394 This information is used to present a changelog/history for a given
|
|
395 page.
|
|
396 """
|
|
397 req.perm.assert_permission('WIKI_VIEW')
|
|
398
|
|
399 if not page.exists:
|
|
400 raise TracError, "Page %s does not exist" % page.name
|
|
401
|
|
402 self._set_title(req, page, 'history')
|
|
403
|
|
404 history = []
|
|
405 for version, t, author, comment, ipnr in page.get_history():
|
|
406 history.append({
|
|
407 'url': req.href.wiki(page.name, version=version),
|
|
408 'diff_url': req.href.wiki(page.name, version=version,
|
|
409 action='diff'),
|
|
410 'version': version,
|
|
411 'time': format_datetime(t),
|
|
412 'time_delta': pretty_timedelta(t),
|
|
413 'author': author,
|
|
414 'comment': wiki_to_oneliner(comment or '', self.env, db),
|
|
415 'ipaddr': ipnr
|
|
416 })
|
|
417 req.hdf['wiki.history'] = history
|
|
418
|
|
419 def _render_view(self, req, db, page):
|
|
420 req.perm.assert_permission('WIKI_VIEW')
|
|
421
|
|
422 page_name = self._set_title(req, page, '')
|
|
423 if page.name == 'WikiStart':
|
|
424 req.hdf['title'] = ''
|
|
425
|
|
426 version = req.args.get('version')
|
|
427 if version:
|
|
428 # Ask web spiders to not index old versions
|
|
429 req.hdf['html.norobots'] = 1
|
|
430
|
|
431 # Add registered converters
|
|
432 for conversion in Mimeview(self.env).get_supported_conversions(
|
|
433 'text/x-trac-wiki'):
|
|
434 conversion_href = req.href.wiki(page.name, version=version,
|
|
435 format=conversion[0])
|
|
436 add_link(req, 'alternate', conversion_href, conversion[1],
|
|
437 conversion[3])
|
|
438
|
|
439 latest_page = WikiPage(self.env, page.name)
|
|
440 req.hdf['wiki'] = {'exists': page.exists,
|
|
441 'version': page.version,
|
|
442 'latest_version': latest_page.version,
|
|
443 'readonly': page.readonly}
|
|
444 if page.exists:
|
|
445 req.hdf['wiki'] = {
|
|
446 'page_html': wiki_to_html(page.text, self.env, req),
|
|
447 'history_href': req.href.wiki(page.name, action='history'),
|
|
448 'last_change_href': req.href.wiki(page.name, action='diff',
|
|
449 version=page.version)
|
|
450 }
|
|
451 if version:
|
|
452 req.hdf['wiki'] = {
|
|
453 'comment_html': wiki_to_oneliner(page.comment or '--',
|
|
454 self.env, db),
|
|
455 'author': page.author,
|
|
456 'age': pretty_timedelta(page.time)
|
|
457 }
|
|
458 else:
|
|
459 if not req.perm.has_permission('WIKI_CREATE'):
|
|
460 raise HTTPNotFound('Page %s not found', page.name)
|
|
461 req.hdf['wiki.page_html'] = html.P('Describe "%s" here' % page_name)
|
|
462
|
|
463 # Show attachments
|
|
464 req.hdf['wiki.attachments'] = attachments_to_hdf(self.env, req, db,
|
|
465 'wiki', page.name)
|
|
466 if req.perm.has_permission('WIKI_MODIFY'):
|
|
467 attach_href = req.href.attachment('wiki', page.name)
|
|
468 req.hdf['wiki.attach_href'] = attach_href
|
|
469
|
|
470 # ISearchSource methods
|
|
471
|
|
472 def get_search_filters(self, req):
|
|
473 if req.perm.has_permission('WIKI_VIEW'):
|
|
474 yield ('wiki', 'Wiki')
|
|
475
|
|
476 def get_search_results(self, req, terms, filters):
|
|
477 if not 'wiki' in filters:
|
|
478 return
|
|
479 db = self.env.get_db_cnx()
|
|
480 sql_query, args = search_to_sql(db, ['w1.name', 'w1.author', 'w1.text'], terms)
|
|
481 cursor = db.cursor()
|
|
482 cursor.execute("SELECT w1.name,w1.time,w1.author,w1.text "
|
|
483 "FROM wiki w1,"
|
|
484 "(SELECT name,max(version) AS ver "
|
|
485 "FROM wiki GROUP BY name) w2 "
|
|
486 "WHERE w1.version = w2.ver AND w1.name = w2.name "
|
|
487 "AND " + sql_query, args)
|
|
488
|
|
489 for name, date, author, text in cursor:
|
|
490 yield (req.href.wiki(name), '%s: %s' % (name, shorten_line(text)),
|
|
491 date, author, shorten_result(text, terms))
|