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