baseui.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. # ===========================================================================
  2. # This file is part of Pwman3.
  3. #
  4. # Pwman3 is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License, version 2
  6. # as published by the Free Software Foundation;
  7. #
  8. # Pwman3 is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with Pwman3; if not, write to the Free Software
  15. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
  16. # ============================================================================
  17. # Copyright (C) 2013, 2014 Oz Nahum Tiram <nahumoz@gmail.com>
  18. # ============================================================================
  19. from __future__ import print_function
  20. import sys
  21. import os
  22. import getpass
  23. import ast
  24. import csv
  25. import time
  26. import re
  27. import select as uselect
  28. from colorama import Fore
  29. from pwman.data.nodes import Node
  30. from pwman.ui import tools
  31. from pwman.util.crypto_engine import CryptoEngine
  32. from .base import HelpUIMixin, AliasesMixin
  33. if sys.version_info.major > 2: # pragma: no cover
  34. raw_input = input
  35. def _heard_enter(): # pragma: no cover
  36. i, o, e = uselect.select([sys.stdin], [], [], 0.0001)
  37. for s in i:
  38. if s == sys.stdin:
  39. sys.stdin.readline()
  40. return True
  41. return False
  42. def _wait_until_enter(predicate, timeout, period=0.25): # pragma: no cover
  43. mustend = time.time() + timeout
  44. while time.time() < mustend:
  45. cond = predicate()
  46. if cond:
  47. break
  48. time.sleep(period)
  49. class BaseCommands(HelpUIMixin, AliasesMixin):
  50. @property
  51. def _xsel(self): # pragma: no cover
  52. if self.hasxsel:
  53. return True
  54. def _get_ids(self, args):
  55. """
  56. Command can get a single ID or
  57. a range of IDs, with begin-end.
  58. e.g. 1-3 , will get 1 to 3.
  59. """
  60. ids = []
  61. rex = re.compile("^(?P<begin>\d+)(?:-(?P<end>\d+))?$")
  62. rex = rex.match(args)
  63. if hasattr(rex, 'groupdict'):
  64. try:
  65. begin = int(rex.groupdict()['begin'])
  66. end = int(rex.groupdict()['end'])
  67. if not end > begin:
  68. print("Start node should be smaller than end node")
  69. return ids
  70. ids += range(begin, end+1)
  71. return ids
  72. except TypeError:
  73. ids.append(int(begin))
  74. else:
  75. print("Could not understand your input...")
  76. return ids
  77. def error(self, exception): # pragma: no cover
  78. if (isinstance(exception, KeyboardInterrupt)):
  79. print('')
  80. else:
  81. print("Error: {0} ".format(exception))
  82. def do_copy(self, args): # pragma: no cover
  83. """copy item to clipboard"""
  84. if not self._xsel:
  85. return
  86. if not args.isdigit():
  87. print("Copy accepts only IDs ...")
  88. return
  89. ids = args.split()
  90. if len(ids) > 1:
  91. print("Can copy only 1 password at a time...")
  92. return
  93. nodes = self._db.getnodes(ids)
  94. for node in nodes:
  95. ce = CryptoEngine.get()
  96. password = ce.decrypt(node[2])
  97. tools.text_to_clipboards(password)
  98. print("erasing in 10 sec...")
  99. time.sleep(10) # TODO: this should be configurable!
  100. tools.text_to_clipboards("")
  101. def do_open(self, args): # pragma: no cover
  102. ids = self._get_ids(args)
  103. if not args:
  104. self.help_open()
  105. return
  106. nodes = self._db.getnodes(ids)
  107. for node in nodes:
  108. ce = CryptoEngine.get()
  109. url = ce.decrypt(node[3])
  110. tools.open_url(url)
  111. def do_exit(self, args): # pragma: no cover
  112. """close the text console"""
  113. self._db.close()
  114. return True
  115. def do_cls(self, args): # pragma: no cover
  116. """clear the screen"""
  117. os.system("clear")
  118. def do_edit(self, args):
  119. """edit a node"""
  120. pass
  121. def do_export(self, args):
  122. """export the database to a given format"""
  123. try:
  124. args = ast.literal_eval(args)
  125. except Exception:
  126. args = {}
  127. filename = args.get('filename', 'pwman-export.csv')
  128. delim = args.get('delimiter', ';')
  129. nodeids = self._db.listnodes()
  130. nodes = self._db.getnodes(nodeids)
  131. with open(filename, 'w') as csvfile:
  132. writer = csv.writer(csvfile, delimiter=delim)
  133. writer.writerow(['Username', 'URL', 'Password', 'Notes',
  134. 'Tags'])
  135. for node in nodes:
  136. n = Node.from_encrypted_entries(node[1], node[2], node[3],
  137. node[4],
  138. node[5:])
  139. tags = n.tags
  140. tags = ','.join(t.strip().decode() for t in tags)
  141. r = list(map(bytes.decode, [n.username, n.url, n.password,
  142. n.notes]))
  143. writer.writerow(r + [tags])
  144. print("Successfuly exported database to {}".format(
  145. os.path.join(os.getcwd(), filename)))
  146. def do_forget(self, args):
  147. """
  148. drop saved key forcing the user to re-enter the master
  149. password
  150. """
  151. enc = CryptoEngine.get()
  152. enc.forget()
  153. def do_passwd(self, args): # pragma: no cover
  154. """change the master password of the database"""
  155. pass
  156. def do_tags(self, args):
  157. """
  158. print all existing tags
  159. """
  160. ce = CryptoEngine.get()
  161. print("Tags:")
  162. tags = self._db.listtags()
  163. for t in tags:
  164. print(ce.decrypt(t).decode())
  165. def _get_tags(self, default=None, reader=raw_input):
  166. """
  167. Read tags from user input.
  168. Tags are simply returned as a list
  169. """
  170. # TODO: add method to read tags from db, so they
  171. # could be used for tab completer
  172. print("Tags: ", end="")
  173. sys.stdout.flush()
  174. taglist = sys.stdin.readline()
  175. tagstrings = taglist.split()
  176. tags = [tn for tn in tagstrings]
  177. return tags
  178. def _prep_term(self):
  179. self.do_cls('')
  180. if sys.platform != 'win32':
  181. rows, cols = tools.gettermsize()
  182. else: # pragma: no cover
  183. rows, cols = 18, 80 # fix this !
  184. cols -= 8
  185. return rows, cols
  186. def _format_line(self, tag_pad, nid="ID", user="USER", url="URL",
  187. tags="TAGS"):
  188. return ("{ID:<3} {USER:<{us}}{URL:<{ur}}{Tags:<{tg}}"
  189. "".format(ID=nid, USER=user,
  190. URL=url, Tags=tags, us=12,
  191. ur=20, tg=tag_pad - 32))
  192. def _print_node_line(self, node, rows, cols):
  193. tagstring = ','.join([t.decode() for t in node.tags])
  194. fmt = self._format_line(cols - 32, node._id, node.username.decode(),
  195. node.url.decode(),
  196. tagstring)
  197. formatted_entry = tools.typeset(fmt, Fore.YELLOW, False)
  198. print(formatted_entry)
  199. def _get_node_ids(self, args):
  200. filter = None
  201. if args:
  202. filter = args.split()[0]
  203. ce = CryptoEngine.get()
  204. filter = ce.encrypt(filter)
  205. nodeids = self._db.listnodes(filter=filter)
  206. return nodeids
  207. def _db_entries_to_nodes(self, raw_nodes):
  208. _nodes_inst = []
  209. # user, pass, url, notes
  210. for node in raw_nodes:
  211. _nodes_inst.append(Node.from_encrypted_entries(
  212. node[1],
  213. node[2],
  214. node[3],
  215. node[4],
  216. node[5:]))
  217. _nodes_inst[-1]._id = node[0]
  218. return _nodes_inst
  219. def do_list(self, args):
  220. """list all existing nodes in database"""
  221. rows, cols = self._prep_term()
  222. nodeids = self._get_node_ids(args)
  223. raw_nodes = self._db.getnodes(nodeids)
  224. _nodes_inst = self._db_entries_to_nodes(raw_nodes)
  225. head = self._format_line(cols-32)
  226. print(tools.typeset(head, Fore.YELLOW, False))
  227. for idx, node in enumerate(_nodes_inst):
  228. self._print_node_line(node, rows, cols)
  229. def _get_input(self, prompt):
  230. print(prompt, end="")
  231. sys.stdout.flush()
  232. return sys.stdin.readline().strip()
  233. def _get_secret(self):
  234. # TODO: enable old functionallity, with password generator.
  235. if sys.stdin.isatty(): # pragma: no cover
  236. p = getpass.getpass()
  237. else:
  238. p = sys.stdin.readline().rstrip()
  239. return p
  240. def _do_new(self, args):
  241. node = {}
  242. node['username'] = self._get_input("Username: ")
  243. node['password'] = self._get_secret()
  244. node['url'] = self._get_input("Url: ")
  245. node['notes'] = self._get_input("Notes: ")
  246. node['tags'] = self._get_tags()
  247. node = Node(clear_text=True, **node)
  248. self._db.add_node(node)
  249. return node
  250. def do_new(self, args): # pragma: no cover
  251. # The cmd module stops if and of do_* return something
  252. # else than None ...
  253. # This is bad for testing, so everything that is do_*
  254. # should call _do_* method which is testable
  255. self._do_new(args)
  256. def do_print(self, args):
  257. if not args.isdigit():
  258. print("print accepts only a single ID ...")
  259. return
  260. nodes = self._db.getnodes([args])
  261. node = self._db_entries_to_nodes(nodes)[0]
  262. print(node)
  263. flushtimeout = self.config.get_value('Global', 'cls_timeout')
  264. flushtimeout = flushtimeout or 10
  265. print("Type Enter to flush screen or wait %s sec. " % flushtimeout)
  266. _wait_until_enter(_heard_enter, float(flushtimeout))
  267. self.do_cls('')
  268. def _do_rm(self, args):
  269. for i in args.split():
  270. if not i.isdigit():
  271. print("%s is not a node ID" % i)
  272. return None
  273. for i in args.split():
  274. ans = tools.getinput(("Are you sure you want to delete node {}"
  275. " [y/N]?".format(i)))
  276. if ans.lower() == 'y':
  277. self._db.removenodes([i])
  278. def do_delete(self, args): # pragma: no cover
  279. CryptoEngine.get()
  280. self._do_rm(args)