baseui.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  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. from pwman.util.crypto_engine import zerome
  34. from pwman.ui.tools import CliMenuItem
  35. from pwman.ui.tools import CMDLoop
  36. if sys.version_info.major > 2: # pragma: no cover
  37. raw_input = input
  38. def _heard_enter(): # pragma: no cover
  39. i, o, e = uselect.select([sys.stdin], [], [], 0.0001)
  40. for s in i:
  41. if s == sys.stdin:
  42. sys.stdin.readline()
  43. return True
  44. return False
  45. def _wait_until_enter(predicate, timeout, period=0.25): # pragma: no cover
  46. mustend = time.time() + timeout
  47. while time.time() < mustend:
  48. cond = predicate()
  49. if cond:
  50. break
  51. time.sleep(period)
  52. class BaseCommands(HelpUIMixin, AliasesMixin):
  53. @property
  54. def _xsel(self): # pragma: no cover
  55. if self.hasxsel:
  56. return True
  57. def _get_ids(self, args):
  58. """
  59. Command can get a single ID or
  60. a range of IDs, with begin-end.
  61. e.g. 1-3 , will get 1 to 3.
  62. """
  63. ids = []
  64. rex = re.compile("^(?P<begin>\d+)(?:-(?P<end>\d+))?$")
  65. rex = rex.match(args)
  66. if hasattr(rex, 'groupdict'):
  67. try:
  68. begin = int(rex.groupdict()['begin'])
  69. end = int(rex.groupdict()['end'])
  70. if not end > begin:
  71. print("Start node should be smaller than end node")
  72. return ids
  73. ids += range(begin, end+1)
  74. return ids
  75. except TypeError:
  76. ids.append(int(begin))
  77. else:
  78. print("Could not understand your input...")
  79. return ids
  80. def error(self, exception): # pragma: no cover
  81. if (isinstance(exception, KeyboardInterrupt)):
  82. print('')
  83. else:
  84. print("Error: {0} ".format(exception))
  85. def do_copy(self, args): # pragma: no cover
  86. """copy item to clipboard"""
  87. if not self._xsel:
  88. return
  89. if not args.isdigit():
  90. print("Copy accepts only IDs ...")
  91. return
  92. ids = args.split()
  93. if len(ids) > 1:
  94. print("Can copy only 1 password at a time...")
  95. return
  96. nodes = self._db.getnodes(ids)
  97. for node in nodes:
  98. ce = CryptoEngine.get()
  99. password = ce.decrypt(node[2])
  100. tools.text_to_clipboards(password)
  101. print("erasing in 10 sec...")
  102. time.sleep(10) # TODO: this should be configurable!
  103. tools.text_to_clipboards("")
  104. def do_open(self, args): # pragma: no cover
  105. ids = self._get_ids(args)
  106. if not args:
  107. self.help_open()
  108. return
  109. nodes = self._db.getnodes(ids)
  110. for node in nodes:
  111. ce = CryptoEngine.get()
  112. url = ce.decrypt(node[3])
  113. tools.open_url(url)
  114. def do_exit(self, args): # pragma: no cover
  115. """close the text console"""
  116. self._db.close()
  117. return True
  118. def do_cls(self, args): # pragma: no cover
  119. """clear the screen"""
  120. os.system("clear")
  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([n.username, n.url, n.password, n.notes])
  142. writer.writerow(r + [tags])
  143. print("Successfuly exported database to {}".format(
  144. os.path.join(os.getcwd(), filename)))
  145. def do_forget(self, args):
  146. """
  147. drop saved key forcing the user to re-enter the master
  148. password
  149. """
  150. enc = CryptoEngine.get()
  151. enc.forget()
  152. def do_passwd(self, args): # pragma: no cover
  153. """change the master password of the database"""
  154. pass
  155. def do_tags(self, args):
  156. """
  157. print all existing tags
  158. """
  159. ce = CryptoEngine.get()
  160. print("Tags:")
  161. tags = self._db.listtags()
  162. for t in tags:
  163. print(ce.decrypt(t).decode())
  164. def _get_tags(self, default=None, reader=raw_input):
  165. """
  166. Read tags from user input.
  167. Tags are simply returned as a list
  168. """
  169. # TODO: add method to read tags from db, so they
  170. # could be used for tab completer
  171. print("Tags: ", end="")
  172. sys.stdout.flush()
  173. taglist = sys.stdin.readline()
  174. tagstrings = taglist.split()
  175. tags = [tn for tn in tagstrings]
  176. return tags
  177. def _prep_term(self):
  178. self.do_cls('')
  179. if sys.platform != 'win32':
  180. rows, cols = tools.gettermsize()
  181. else: # pragma: no cover
  182. rows, cols = 18, 80 # fix this !
  183. cols -= 8
  184. return rows, cols
  185. def _format_line(self, tag_pad, nid="ID", user="USER", url="URL",
  186. tags="TAGS"):
  187. return ("{ID:<3} {USER:<{us}}{URL:<{ur}}{Tags:<{tg}}"
  188. "".format(ID=nid, USER=user,
  189. URL=url, Tags=tags, us=12,
  190. ur=20, tg=tag_pad - 32))
  191. def _print_node_line(self, node, rows, cols):
  192. tagstring = ','.join([t.decode() for t in node.tags])
  193. fmt = self._format_line(cols - 32, node._id, node.username.decode(),
  194. node.url.decode(),
  195. tagstring)
  196. formatted_entry = tools.typeset(fmt, Fore.YELLOW, False)
  197. print(formatted_entry)
  198. def _get_node_ids(self, args):
  199. filter = None
  200. if args:
  201. filter = args.split()[0]
  202. ce = CryptoEngine.get()
  203. filter = ce.encrypt(filter)
  204. nodeids = self._db.listnodes(filter=filter)
  205. return nodeids
  206. def _db_entries_to_nodes(self, raw_nodes):
  207. _nodes_inst = []
  208. # user, pass, url, notes
  209. for node in raw_nodes:
  210. _nodes_inst.append(Node.from_encrypted_entries(
  211. node[1],
  212. node[2],
  213. node[3],
  214. node[4],
  215. node[5:]))
  216. _nodes_inst[-1]._id = node[0]
  217. return _nodes_inst
  218. def do_edit(self, args, menu=None):
  219. ids = self._get_ids(args)
  220. for i in ids:
  221. try:
  222. i = int(i)
  223. node = self._db.getnodes([i])[0]
  224. node = node[1:5] + [node[5:]]
  225. node = Node.from_encrypted_entries(*node)
  226. if not menu:
  227. menu = CMDLoop()
  228. print ("Editing node %d." % (i))
  229. menu.add(CliMenuItem("Username",
  230. self._get_input,
  231. node.username,
  232. node.username))
  233. menu.add(CliMenuItem("Password", self._get_secret,
  234. node.password,
  235. node.password))
  236. menu.add(CliMenuItem("Url", self._get_input,
  237. node.url,
  238. node.url))
  239. menunotes = CliMenuItem("Notes", self._get_input,
  240. node.notes,
  241. node.notes)
  242. menu.add(menunotes)
  243. tgetter = lambda: ', '.join(t for t in node.tags)
  244. menu.add(CliMenuItem("Tags", self._get_input,
  245. tgetter(),
  246. node.tags))
  247. menu.run(node)
  248. # _db.editnode does not recieve a node instance
  249. self._db.editnode(i, **node.to_encdict())
  250. # when done with node erase it
  251. zerome(node._password)
  252. except Exception as e:
  253. self.error(e)
  254. def do_list(self, args):
  255. """list all existing nodes in database"""
  256. rows, cols = self._prep_term()
  257. nodeids = self._get_node_ids(args)
  258. raw_nodes = self._db.getnodes(nodeids)
  259. _nodes_inst = self._db_entries_to_nodes(raw_nodes)
  260. head = self._format_line(cols-32)
  261. print(tools.typeset(head, Fore.YELLOW, False))
  262. for idx, node in enumerate(_nodes_inst):
  263. self._print_node_line(node, rows, cols)
  264. def _get_input(self, prompt):
  265. print(prompt, end="")
  266. sys.stdout.flush()
  267. return sys.stdin.readline().strip()
  268. def _get_secret(self):
  269. # TODO: enable old functionallity, with password generator.
  270. if sys.stdin.isatty(): # pragma: no cover
  271. p = getpass.getpass()
  272. else:
  273. p = sys.stdin.readline().rstrip()
  274. return p
  275. def _do_new(self, args):
  276. node = {}
  277. node['username'] = self._get_input("Username: ")
  278. node['password'] = self._get_secret()
  279. node['url'] = self._get_input("Url: ")
  280. node['notes'] = self._get_input("Notes: ")
  281. node['tags'] = self._get_tags()
  282. node = Node(clear_text=True, **node)
  283. self._db.add_node(node)
  284. return node
  285. def do_new(self, args): # pragma: no cover
  286. # The cmd module stops if and of do_* return something
  287. # else than None ...
  288. # This is bad for testing, so everything that is do_*
  289. # should call _do_* method which is testable
  290. self._do_new(args)
  291. def do_print(self, args):
  292. if not args.isdigit():
  293. print("print accepts only a single ID ...")
  294. return
  295. nodes = self._db.getnodes([args])
  296. node = self._db_entries_to_nodes(nodes)[0]
  297. print(node)
  298. flushtimeout = self.config.get_value('Global', 'cls_timeout')
  299. flushtimeout = flushtimeout or 10
  300. print("Type Enter to flush screen or wait %s sec. " % flushtimeout)
  301. _wait_until_enter(_heard_enter, float(flushtimeout))
  302. self.do_cls('')
  303. def _do_rm(self, args):
  304. for i in args.split():
  305. if not i.isdigit():
  306. print("%s is not a node ID" % i)
  307. return None
  308. for i in args.split():
  309. ans = tools.getinput(("Are you sure you want to delete node {}"
  310. " [y/N]?".format(i)))
  311. if ans.lower() == 'y':
  312. self._db.removenodes([i])
  313. def do_delete(self, args): # pragma: no cover
  314. CryptoEngine.get()
  315. self._do_rm(args)