summaryrefslogtreecommitdiff
path: root/viwiki
blob: 5779654c6740a51942e96104cf2d020d9fa4770b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
#!/usr/bin/env python2.7
"""
Wiki API and Editor
by Mike Crute (mcrute@gmail.com)

This module contains a full API for MoinMoin as well as a shim to download
pages, present them in vim and upload the changes. Great care has been taken to
ensure that this script relies only on the python standard library and that it
is compatible with both Python 2.7+ and Python 3.0+

This module is usually never on the pythonpath for a system but can be loaded
into other python scripts for use by copying this function into their code.

def get_wiki(path):
    import imp, os.path
    return imp.load_module('wiki', open(os.path.expanduser(path)), path,
            [s for s in imp.get_suffixes() if s[0] == '.py'][0])

To get a programatic handle to the API create a class like this which will
parse ~/.wikivimrc and use the login info from the [wiki_name] key.

>>> wiki = get_wiki('~/bin/viwiki')
>>> api = wiki.WikiAPI(*wiki.get_credentials('wiki_name'))

The API supports attachment handling but a separate program called wikiattach
is used for managing attachments from the command line.
"""

import re
import os
import imp
import sys
import stat
import os.path
import tempfile
import subprocess
from codecs import open
from optparse import OptionParser

try:
    from cStringIO import StringIO as BytesIO
    from ConfigParser import SafeConfigParser
    from xmlrpclib import ServerProxy, MultiCall, Fault, Binary
except ImportError:
    from io import BytesIO
    from configparser import SafeConfigParser
    from xmlrpc.client import ServerProxy, MultiCall, Fault, Binary


def get_wiki_config(wiki_name=None):
    """Parses wiki config file and provides config dictionary

    Parses the ~/.wikivimrc file and returns a dictionary of config values for
    the named wiki. If no wiki_name is passed the function will use the first
    wiki defined in the file.
    """
    config = SafeConfigParser()
    config.read(os.path.expanduser("~/.wikivimrc"))

    section = wiki_name if wiki_name else config.sections()[0]

    return dict(
            (key, config.get(section, key))
            for key in config.options(section))


def get_wiki_api(wiki_name=None):
    """Parses wiki config file and provides wiki API class

    Parses the config file and looks for an `api` key in the wiki config. If
    that is specified it must contain a string of wiki class name, colon, path
    to wiki class file per the example below. That class will be imported and
    returned, otherwise the default MoinMoin wiki API is returned.
    """
    config = get_wiki_config(wiki_name)
    api = config.get("api", "WikiAPI").split(":")

    if len(api) == 2:
        module = imp.load_module(
                'wiki_api', open(os.path.expanduser(api[1])), api[1],
                [s for s in imp.get_suffixes() if s[0] == '.py'][0])
        return getattr(module, api[0])
    else:
        return globals()[api[0]]


def get_credentials(wiki_name=None):
    """Parses wiki config file and provides credentials tuple

    Parses the ~/.wikivimrc file and returns a credentials tuple suitable for
    unpacking directly into to the WikiAPI constructor.

    If no wiki_name is passed the function will use the first wiki defined in
    the file.

    The format of the file is:
    [wikiname]
    host = https://example.com/
    username = foobear
    password = secret
    """
    config = get_wiki_config(wiki_name)
    return (config["host"], config.get("username"), config.get("password"))


class WikiAPI(object):
    """Low Level Wiki API

    This class wraps the low level XMLRPC wiki interface and exposes higher
    level methods that do specific tasks in a little bit more python friendly
    way. This assumes a private wiki and requires credentials.
    """

    DEFAULT_FORMAT = "moin"

    def __init__(self, host, username, password):
        self.wiki = ServerProxy("{0}?action=xmlrpc2".format(host))
        self.token = self.wiki.getAuthToken(username, password)

    def __call__(self, method, *args):
        proxy = MultiCall(self.wiki)
        proxy.applyAuthToken(self.token)
        getattr(proxy, method)(*args)
        results = tuple(proxy())

        if results[0] != "SUCCESS":
            raise Exception("Authentication failed!")

        return results[1]

    def page_format(self, page, contents):
        """Determine file format of a page
        """
        format = re.findall("#format (.*)", contents)
        format = format[0] if len(format) else self.DEFAULT_FORMAT
        return self.DEFAULT_FORMAT if format == "wiki" else format

    def search(self, term):
        # http://hg.moinmo.in/moin/1.9/file/tip/MoinMoin/xmlrpc/__init__.py#l683
        return [page[0] for page in
            self("searchPagesEx", term, "text", 0, False, 0, False)]

    def get_page(self, page_name):
        try:
            return self("getPage", page_name).decode("utf-8")
        except Fault as error:
            if error.faultString == "No such page was found.":
                return ""

    def put_page(self, page_name, contents):
        try:
            return self("putPage", page_name, contents.encode("utf-8"))
        except Fault  as error:
            print(error)
            return False

    def list_attachments(self, page_name):
        return self("listAttachments", page_name)

    def get_attachment(self, page_name, attachment):
        return BytesIO(self("getAttachment", page_name, attachment).data)

    def put_attachment(self, page_name, name, contents):
        data = Binary(contents.read())
        self("putAttachment", page_name, name, data)

    def delete_attachment(self, page_name, attachment):
        self("deleteAttachment", page_name, attachment)


