db.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. #!/usr/bin/env python3
  2. #
  3. # db.py by Bill Weinman <http://bw.org/contact/>
  4. # This is part of jurl - Jump to URL (a private short URL service)
  5. # Copyright (c) 2010-2017 The BearHeart Group, LLC
  6. # update 2017-09-29 - for Python 3 EssT
  7. #
  8. import sys, os
  9. import sqlite3
  10. from hashlib import md5
  11. from bwCGI import bwCGI
  12. from bwDB import bwDB
  13. from bwTL import tlFile
  14. from bwConfig import configFile
  15. __version__ = "2.0.1"
  16. # namespace container for global variables
  17. g = dict(
  18. VERSION = 'db.py {} bwDB {}'.format(__version__, bwDB.version()),
  19. config_file = 'db.conf',
  20. template_ext = '.html',
  21. table_name = 'jurl',
  22. stacks = dict(
  23. messages = [],
  24. errors = [],
  25. hiddens = []
  26. )
  27. )
  28. def main():
  29. init()
  30. if 'a' in g['vars']: dispatch()
  31. main_page()
  32. def init():
  33. g['cgi'] = bwCGI()
  34. g['cgi'].send_header()
  35. g['vars'] = g['cgi'].vars()
  36. g['linkback'] = g['cgi'].linkback()
  37. g['config'] = configFile(g['config_file']).recs()
  38. g['tl'] = tlFile(None, showUnknowns = True)
  39. g['db'] = bwDB( filename = g['config']['db'], table = g['table_name'] )
  40. def dispatch():
  41. v = g['vars']
  42. a = v.getfirst('a')
  43. if a == 'add':
  44. add()
  45. elif a == 'edit_del':
  46. if 'edit' in v: edit()
  47. elif 'delete' in v: delete_confirm()
  48. else: error("invalid edit_del")
  49. elif a == 'update':
  50. if 'cancel' in v:
  51. message('Edit canceled')
  52. main_page()
  53. else: update()
  54. elif a == 'delete_do':
  55. if 'cancel' in v:
  56. message('Delete canceled')
  57. main_page()
  58. else: delete_do()
  59. else:
  60. error("unhandled jump: ", a)
  61. main_page()
  62. def main_page():
  63. # save values
  64. unkflag = g['tl'].flags['showUnknowns']
  65. g['tl'].flags['showUnknowns'] = False;
  66. tURL = var('targetURL')
  67. sURL = var('shortURL')
  68. g['tl'].flags['showUnknowns'] = unkflag;
  69. listrecs()
  70. if tURL is not None: var('targetURL', tURL)
  71. if sURL is not None: var('shortURL', sURL)
  72. hidden('a', 'add')
  73. page('main', 'Enter a new short URL')
  74. def listrecs():
  75. ''' display the database content '''
  76. db = g['db']
  77. v = g['vars']
  78. sql_limit = int(g['config'].get('sql_limit', 25))
  79. # how many records do we have?
  80. count = db.countrecs()
  81. message('There are {} records in the database. Add some more!'.format(count or 'no'))
  82. # how many pages do we have?
  83. numpages = count // int(sql_limit)
  84. if count % int(sql_limit): numpages += 1
  85. # what page is this?
  86. curpage = 0
  87. if 'jumppage' in v:
  88. curpage = int(v.getfirst('jumppage'))
  89. elif 'nextpage' in v:
  90. curpage = int(v.getfirst('pageno')) + 1
  91. elif 'prevpage' in v:
  92. curpage = int(v.getfirst('pageno')) - 1
  93. pagebar = list_pagebar(curpage, numpages)
  94. a = ''
  95. q = '''
  96. SELECT * FROM {}
  97. ORDER BY shortURL
  98. LIMIT ?
  99. OFFSET ?
  100. '''.format(g['table_name'])
  101. for r in db.sql_query(q, [sql_limit, (curpage * sql_limit)]):
  102. set_form_vars(**r)
  103. a += getpage('recline')
  104. set_form_vars()
  105. var('CONTENT', pagebar + a + pagebar )
  106. def list_pagebar(pageno, numpages):
  107. ''' return the html for the pager line '''
  108. prevlink = '<span class="n">&lt;&lt;</span>'
  109. nextlink = '<span class="n">&gt;&gt;</span>'
  110. linkback = g['linkback']
  111. if pageno > 0:
  112. prevlink = '<a href="{}?pageno={}&prevpage=1">&lt;&lt;</a>'.format(linkback, pageno)
  113. if pageno < ( numpages - 1 ):
  114. nextlink = '<a href="{}?pageno={}&nextpage=1">&gt;&gt;</a>'.format(linkback, pageno)
  115. pagebar = ''
  116. for n in range(0, numpages):
  117. if n is pageno: pagebar += '<span class="n">{}</span>'.format(n + 1)
  118. else: pagebar += '<a href="{}?jumppage={}">{}</a>'.format(linkback, n, n + 1)
  119. var('prevlink', prevlink)
  120. var('nextlink', nextlink)
  121. var('pagebar', pagebar)
  122. p = getpage('nextprev')
  123. return p
  124. def page(pagename, title = ''):
  125. ''' display a page from html template '''
  126. tl = g['tl']
  127. htmldir = g['config']['htmlDir']
  128. file_ext = g['template_ext']
  129. var('pageTitle', title)
  130. var('VERSION', g['VERSION'])
  131. set_stack_vars()
  132. for p in ( 'header', pagename, 'footer' ):
  133. try:
  134. tl.file(os.path.join(htmldir, p + file_ext))
  135. for line in tl.readlines(): print(line, end='') # lines are already terminated
  136. except IOError as e:
  137. errorexit('Cannot open file ({})'.format(e))
  138. exit()
  139. def getpage(p):
  140. ''' return a page as text from an html template '''
  141. tl = g['tl']
  142. htmldir = g['config']['htmlDir']
  143. file_ext = g['template_ext']
  144. a = ''
  145. try:
  146. tl.file(os.path.join(htmldir, p + file_ext))
  147. for line in tl.readlines(): a += line # lines are already terminated
  148. except IOError as e:
  149. errorexit('Cannot open file ({})'.format(e))
  150. return(a)
  151. ### actions
  152. def add():
  153. db = g['db']
  154. v = g['vars']
  155. cgi = g['cgi']
  156. sURL = tURL = ''
  157. if 'shortURL' in v: sURL = v.getfirst('shortURL')
  158. else: sURL = ''
  159. if 'targetURL' in v: tURL = v.getfirst('targetURL')
  160. else: main_page()
  161. rec = dict(
  162. shortURL = cgi.entity_encode(sURL),
  163. targetURL = cgi.entity_encode(tURL)
  164. )
  165. if 'generate' in v:
  166. rec['shortURL'] = shorten(tURL)
  167. set_form_vars(**rec)
  168. hidden('a', 'add')
  169. main_page()
  170. if 'shortURL' in v:
  171. try:
  172. db.insert(rec)
  173. except (sqlite3.IntegrityError) as e:
  174. error('Duplicate Short URL is not allowed')
  175. set_form_vars(**rec)
  176. hidden('a', 'add')
  177. main_page()
  178. message('Record ({}) added'.format(rec['shortURL']))
  179. main_page()
  180. def edit():
  181. id = g['vars'].getfirst('id')
  182. rec = g['db'].getrec(id)
  183. set_form_vars(**rec)
  184. hidden('a', 'update')
  185. hidden('id', id)
  186. hidden('sURL', rec['shortURL'])
  187. page('edit', 'Edit this short URL')
  188. def delete_confirm():
  189. id = g['vars'].getfirst('id')
  190. rec = g['db'].getrec(id)
  191. set_form_vars(**rec)
  192. hidden('a', 'delete_do')
  193. hidden('id', id)
  194. hidden('shortURL', rec['shortURL'])
  195. page('delconfirm', 'Delete this short URL?')
  196. def delete_do():
  197. db = g['db']
  198. v = g['vars']
  199. id = v.getfirst('id')
  200. shortURL = v.getfirst('shortURL')
  201. db.delete(id)
  202. message('Record ({}) deleted'.format(shortURL))
  203. main_page()
  204. def update():
  205. db = g['db']
  206. v = g['vars']
  207. cgi = g['cgi']
  208. sURL = cgi.entity_encode(v.getfirst('sURL'))
  209. id = v.getfirst('id')
  210. rec = dict(
  211. id = id,
  212. targetURL = cgi.entity_encode(v.getfirst('targetURL'))
  213. )
  214. db.update(id, rec)
  215. message('Record ({}) updated'.format(sURL))
  216. main_page()
  217. ### manage template variables
  218. def var(n, v = None):
  219. ''' shortcut for setting a variable '''
  220. return g['tl'].var(n, v)
  221. def set_form_vars(**kwargs):
  222. s = kwargs.get('shortURL', '')
  223. t = kwargs.get('targetURL', '')
  224. id = kwargs.get('id', '')
  225. var('shortURL', s)
  226. var('targetURL', t)
  227. var('id', id)
  228. var('SELF', g['linkback'])
  229. def stackmessage(stack, *list, **kwargs):
  230. sep = kwargs.get('sep', ' ')
  231. m = sep.join(str(i) for i in list)
  232. g['stacks'][stack].append(m)
  233. def message(*list, **kwargs):
  234. stackmessage('messages', *list, **kwargs)
  235. def error(*list, **kwargs):
  236. if 'cgi' in g:
  237. stackmessage('errors', *list, **kwargs)
  238. else:
  239. errorexit(' '.join(list))
  240. def hidden(n, v):
  241. g['stacks']['hiddens'].append([n, v])
  242. def set_stack_vars():
  243. a = ''
  244. for m in g['stacks']['messages']:
  245. a += '<p class="message">{}</p>\n'.format(m)
  246. var('MESSAGES', a)
  247. a = ''
  248. for m in g['stacks']['errors']:
  249. a += '<p class="error">{}</p>\n'.format(m)
  250. var('ERRORS', a)
  251. a = ''
  252. for m in g['stacks']['hiddens']:
  253. a += '<input type="hidden" name="{}" value="{}" />\n'.format(*m)
  254. var('hiddens', a)
  255. ### utilities
  256. def shorten(s):
  257. lookup = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
  258. lsz = len(lookup)
  259. m = md5(s.encode('utf-8')) # md5 because it's short - doesn't need to be secure or reversible
  260. out = m.digest();
  261. return ''.join('{}'.format(lookup[x % lsz]) for x in out)
  262. def errorexit(e):
  263. me = os.path.basename(sys.argv[0])
  264. print('<p style="color:red">')
  265. print('{}: {}'.format(me, e))
  266. print('</p>')
  267. exit(0)
  268. def message_page(*list):
  269. message(*list)
  270. main_page()
  271. def debug(*args):
  272. print(*args, file=sys.stderr)
  273. if __name__ == "__main__": main()