1# -*- coding: utf-8 -*- 2# 3# Copyright (C) 2003-2021 Edgewall Software 4# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com> 5# Copyright (C) 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 https://trac.edgewall.org/wiki/TracLicense. 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 https://trac.edgewall.org/log/. 15# 16# Author: Jonas Borgström <jonas@edgewall.com> 17# Christopher Lenz <cmlenz@gmx.de> 18 19from trac.core import * 20from trac.resource import Resource 21from trac.util.datefmt import datetime_now, from_utimestamp, to_utimestamp, utc 22from trac.util.translation import _ 23from trac.wiki.api import WikiSystem, validate_page_name 24 25 26class WikiPage(object): 27 """Represents a wiki page (new or existing).""" 28 29 realm = WikiSystem.realm 30 31 @property 32 def resource(self): 33 return Resource(self.realm, self.name, self._resource_version) 34 35 def __init__(self, env, name=None, version=None): 36 """Create a new page object or retrieves an existing page. 37 38 :param env: an `Environment` object. 39 :param name: the page name or a `Resource` object. 40 :param version: the page version. The value takes precedence over the 41 `Resource` version when both are specified. 42 """ 43 self.env = env 44 if version: 45 try: 46 version = int(version) 47 except ValueError: 48 version = None 49 50 if isinstance(name, Resource): 51 resource = name 52 name = resource.id 53 if version is None and resource.version is not None: 54 try: 55 version = int(resource.version) 56 except ValueError: 57 version = None 58 59 self.name = name 60 # The version attribute always returns the version of the page, 61 # however resource.version will be None when version hasn't been 62 # specified when creating the object and the object represents the 63 # most recent version of the page. This behavior is used in web_ui.py 64 # to determine whether to render a versioned page, or just the most 65 # recent version of the page. 66 self._resource_version = version 67 if name: 68 self._fetch(name, version) 69 else: 70 self.version = 0 71 self.text = self.comment = self.author = '' 72 self.time = None 73 self.readonly = 0 74 self.old_text = self.text 75 self.old_readonly = self.readonly 76 77 def _fetch(self, name, version=None): 78 if version is not None: 79 sql = """SELECT version, time, author, text, comment, readonly 80 FROM wiki WHERE name=%s AND version=%s""" 81 args = (name, int(version)) 82 else: 83 sql = """SELECT version, time, author, text, comment, readonly 84 FROM wiki WHERE name=%s ORDER BY version DESC LIMIT 1""" 85 args = (name,) 86 for version, time, author, text, comment, readonly in \ 87 self.env.db_query(sql, args): 88 self.version = int(version) 89 self.author = author 90 self.time = from_utimestamp(time) 91 self.text = text 92 self.comment = comment 93 self.readonly = int(readonly) if readonly else 0 94 break 95 else: 96 self.version = 0 97 self.text = self.comment = self.author = '' 98 self.time = None 99 self.readonly = 0 100 101 def __repr__(self): 102 if self.name is None: 103 name = self.name 104 else: 105 name = '%s@%s' % (self.name, self.version) 106 return '<%s %r>' % (self.__class__.__name__, name) 107 108 exists = property(lambda self: self.version > 0) 109 110 def delete(self, version=None): 111 """Delete one or all versions of a page. 112 """ 113 if not self.exists: 114 raise TracError(_("Cannot delete non-existent page")) 115 116 with self.env.db_transaction as db: 117 if version is None: 118 # Delete a wiki page completely 119 db("DELETE FROM wiki WHERE name=%s", (self.name,)) 120 self.env.log.info("Deleted page %s", self.name) 121 else: 122 # Delete only a specific page version 123 db("DELETE FROM wiki WHERE name=%s and version=%s", 124 (self.name, version)) 125 self.env.log.info("Deleted version %d of page %s", version, 126 self.name) 127 128 if version is None or version == self.version: 129 self._fetch(self.name, None) 130 131 if not self.exists: 132 # Invalidate page name cache 133 del WikiSystem(self.env).pages 134 # Delete orphaned attachments 135 from trac.attachment import Attachment 136 Attachment.delete_all(self.env, self.realm, self.name) 137 138 # Let change listeners know about the deletion 139 if not self.exists: 140 for listener in WikiSystem(self.env).change_listeners: 141 listener.wiki_page_deleted(self) 142 else: 143 for listener in WikiSystem(self.env).change_listeners: 144 if hasattr(listener, 'wiki_page_version_deleted'): 145 listener.wiki_page_version_deleted(self) 146 147 def save(self, author, comment, t=None, replace=False): 148 """Save a new version of a page.""" 149 if not validate_page_name(self.name): 150 raise TracError(_("Invalid Wiki page name '%(name)s'", 151 name=self.name)) 152 153 new_text = self.text != self.old_text 154 if not new_text and self.readonly == self.old_readonly: 155 raise TracError(_("Page not modified")) 156 t = t or datetime_now(utc) 157 158 with self.env.db_transaction as db: 159 if new_text: 160 if replace and self.version != 0: 161 db(""" 162 UPDATE wiki SET text=%s WHERE name=%s AND version=%s 163 """, (self.text, self.name, self.version)) 164 else: 165 self.version += 1 166 db("""INSERT INTO wiki 167 (name,version,time,author,text,comment,readonly) 168 VALUES (%s,%s,%s,%s,%s,%s,%s) 169 """, (self.name, self.version, to_utimestamp(t), 170 author, self.text, comment, self.readonly)) 171 else: 172 db("UPDATE wiki SET readonly=%s WHERE name=%s", 173 (self.readonly, self.name)) 174 if self.version == 1: 175 # Invalidate page name cache 176 del WikiSystem(self.env).pages 177 178 self.author = author 179 self.comment = comment 180 self.time = t 181 182 for listener in WikiSystem(self.env).change_listeners: 183 with self.env.component_guard(listener): 184 if self.version == 1: 185 listener.wiki_page_added(self) 186 else: 187 listener.wiki_page_changed(self, self.version, t, comment, 188 author) 189 190 self.old_readonly = self.readonly 191 self.old_text = self.text 192 193 def rename(self, new_name): 194 """Rename wiki page in-place, keeping the history intact. 195 Renaming a page this way will eventually leave dangling references 196 to the old page - which literally doesn't exist anymore. 197 """ 198 if not self.exists: 199 raise TracError(_("Cannot rename non-existent page")) 200 201 if not new_name: 202 raise TracError(_("A new name is mandatory for a rename.")) 203 204 if self.name == new_name: 205 raise TracError(_("Page name is unchanged.")) 206 207 if not validate_page_name(new_name): 208 raise TracError(_("Invalid Wiki page name '%(name)s'", 209 name=new_name)) 210 old_name = self.name 211 212 with self.env.db_transaction as db: 213 new_page = WikiPage(self.env, new_name) 214 if new_page.exists: 215 raise TracError(_("The page '%(name)s' already exists.", 216 name=new_name)) 217 218 db("UPDATE wiki SET name=%s WHERE name=%s", (new_name, old_name)) 219 # Invalidate page name cache 220 del WikiSystem(self.env).pages 221 # Reparent attachments 222 from trac.attachment import Attachment 223 Attachment.reparent_all(self.env, self.realm, old_name, 224 self.realm, new_name) 225 226 self.name = new_name 227 self.env.log.info("Renamed page %s to %s", old_name, new_name) 228 229 for listener in WikiSystem(self.env).change_listeners: 230 if hasattr(listener, 'wiki_page_renamed'): 231 listener.wiki_page_renamed(self, old_name) 232 233 def edit_comment(self, new_comment): 234 """Edit comment of wiki page version in-place.""" 235 if not self.exists: 236 raise TracError(_("Cannot edit comment of non-existent page")) 237 238 old_comment = self.comment 239 240 with self.env.db_transaction as db: 241 db("UPDATE wiki SET comment=%s WHERE name=%s AND version=%s", 242 (new_comment, self.name, self.version)) 243 244 self.comment = new_comment 245 self.env.log.info("Changed comment on page %s version %s to %s", 246 self.name, self.version, new_comment) 247 248 for listener in WikiSystem(self.env).change_listeners: 249 if hasattr(listener, 'wiki_page_comment_modified'): 250 listener.wiki_page_comment_modified(self, old_comment) 251 252 def get_history(self): 253 """Retrieve the edit history of a wiki page. 254 255 :return: a tuple containing the `version`, `datetime`, `author` 256 and `comment`. 257 """ 258 for version, ts, author, comment in self.env.db_query(""" 259 SELECT version, time, author, comment FROM wiki 260 WHERE name=%s AND version<=%s ORDER BY version DESC 261 """, (self.name, self.version)): 262 yield version, from_utimestamp(ts), author, comment 263