class WikiEditor(object):
    """Wiki Page Command Line Editor

    This class handles editing and updating wiki pages based on arguments
    passed on the command line. It requires a connected WikiAPI instance to do
    its dirty work.
    """

    EDITOR_OPTIONS = {
        "vim": "vim -c ':set wrap spell ft={format}' {filename}",
    }

    def __init__(self, wiki_api, editor=None):
        self.api = wiki_api
        self.editor = editor if editor else os.environ.get("EDITOR", "vim")
        self.editor = self.EDITOR_OPTIONS.get(self.editor, self.editor)

    @staticmethod
    def _get_tempfile():
        _, filename = tempfile.mkstemp()
        return filename, open(filename, "w", "utf-8")

    @staticmethod
    def _file_last_updated(filename):
        return os.stat(filename).st_mtime

    def download_page(self, page):
        page_contents = self.api.get_page(page)

        filename, open_file = self._get_tempfile()
        format = self.api.page_format(page, page_contents)

        with open_file as fh:
            fh.write(page_contents)

        return filename, format

    def upload_page(self, page, filename, timestamp):
        updated_last = self._file_last_updated(filename)

        if timestamp == updated_last:
            print("Nothing changed")
            return

        with open(filename, "r", "utf-8") as fh:
            contents = fh.read()

        try:
            if not self.api.put_page(page, contents):
                raise Exception("failed to write page")
        except:
            print("Failed to save page.")
            print("Contents exist in {!r}".format(filename))
            return
        finally:
            del contents

        os.unlink(filename)

    def edit_page(self, page):
        filename, format = self.download_page(page)
        updated_first = self._file_last_updated(filename)

        subprocess.call(
                self.editor.format(format=format, filename=filename),
                shell=True)

        self.upload_page(page, filename, updated_first)


class NonInteractiveEditor(WikiEditor):
    """Non-Interactive Wiki Page Editor

    This class handles editing and updating wiki pages based on arguments
    passed on the command line. It requires a connected WikiAPI instance to do
    its dirty work.
    """

    @staticmethod
    def get_input(arg=None):
        # cat foo.txt | viwiki PageName
        # viwiki PageName < foo.txt
        if (stat.S_IFMT(os.fstat(sys.stdin.fileno()).st_mode)
                in (stat.S_IFREG, stat.S_IFIFO)):
            if hasattr(sys.stdin, 'buffer'):
                return sys.stdin.buffer
            else:
                return sys.stdin

        if not arg:
            return None

        # viwiki PageName <(cat foo.txt)
        # viwiki PageName /path/to/foo.txt
        if os.path.exists(arg):
            return open(arg, 'rb')

    def edit_page(self, args):
        input = self.get_input(args[1] if len(args) == 2 else None)

        if input:
            self.api.put_page(args[0], input.read().decode("utf-8"))
        else:
            sys.stdout.write(self.api.get_page(args[0]).encode("utf-8"))
            sys.stdout.flush()


def main(default_wiki=None):
    parser = OptionParser()
    parser.add_option("-n", "--non-interactive", dest="interactive",
            help="Don't start vim", action="store_false", default=True)
    parser.add_option("-w", "--wiki", dest="wiki", default=default_wiki,
            help="Wiki config to use")
    options, args = parser.parse_args()

    api_class = get_wiki_api(options.wiki)
    api = api_class(*get_credentials(options.wiki))

    if len(args) == 0:
        print("usage: viwiki <page name>")
        return 1

    if options.interactive:
        WikiEditor(api).edit_page(args[0])
    else:
        NonInteractiveEditor(api).edit_page(args)


if __name__ == "__main__":
    main()