MaePadWeb 2.1 "Secession" released
[maepadweb.git] / maepadweb.py
blobec6b42fca3f4d81c0f6210d91e39c16e678740f5
1 #!/usr/bin/python
3 # This file is part of MaePadWeb
4 # Copyright (c) 2010 Thomas Perl <thp.io/about>
5 # http://thp.io/2010/maepad/
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 import os
22 os.chdir(os.path.dirname(__file__) or '.')
24 import sys
25 sys.path.insert(0, os.path.dirname(__file__) or '.')
27 import minidb
28 import urlparse
29 import cgi
30 import random
31 import re
32 import string
33 import subprocess
34 import BaseHTTPServer
35 import threading
36 import gtk
37 import gobject
39 try:
40 import json
41 except:
42 import simplejson as json
44 import gconf
46 class MaePad(object):
47 PIXMAPS = '/usr/share/pixmaps/maepad/'
48 THEME_BASE = '/etc/hildon/theme/images/'
49 THEME = {
50 'toolbar_hi': THEME_BASE + 'toolbar_button_pressed.png',
51 'toolbar_bg': THEME_BASE + 'ToolbarPrimaryBackground.png',
52 'title_bg': THEME_BASE + 'wmTitleBar.png',
53 'title_btn': THEME_BASE + 'wmBackIcon.png',
55 TOOLBAR_HEIGHT = 70
56 TITLE_HEIGHT = 56
58 # The data of a fully transparent 1x1 GIF
59 NIX_GIF = 'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='.decode('base64')
61 class Node(object):
62 UNKNOWN, TEXT, SKETCH, CHECKLIST = range(4)
64 class NodeFlag(object):
65 NONE = 0
66 SKETCHLINES, SKETCHGRAPH, WORDRAP = (1 << x for x in range(3))
68 class ChecklistStyle(object):
69 CHECKED, BOLD, STRIKE = (1 << x for x in range(3))
71 class checklists(object):
72 __slots__ = {
73 'idx': int,
74 'nodeid': int,
75 'name': str,
76 'style': int,
77 'color': int,
78 'ord': int,
81 @classmethod
82 def sorted(cls, l):
83 c = lambda a, b: cmp((a.ord, a.idx), (b.ord, b.idx))
84 return sorted(l, c)
87 class nodes(object):
88 __slots__ = {
89 'nodeid': int,
90 'parent': int,
91 'bodytype': int,
92 'name': str,
93 'body': str,
94 'nameblob': str,
95 'bodyblob': str,
96 'lastmodified': int,
97 'ord': int,
98 'flags': int,
101 @classmethod
102 def sorted(cls, l):
103 c = lambda a, b: cmp(a.ord, b.ord)
104 return sorted(l, c)
107 class MaePadServer(BaseHTTPServer.BaseHTTPRequestHandler):
108 db = None
109 close = False
110 password = None
112 def start_output(self, status=200, content_type='text/html'):
113 self.send_response(status)
114 self.send_header('Content-type', content_type)
115 if status == 401:
116 self.send_header('WWW-Authenticate', 'Basic realm="MaePadWeb"')
117 self.end_headers()
119 def send_as(self, data, mimetype):
120 self.start_output(content_type=mimetype)
121 self.wfile.write(data)
122 self.wfile.close()
124 send_html = lambda s, d: s.send_as(d, 'text/html')
125 send_js = lambda s, d: s.send_as(d, 'text/javascript')
126 send_json = lambda s, d: s.send_as(json.dumps(d), 'text/plain')
127 send_png = lambda s, d: s.send_as(d, 'image/png')
128 send_gif = lambda s, d: s.send_as(d, 'image/gif')
130 def _nodelist__json(self):
131 def get_nodes(parent=0):
132 return [(x.nodeid, x.name, x.bodytype, get_nodes(x.nodeid)) \
133 for x in self.db.load(MaePad.nodes, parent=parent)]
134 self.send_json(get_nodes())
136 def _checklist__json(self, id):
137 c = lambda x: (x.idx, x.name, x.style, '%06x' % x.color)
138 self.send_json([c(x) for x in self.db.load(MaePad.checklists, nodeid=id)])
140 def _checklist__setBold(self, id, bold):
141 bold = (bold == 'true')
142 element = self.db.get(MaePad.checklists, idx=id)
143 if bold:
144 newStyle = element.style | MaePad.ChecklistStyle.BOLD
145 else:
146 newStyle = element.style & ~(MaePad.ChecklistStyle.BOLD)
147 self.db.update(element, style=newStyle)
148 self.send_json(True)
150 def _checklist__setStrike(self, id, strike):
151 strike = (strike == 'true')
152 element = self.db.get(MaePad.checklists, idx=id)
153 if strike:
154 newStyle = element.style | MaePad.ChecklistStyle.STRIKE
155 else:
156 newStyle = element.style & ~(MaePad.ChecklistStyle.STRIKE)
157 self.db.update(element, style=newStyle)
158 self.send_json(True)
160 def _checklist__setCheck(self, id, check):
161 check = (check == 'true')
162 element = self.db.get(MaePad.checklists, idx=id)
163 if check:
164 newStyle = element.style | MaePad.ChecklistStyle.CHECKED
165 else:
166 newStyle = element.style & ~(MaePad.ChecklistStyle.CHECKED)
167 self.db.update(element, style=newStyle)
168 self.send_json(True)
170 def _checklist__setText(self, id, text):
171 element = self.db.get(MaePad.checklists, idx=id)
172 self.db.update(element, name=text)
173 self.send_json(True)
175 def _checklist__addItem(self, id, text):
176 print repr(id)
177 checklist = MaePad.checklists()
178 checklist.nodeid = int(id)
179 checklist.name = text
180 checklist.style = 0
181 checklist.color = 0
182 checklist.ord = 0
183 self.db.save(checklist)
184 self.send_json(True)
186 def _nix__gif(self):
187 self.send_gif(MaePad.NIX_GIF)
189 def _checklist__deleteItem(self, id):
190 self.db.delete(MaePad.checklists, idx=id)
191 self.send_json(True)
193 def _sketch__png(self, id):
194 self.send_png(self.db.get(MaePad.nodes, nodeid=id).bodyblob)
196 def _richtext__html(self, id):
197 self.send_html(self.db.get(MaePad.nodes, nodeid=id).bodyblob)
199 def send_icon(self, icon_name):
200 theme = gtk.icon_theme_get_default()
201 icon = theme.lookup_icon(icon_name, 32, 0)
202 if icon is None:
203 filename = os.path.join(MaePad.PIXMAPS,\
204 icon_name+'.png')
205 else:
206 filename = icon.get_filename()
208 self.send_png(open(filename, 'rb').read())
210 def send_theme(self, theme_part):
211 if theme_part in MaePad.THEME:
212 self.send_png(open(MaePad.THEME[theme_part], 'rb').read())
214 def is_valid_auth(self, auth):
215 if not auth:
216 return False
217 if not auth.startswith('Basic '):
218 return False
219 try:
220 auth = auth[len('Basic '):].decode('base64')
221 except Exception, e:
222 return False
224 username, password = auth.split(':', 1)
225 return (password == MaePadServer.password)
227 def client_is_local(self):
228 host, port = self.client_address
229 return host.startswith('127.')
231 def do_GET(self):
232 url = urlparse.urlparse(self.path)
233 query = cgi.parse_qs(url.query)
234 query = dict((x, y[-1]) for x, y in query.items())
235 path = filter(None, url.path.split('/'))
237 if len(path) == 0:
238 mode = 'index'
239 elif len(path) == 1:
240 mode = path[0]
241 elif len(path) == 2 and path[0] == 'icons':
242 return self.send_icon(path[1])
243 elif len(path) == 2 and path[0] == 'theme':
244 return self.send_theme(path[1])
245 else:
246 self.start_output(404, 'text/plain')
247 self.wfile.write("404'd!")
248 self.wfile.close()
250 if MaePadServer.password and not self.client_is_local():
251 auth = self.headers.get('Authorization', '')
252 if not self.is_valid_auth(auth):
253 self.start_output(401, 'text/plain')
254 self.wfile.write('please authenticate.')
255 self.wfile.close()
256 return
258 if mode == 'index':
259 self.send_html(open('index.html').read())
260 elif mode == 'jquery.js':
261 self.send_js(open('jquery-1.4.3.min.js').read())
262 elif mode == 'logo.png':
263 self.send_png(open('logo.png').read())
264 else:
265 attr_name = '_' + mode.replace('.', '__')
266 if hasattr(self, attr_name):
267 getattr(self, attr_name)(**query)
269 client = gconf.client_get_default()
270 database_file = client.get_string('/apps/maepad/general/lastdocument') or 'memos.db'
272 MaePadServer.db = minidb.Store(database_file)
273 PORT = 8888
275 def server_thread_proc():
276 server = BaseHTTPServer.HTTPServer(('', PORT), MaePadServer)
277 server.timeout = 1
279 try:
280 while not MaePadServer.close:
281 server.handle_request()
282 except:
283 pass
285 def start_server():
286 t = threading.Thread(target=server_thread_proc)
287 t.setDaemon(True)
288 t.start()
290 def get_ips():
291 p = subprocess.Popen(['/sbin/ifconfig'], stdout=subprocess.PIPE)
292 stdout, stderr = p.communicate()
293 p.wait()
294 return [ip for ip, rest in re.findall(r'addr:((\d+\.){3}\d+)', stdout) if not ip.startswith('127.')]
296 w = gtk.Window()
297 w.set_title('MaePadWeb Server')
299 vb = gtk.VBox()
300 vb.set_border_width(10)
302 try:
303 import hildon
304 sw = hildon.PannableArea()
305 except Exception, e:
306 print >>sys.stderr, e
307 sw = gtk.ScrolledWindow()
309 w.add(sw)
310 sw.add_with_viewport(vb)
312 def make_button(title, value=None):
313 try:
314 import hildon
315 b = hildon.Button(gtk.HILDON_SIZE_THUMB_HEIGHT | gtk.HILDON_SIZE_AUTO_WIDTH, 0)
316 b.set_title(title)
317 if value:
318 b.set_value(value)
319 return b
320 except Exception, e:
321 return gtk.Button(title)
323 def generate_password():
324 # http://stackoverflow.com/questions/3854692
325 chars = string.letters + string.digits
326 length = 6
327 return ''.join(random.choice(chars) for _ in xrange(length))
330 def open_website(url):
331 import osso
332 context = osso.Context('MaePadWeb', '1.0', False)
333 rpc = osso.Rpc(context)
334 rpc.rpc_run_with_defaults('osso_browser', \
335 'open_new_window', \
336 (url,))
338 def on_start_server_clicked(button):
339 for child in vb.get_children():
340 vb.remove(child)
342 # Generate a random password
343 MaePadServer.password = generate_password()
345 start_server()
346 vb.add(gtk.Label('Server running on port %d. Close window to stop server.' % PORT))
347 vb.add(gtk.Label('DB file: ' + database_file))
348 vb.add(gtk.Label('Your IP(s): ' + ', '.join(get_ips())))
349 ip = '127.0.0.1'
350 url = 'http://%s:%s/' % (ip, PORT)
351 b = make_button('Open in web browser', url)
352 b.connect('clicked', lambda b, url: open_website(url), url)
353 vb.pack_start(b, expand=False)
354 l = gtk.Label()
355 l.set_markup('Username: <i>any</i>, Password: <b><tt>%s</tt></b>' % MaePadServer.password)
356 vb.add(l)
357 vb.show_all()
359 b = make_button('Start server', 'Start web server on port %d' % PORT)
360 b.connect('clicked', on_start_server_clicked)
362 vb.pack_start(b, expand=False)
363 vb.pack_start(gtk.Label(''))
364 vb.pack_start(gtk.Label('The service is not encrypted. For use in trusted networks only.'))
365 vb.pack_start(gtk.Label('Do not run MaePad and MaePadWeb simultaneously.'))
367 def on_destroy(w):
368 MaePadServer.close = True
369 gtk.main_quit()
371 w.connect('destroy', on_destroy)
372 w.show_all()
374 gobject.threads_init()
376 try:
377 gtk.main()
378 except Exception, e:
379 print >>sys.stderr, 'Caught exception; closing ('+str(e)+')'
381 MaePadServer.db.commit()