From f0a972af06837d0bcb027c5bea58df32a04efa74 Mon Sep 17 00:00:00 2001 From: TLINDEN Date: Fri, 10 Feb 2012 20:38:15 +0100 Subject: [PATCH] ADDED: New backend added: NOTEDB::pwsafe3, which adds support to store notes in a Password Safe v3 database. FIXED: -d didn't work, because of a typo in mode assignment. --- Changelog | 10 + Makefile.PL | 6 +- NOTEDB.pm | 4 +- NOTEDB/pwsafe3.pm | 589 +++++++++++++++ README | 4 +- VERSION | 2 +- config/noterc | 38 +- note | 1817 +++++++++++++++++++++++++++++++++++++++++++++ note.pod | 7 +- 9 files changed, 2465 insertions(+), 12 deletions(-) create mode 100644 NOTEDB/pwsafe3.pm create mode 100755 note diff --git a/Changelog b/Changelog index 2e9a8e7..e1e9f08 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,13 @@ +1.3.8: +ADDED: New backend added: NOTEDB::pwsafe3, which adds support to store + notes in a Password Safe v3 database. +FIXED: -d didn't work, because of a typo in mode assignment. +================================================================================ +1.3.7: +ADDED: added ticket feature, which adds a unique id to each + new note, which persists during imports/exports. the + id is a randomly generated string. +================================================================================ 1.3.6: ADDED: Added test cases for "make test" ADDED: Added test for optional and required perl modules in diff --git a/Makefile.PL b/Makefile.PL index c3db63f..124e4d8 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -9,13 +9,14 @@ my %optional = ( 'Config::General' => "Required by general DB backend", 'DB_File' => "Required by dbm DB backend", 'DBI' => "Required by mysql DB backend", - 'DBD::mysql' => "Required by mysql DB backend" + 'DBD::mysql' => "Required by mysql DB backend", + 'Crypt::PWSafe3' => "Required by Password Safe v3 backend" ); foreach my $module (sort keys %optional) { eval "require $module"; if ($@) { - warn("Optional module $module no installed, $optional{$module}\n"); + warn("Optional module $module not installed, $optional{$module}\n"); } } @@ -33,6 +34,7 @@ WriteMakefile( 'Getopt::Long' => 0, 'Fcntl' => 0, 'IO::Seekable' => 0, + 'YAML' => 0, }, ($ExtUtils::MakeMaker::VERSION ge '6.31'? ('LICENSE' => 'perl', ) : ()), 'clean' => { FILES => 't/*.out t/test.cfg *~ */*~' } diff --git a/NOTEDB.pm b/NOTEDB.pm index c561e3c..6c0f91a 100644 --- a/NOTEDB.pm +++ b/NOTEDB.pm @@ -2,7 +2,7 @@ # this is a generic module, used by note database # backend modules. # -# Copyright (c) 2000-2004 Thomas Linden +# Copyright (c) 2000-2012 Thomas Linden package NOTEDB; @@ -10,7 +10,7 @@ package NOTEDB; use Exporter (); use vars qw(@ISA @EXPORT $crypt_supported); -$NOTEDB::VERSION = "1.31"; +$NOTEDB::VERSION = "1.32"; BEGIN { # make sure, it works, otherwise encryption diff --git a/NOTEDB/pwsafe3.pm b/NOTEDB/pwsafe3.pm new file mode 100644 index 0000000..31237e0 --- /dev/null +++ b/NOTEDB/pwsafe3.pm @@ -0,0 +1,589 @@ +# Perl module for note +# pwsafe3 backend. see docu: perldoc NOTEDB::pwsafe3 + +package NOTEDB::pwsafe3; + +$NOTEDB::pwsafe3::VERSION = "1.00"; + +use strict; +use Data::Dumper; +use Time::Local; +use Crypt::PWSafe3; + +use NOTEDB; + +use Fcntl qw(LOCK_EX LOCK_UN); + +use Exporter (); +use vars qw(@ISA @EXPORT); +@ISA = qw(NOTEDB Exporter); + + + + + +sub new { + my($this, %param) = @_; + + my $class = ref($this) || $this; + my $self = {}; + bless($self,$class); + + $self->{dbname} = $param{dbname} || File::Spec->catfile($ENV{HOME}, ".notedb"); + + $self->{mtime} = $self->get_stat(); + $self->{unread} = 1; + $self->{data} = {}; + $self->{LOCKFILE} = $param{dbname} . "~LOCK"; + + return $self; +} + + +sub DESTROY { + # clean the desk! +} + +sub version { + my $this = shift; + return $NOTEDB::pwsafe3::VERSION; +} + +sub get_stat { + my ($this) = @_; + my $mtime = (stat($this->{dbname}))[9]; + return $mtime; +} + +sub filechanged { + my ($this) = @_; + my $current = $this->get_stat(); + if ($current > $this->{mtime}) { + $this->{mtime} = $current; + return $current; + } + else { + return 0; + } +} + +sub set_del_all { + my $this = shift; + unlink $this->{dbname}; + open(TT,">$this->{dbname}") or die "Could not create $this->{dbname}: $!\n"; + close (TT); +} + + +sub get_single { + my($this, $num) = @_; + my($address, $note, $date, $buffer, $n, $t, $buffer, ); + + my %data = $this->get_all(); + + return ($data{$num}->{note}, $data{$num}->{date}); +} + + +sub get_all { + my $this = shift; + my($num, $note, $date, %res); + + if ($this->unchanged) { + return %{$this->{cache}}; + } + + my %data = $this->_retrieve(); + + foreach my $num (keys %data) { + ($res{$num}->{date}, $res{$num}->{note}) = $this->_pwsafe3tonote($data{$num}->{note}); + } + + $this->cache(%res); + return %res; +} + +sub import_data { + my ($this, $data) = @_; + + my $fh; + + if (-s $this->{dbname}) { + $fh = new FileHandle "<$this->{dbname}" or die "could not open $this->{dbname}\n"; + flock $fh, LOCK_EX; + } + + my $key = $this->_getpass(); + + eval { + my $vault = new Crypt::PWSafe3(password => $key, file => $this->{dbname}); + + foreach my $num (keys %{$data}) { + my $checksum = $this->get_nextnum(); + my %record = $this->_notetopwsafe3($checksum, $data->{$num}->{note}, $data->{$num}->{date}); + + my $rec = new Crypt::PWSafe3::Record(); + $rec->uuid($record{uuid}); + $vault->addrecord($rec); + $vault->modifyrecord($record{uuid}, %record); + } + + $vault->save(); + }; + if ($@) { + print "Exception caught:\n$@\n"; + exit 1; + } + + eval { + flock $fh, LOCK_UN; + $fh->close(); + }; + + $this->{keepkey} = 0; + $this->{key} = 0; +} + +sub get_nextnum { + my $this = shift; + my($num, $te, $me, $buffer); + + my $ug = new Data::UUID; + + $this->{nextuuid} = unpack('H*', $ug->create()); + $num = $this->_uuid( $this->{nextuuid} ); + + return $num; +} + +sub get_search { + my($this, $searchstring) = @_; + my($buffer, $num, $note, $date, %res, $t, $n, $match); + + my $regex = $this->generate_search($searchstring); + eval $regex; + if ($@) { + print "invalid expression: \"$searchstring\"!\n"; + return; + } + $match = 0; + + if ($this->unchanged) { + foreach my $num (keys %{$this->{cache}}) { + $_ = $this->{cache}{$num}->{note}; + eval $regex; + if ($match) { + $res{$num}->{note} = $this->{cache}{$num}->{note}; + $res{$num}->{date} = $this->{cache}{$num}->{date} + } + $match = 0; + } + return %res; + } + + my %data = $this->get_all(); + + foreach my $num(sort keys %data) { + $_ = $data{$num}->{note}; + eval $regex; + if($match) + { + $res{$num}->{note} = $data{$num}->{note}; + $res{$num}->{date} = $data{$num}->{data}; + } + $match = 0; + } + + return %res; +} + + + + +sub set_edit { + my($this, $num, $note, $date) = @_; + + my %data = $this->_retrieve(); + + my %record = $this->_notetopwsafe3($num, $note, $date); + + if (exists $data{$num}) { + $data{$num}->{note} = \%record; + $this->_store(\%record); + } + else { + %record = $this->_store(\%record, 1); + } + + $this->changed; +} + + +sub set_new { + my($this, $num, $note, $date) = @_; + $this->set_edit($num, $note, $date); +} + + +sub set_del { + my($this, $num) = @_; + + my $fh = new FileHandle "<$this->{dbname}" or die "could not open $this->{dbname}\n"; + flock $fh, LOCK_EX; + + my $key = $this->_getpass(); + eval { + my $vault = new Vault($key, $this->{dbname}); + $vault->delrecord($this->_getuuid($num)); + + $vault->write_to_file($this->{dbname}, $key); + }; + if ($@) { + print "Exception caught:\n$@\n"; + exit 1; + } + flock $fh, LOCK_UN; + $fh->close(); + + # finally re-read the db, so that we always have the latest data + $this->_retrieve($key); + $this->changed; + return; +} + +sub set_recountnums { + my($this) = @_; + # unsupported + return; +} + + +sub _store { + my ($this, $record, $create) = @_; + + my $fh; + + if (-s $this->{dbname}) { + $fh = new FileHandle "<$this->{dbname}" or die "could not open $this->{dbname}\n"; + flock $fh, LOCK_EX; + } + + my $key = $this->_getpass(); + eval { + my $vault = new Crypt::PWSafe3(password => $key, file => $this->{dbname}); + if ($create) { + my $rec = new Crypt::PWSafe3::Record(); + $rec->uuid($record->{uuid}); + $vault->addrecord($rec); + $vault->modifyrecord($record->{uuid}, %{$record}); + } + else { + $vault->modifyrecord($record->{uuid}, %{$record}); + } + $vault->save(); + }; + if ($@) { + print "Exception caught:\n$@\n"; + exit 1; + } + + eval { + flock $fh, LOCK_UN; + $fh->close(); + }; + + # finally re-read the db, so that we always have the latest data + $this->_retrieve($key); +} + +sub _retrieve { + my ($this, $key) = @_; + my $file = $this->{dbname}; + if (-s $file) { + if ($this->filechanged() || $this->{unread}) { + my $fh = new FileHandle "<$this->{dbname}" or die "could not open $this->{dbname}\n"; + flock $fh, LOCK_EX; + + my %data; + if (! $key) { + $key = $this->_getpass(); + } + eval { + my $vault = new Crypt::PWSafe3(password => $key, file => $this->{dbname}); + + my @records = $vault->getrecords(); + + foreach my $record (@records) { + my $num = $this->_uuid( $record->uuid ); + my %entry = ( + uuid => $record->uuid, + title => $record->title, + user => $record->user, + passwd => $record->passwd, + notes => $record->notes, + group => $record->group, + lastmod=> $record->lastmod, + ); + $data{$num}->{note} = \%entry; + } + }; + if ($@) { + print "Exception caught:\n$@\n"; + exit 1; + } + + flock $fh, LOCK_UN; + $fh->close(); + + $this->{unread} = 0; + $this->{data} = \%data; + return %data; + } + else { + return %{$this->{data}}; + } + } + else { + return (); + } +} + +sub _pwsafe3tonote { + # + # convert pwsafe3 record to note record + my ($this, $record) = @_; + my $date = scalar localtime($record->{lastmod}); + chomp $date; + my $note; + if ($record->{group}) { + my $group = $record->{group}; + # convert group separator + $group =~ s#\.#/#g; + $note = "/$group/\n"; + } + + # pwsafe3 uses windows newlines, so convert ours + $record->{notes} =~ s/\r\n/\n/gs; + + # + # we do NOT add user and password fields here extra + # because if it is contained in the note, from were + # it was extracted initially, where it remains anyway + $note .= "$record->{title}\n$record->{notes}"; + + return ($date, $note); +} + +sub _notetopwsafe3 { + # + # convert note record to pwsafe3 record + # only used on create or save + # + # this one is the critical part, because the two + # record types are fundamentally incompatible. + # we parse our record and try to guess the values + # required for pwsafe3 + # + # expected input for note: + # /path/ -> group, optional + # any text -> title + # User: xxx -> user + # Password: xxx -> passwd + # anything else -> notes + # + # expected input for date: + # 23.02.2010 07:56:27 + my ($this, $num, $text, $date) = @_; + my ($group, $title, $user, $passwd, $notes, $ts, $content); + if ($text =~ /^\//) { + ($group, $title, $content) = split /\n/, $text, 3; + } + else { + ($title, $content) = split /\n/, $text, 2; + } + + $user = $passwd = ''; + if ($content =~ /(user|username|login|account|benutzer):\s*(.+)/i) { + $user = $2; + } + if ($content =~ /(password|pass|passwd|kennwort|pw):\s*(.+)/i) { + $passwd = $2; + } + + # 1 2 3 5 6 7 + if ($date =~ /^(\d\d)\.(\d\d)\.(\{4}) (\d\d):(\d\d):(\d\d)$/) { + $ts = timelocal($7, $6, $5, $1, $2-1, $3-1900); + } + + # make our topics pwsafe3 compatible groups + $group =~ s#^/##; + $group =~ s#/$##; + $group =~ s#/#.#g; + + # pwsafe3 uses windows newlines, so convert ours + $content =~ s/\n/\r\n/gs; + + my %record = ( + uuid => $this->_getuuid($num), + user => $user, + passwd => $passwd, + group => $group, + title => $title, + lastmod=> $ts, + notes => $content, + ); + + return %record; +} + +sub _uuid { + # + # Convert a given pwsafe3 uuid to a number + # and store them for recursive access + my ($this, $uuid) = @_; + if (exists $this->{uuidnum}->{$uuid}) { + return $this->{uuidnum}->{$uuid}; + } + + my $intuuid = $uuid; + $intuuid =~ s/[\-]//g; + $intuuid = unpack('h32', $intuuid); + my $cuid = $intuuid; + my $checksum; + while () { + $checksum = unpack("%16C*", $cuid) - 1600; + while ($checksum < 0) { + $checksum++; # avoid negative numbers + } + if (! exists $this->{numuuid}->{$checksum}) { + $this->{uuidnum}->{$uuid} = $checksum; + $this->{numuuid}->{$checksum} = $uuid; + last; + } + else { + $cuid .= $.; + } + } + return $checksum; +} + +sub _getuuid { + my ($this, $num) = @_; + return $this->{numuuid}->{$num}; +} + +sub _getpass { + # + # We're doing this here ourselfes + # because the note way of handling encryption + # doesn't work with pwsafe3, we can't hold a cipher + # structure in memory, because pwsafe3 handles this + # itself. + # Instead we ask for the password everytime we want + # to fetch data from the actual file OR want to write + # to it. To minimize reads, we use caching by default. + my($this) = @_; + if ($this->{key}) { + return $this->{key}; + } + else { + my $key; + print "password: "; + eval { + local($|) = 1; + local(*TTY); + open(TTY,"/dev/tty") or die "No /dev/tty!"; + system ("stty -echo ); + print STDERR "\r\n"; + system ("stty echo ; + } + if ($this->{keepkey}) { + $this->{key} = $key; + } + return $key; + } +} + +1; # keep this! + +__END__ + +=head1 NAME + +NOTEDB::pwsafe3 - module lib for accessing a notedb from perl + +=head1 SYNOPSIS + + # include the module + use NOTEDB; + + # create a new NOTEDB object + $db = new NOTEDB("text", "/home/tom/.notedb", 4096, 24); + + # decide to use encryption + # $key is the cipher to use for encryption + # $method must be either Crypt::IDEA or Crypt::DES + # you need Crypt::CBC, Crypt::IDEA and Crypt::DES to have installed. + $db->use_crypt($key,$method); + + # do not use encryption + # this is the default + $db->no_crypt; + + # get a single note + ($note, $date) = $db->get_single(1); + + # search for a certain note + %matching_notes = $db->get_search("somewhat"); + # format of returned hash: + #$matching_notes{$numberofnote}->{'note' => 'something', 'date' => '23.12.2000 10:33:02'} + + # get all existing notes + %all_notes = $db->get_all(); + # format of returnes hash like the one from get_search above + + # get the next noteid available + $next_num = $db->get_nextnum(); + + # modify a certain note + $db->set_edit(1, "any text", "23.12.2000 10:33:02"); + + # create a new note + $db->set_new(5, "any new text", "23.12.2000 10:33:02"); + + # delete a certain note + $db->set_del(5); + + # turn on encryption. CryptMethod must be IDEA, DES or BLOWFISH + $db->use_crypt("passphrase", "CryptMethod"); + + # turn off encryption. This is the default. + $db->no_crypt(); + + +=head1 DESCRIPTION + +You can use this module for accessing a note database. This backend uses +a text file for storage and Config::General for accessing the file. + +Currently, NOTEDB module is only used by note itself. But feel free to use it +within your own project! Perhaps someone want to implement a webinterface to +note... + +=head1 USAGE + +please see the section SYNOPSIS, it says it all. + +=head1 AUTHOR + +Thomas Linden + + +=cut + + diff --git a/README b/README index 5ef2c87..e1564ca 100644 --- a/README +++ b/README @@ -1,4 +1,4 @@ -note 1.3.5 by Thomas Linden, 07/19/2009 +note 1.3.8 by Thomas Linden, 02/01/2012 ======================================= Introduction @@ -214,4 +214,4 @@ and I'll add you. Last changed ============ -07/19/2009 +02/01/2012 diff --git a/VERSION b/VERSION index 80e78df..e05cb33 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.5 +1.3.8 diff --git a/config/noterc b/config/noterc index 5411cad..cc22abe 100644 --- a/config/noterc +++ b/config/noterc @@ -1,11 +1,11 @@ - # note 1.3.5 -*- sh -*- + # note 1.3.8 -*- sh -*- # # This is a sample config for the note script # There are useful defaults set in note itself. # # Copy it to your $HOME as .noterc # - # note is Copyright (c) 1999-2009 Thomas Linden. + # note is Copyright (c) 1999-2012 Thomas Linden. # You can contact me per email: # # Comments start with #, empty lines will be ignored. @@ -29,7 +29,7 @@ # to use. Please refer to the corresponding documentation # for closer information about the certain backend! # Currently supported types: "binary", "dbm", "mysql", - # "general" or "text". + # "general", "dumper", "pwsafe3" or "text". # You must also edit/uncomment one section below for the # backend you want to use! dbdriver = binary @@ -66,11 +66,41 @@ dbm::directory = ~/.notedbm # directory general::dbname = ~/.notedb # filename - # + # # TEXT backend text::dbname = ~/.notedb # filename + # + # DUMPER backend +dumper::dbname = ~/.notedb # filename + + + + # + # Password Safe v3 backend + # Some notes on this one: + # This backend maintains encryption itself, which is + # mandatory as well. So you'll have to disable encryption + # (UseEncryption = NO)! + # + # The Password Safe v3 file has its own fields for + # password and username, which note doesn't have. To be + # compatible, the pwsafe3 backend parses the note text + # for those fields and stores them accordignly to the db: + # + # For username field: user|username|login|account|benutzer + # For passwd field: password|pass|passwd|kennwort|pw + # + # If it doesn't find it, it will put empty strings + # into the pwsafe3 database. + # + # The pwsafe3 database can be accessed by Password Safe + # (see: http://passwordsafe.sourceforge.net/) or other + # tools which support the format (see: + # http://passwordsafe.sourceforge.net/relatedprojects.shtml) +pwsafe3::dbname = ~/db.psafe3 # filename + # # You can use encryption with note, that means notes and diff --git a/note b/note new file mode 100755 index 0000000..8b1b3aa --- /dev/null +++ b/note @@ -0,0 +1,1817 @@ +#!/usr/bin/perl +# +# note - console notes management with database and encryption support. +# Copyright (C) 1999-2012 Thomas Linden (see README for details!) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# - Thomas Linden +# +# latest version on: +# http://www.daemon.de/note/ +# + +use lib qw(.); + +BEGIN { + # works on unix or cygwin only! + my $path = $0; + $path =~ s#/[^/]*$##; + unshift @INC, "$path/.."; +} + +use strict; +no strict 'refs'; +use Getopt::Long; +use FileHandle; +use File::Spec; +use Data::Dumper; +use YAML; + +# +# prototypes +# +sub usage; # print usage message for us thumb userz :-) +sub find_editor; # returns an external editor for use +sub output; # used by &list and &display +sub C; # print colourized +sub num_bereich; # returns array from "1-4" (1,2,3,4) +sub getdate; # return pretty formatted day +sub new; # crate new note +sub edit; # edit a note +sub del; # delete a note +sub display; # display one or more notes +sub list; # note-listing +sub help; # interactive help screen +sub import; # import from notedb-dump +sub display_tree; # show nice tree-view +sub tree; # build the tree +sub print_tree; # print the tree, contributed by Jens Heunemann . THX! +sub ticket; # return a random string which is used as ticket number for new note entries + +# +# globals +# +my ( + # + # commandline options + # + $opt_, $opt_i, $opt_r, $opt_e, $opt_d, $opt_enc, + $opt_s, $opt_t, $opt_T, $opt_l, $opt_L, $opt_c, + $opt_D, $opt_I, $opt_o, $opt_h, $opt_n, $opt_v, + + # + # set from commandline (or interactive) + # + $number, $searchstring, $dump_file, $ImportType, $NewType, $Raw, $TOPIC, + + # + # configuration options + %conf, %driver, + + # + # processed colors + # + $BORDERC, $_BORDERC, $NOTEC, $NUMC, $_NUMC, $_NOTEC, $TIMEC, + $_TIMEC, $TOPICC, $_TOPICC, + + # + # config presets + # + $DEFAULTDBNAME, $USER, $PATH, $CONF, + + # + # internals + # + $TYPE, $mode, $NoteKey, %Color, @LastTopic, $timelen, $maxlen, + $version, $CurTopic, $CurDepth, $WantTopic, $db, + $sizeof, %TP, $TreeType, $ListType, $SetTitle, $clearstring, + @ArgTopics, $key, $typedef, @NumBlock, $has_nothing, @completion_topics, @completion_notes, + @randomlist, $hardparams + ); + + +# +# DEFAULTS, allows one to use note without a config +# don't change them, instead use the config file! +# + +%conf = ( + 'numbercolor' => 'blue', + 'bordercolor' => 'black', + 'timecolor' => 'black', + 'topiccolor' => 'black', + 'notecolor' => 'green', + 'alwaysinteractive' => 1, + 'keeptimestamp' => 0, + 'readonly' => 0, + 'shortcd' => 1, + 'autoclear' => 0, + 'maxlen' => 'auto', + 'defaultlong' => 0, + 'dbdriver' => 'binary', + 'timeformat' => 'DD.MM.YYYY hh:mm:ss', + 'usecolors' => 0, + 'addticket' => 0, + 'formattext' => 0, + 'alwayseditor' => 1, + 'useencryption' => 0, + 'tempdirectory' => File::Spec->tmpdir(), + 'topicseparator' => '/', + 'printlines' => 0, + 'cache' => 0, + 'preferrededitor' => '' +); + +# these are not customizable at runtime! +$hardparams = "(readonly|maxlen|dbdriver|useencryption|cryptmethod)"; + +$CONF = File::Spec->catfile($ENV{HOME}, ".noterc"); +$USER = getlogin || getpwuid($<); +chomp $USER; +$TOPIC = 1; +$version = "1.3.8"; +$CurDepth = 1; # the current depth inside the topic "directory" structure... +$maxlen = "auto"; +$timelen = 22; + +@randomlist = ('a'..'z', 0..9, 'A'..'Z'); + +# colors available +# \033[1m%30s\033[0m +%Color = ( 'black' => '0;30', + 'red' => '0;31', + 'green' => '0;32', + 'yellow' => '0;33', + 'blue' => '0;34', + 'magenta' => '0;35', + 'cyan' => '0;36', + 'white' => '0;37', + 'B' => '1;30', + 'BLACK' => '1;30', + 'RED' => '1;31', + 'GREEN' => '1;32', + 'YELLOW' => '1;33', + 'BLUE' => '1;34', + 'MAGENTA' => '1;35', + 'CYAN' => '1;36', + 'WHITE' => '1;37', + 'black_' => '4;30', + 'red_' => '4;31', + 'green_' => '4;32', + 'yellow_' => '4;33', + 'blue_' => '4;34', + 'magenta_' => '4;35', + 'cyan_' => '4;36', + 'white_' => '4;37', + 'blackI' => '7;30', + 'redI' => '7;31', + 'greenI' => '7;32', + 'yellowI' => '7;33', + 'blueI' => '7;34', + 'magentaI' => '7;35', + 'cyanI' => '7;36', + 'whiteI' => '7;37', + 'white_black' => '40;37;01', + 'bold' => ';01', + 'hide' => '44;34' + ); + +# +# process command line args +# +if ($ARGV[0] eq "") { + $mode = "new"; +} +elsif ($#ARGV == 0 && $ARGV[0] eq "-") { + $mode = "new"; + $NewType = 1; # read from STDIN until EOF + shift; + undef $has_nothing; +} +else { + Getopt::Long::Configure( qw(no_ignore_case)); + GetOptions ( + "interactive|i!" => \$opt_i, # no arg + "config|c=s" => \$opt_c, # string, required + "raw|r!" => \$opt_r, # no arg + "edit|e=i" => \$opt_e, # integer, required + "delete|d=s" => \$opt_d, # integer, required + "search|s=s" => \$opt_s, # string, required + "tree|topic|t!" => \$opt_t, # no arg + "longtopic|T!" => \$opt_T, # no arg + "list|l:s" => \$opt_l, # string, optional + "longlist|L:s" => \$opt_L, # string, optional + "dump|Dump|D:s" => \$opt_D, # string, optional + "import|Import|I:s" => \$opt_I, # string, optional + "overwrite|o!" => \$opt_o, # no arg + "help|h|?!" => \$opt_h, # no arg + "version|v!" => \$opt_v, # no arg + "encrypt=s" => \$opt_enc, # string, required + ); + $opt_n = shift; # after that @ARGV contains eventually + # a note-number + # $opt_ is a single dash, in case of existence! + # + # determine mode + # + if ($opt_i) { + $mode = "interactive"; + } + elsif (defined $opt_l || defined $opt_L) { + $mode = "list"; + if (defined $opt_l) { + @ArgTopics = split /$conf{topicseparator}/, $opt_l; + } + else { + $ListType = "LONG"; + @ArgTopics = split /$conf{topicseparator}/, $opt_L; + } + $CurDepth += $#ArgTopics + 1 if($opt_l || $opt_L); + $CurTopic = $ArgTopics[$#ArgTopics]; # use the last element everytime... + } + elsif ($opt_t || $opt_T) { + $mode = "tree"; + $mode = "display_tree"; + $TreeType = "LONG" if($opt_T); + } + elsif (defined $opt_s) { + $mode = "search"; + $searchstring = $opt_s; + } + elsif ($opt_e) { + $mode = "edit"; + $number = $opt_e; + } + elsif ($opt_d) { + $mode = "del"; + $number = $opt_d; + } + elsif ($opt_enc) { + $mode = "encrypt_passwd"; + $clearstring = $opt_enc; + } + elsif (defined $opt_D) { + $mode = "dump"; + if (!$opt_) { + if ($opt_D ne "") { + $dump_file = $opt_D; + } + else { + $dump_file = "note.dump.$$"; + print "no dumpfile specified, using $dump_file.\n"; + } + } + else { + $dump_file = "-"; # use STDIN + } + } + elsif (defined $opt_I) { + $mode = "import"; + if (!$opt_) { + if ($opt_I ne "") { + $dump_file = $opt_I; + } + else { + print "Import-error! No dump_file specified!\n"; + exit(1); + } + } + else { + $dump_file = "-"; + } + } + elsif ($opt_v) { + print "This is note $version by Thomas Linden .\n"; + exit(0); + } + elsif ($opt_h) { + &usage; + } + else { + if ($opt_c && $mode eq "" && !$opt_n) { + $mode = "new"; + } + elsif ($opt_c && $mode eq "") { + $mode = ""; # huh?! + } + else { + $has_nothing = 1; + } + } + ### determine generic options + if ($opt_n =~ /^[\d+\-?\,*]+$/) { + # first arg is a digit! + if ($mode eq "") { + $number = $opt_n; + $mode = "display"; + undef $has_nothing; + } + else { + print "mode <$mode> does not take a numerical argument!\n"; + exit(1); + } + } + elsif ($opt_n ne "") { + print "Unknown option: $opt_n\n"; + &usage; + } + if ($opt_r) { + $Raw = 1; + } + if ($opt_o) { + $ImportType = "overwrite"; + if (!$opt_I) { + print "--overwrite is only suitable for use with --import!\n"; + exit(1); + } + } + ##### +} +if ($has_nothing && $mode eq "") { + &usage; +} + + +# read the configfile. +$CONF = $opt_c if($opt_c); # if given by commandline, use this. +if (-e $CONF) { + &getconfig($CONF); +} +elsif ($opt_c) { + # only wrong, if specified by commandline! else use default values! + print STDERR "Could not open \"$CONF\": file does not exist or permission denied!\n"; + exit(1); +} + +# directly jump to encrypt, 'cause this sub does +# not require a database connection +if ($mode eq "encrypt_passwd") { + &encrypt_passwd; + exit; +} + +# Always interactive? +if ($conf{alwaysinteractive} && $mode ne "dump" && $mode ne "import") { + $mode = "interactive"; +} + +# OK ... Long-Listing shall be default ... You wanted it!!! +if ($conf{defaultlong}) { + # takes only precedence in commandline mode + $ListType="LONG"; +} + + + + +# calculate some constants... +$BORDERC = "<$conf{bordercolor}>"; +$_BORDERC = ""; +$NUMC = "<$conf{numbercolor}>"; +$_NUMC = ""; +$NOTEC = "<$conf{notecolor}>"; +$_NOTEC = ""; +$TIMEC = "<$conf{timecolor}>"; +$_TIMEC = ""; +$TOPICC = "<$conf{topiccolor}>"; +$_TOPICC = ""; + +$NoteKey = $conf{topicseparator} . "notes" . $conf{topicseparator}; + + + + +# default permissions on new files (tmp) +umask 077; + + +# load the parent module +&load_driver(1); + +# check wether the user wants to use encryption: +if ($conf{useencryption} && $NOTEDB::crypt_supported == 1) { + if ($conf{cryptmethod} eq "") { + $conf{cryptmethod} = "Crypt::IDEA"; + } + if (!exists $ENV{'NOTE_PASSWD'}) { + print "password: "; + eval { + local($|) = 1; + local(*TTY); + open(TTY,"/dev/tty") or die "No /dev/tty!"; + system ("stty -echo ); + print STDERR "\r\n"; + system ("stty echo ; + } + } + else { + $key = $ENV{'NOTE_PASSWD'}; + } + chomp $key; + if ($conf{dbdriver} eq "mysql") { + eval { + require Crypt::CBC; + my $cipher = new Crypt::CBC($key, $conf{cryptmethod}); + # decrypt the dbpasswd, if it's encrypted! + $driver{mysql}->{dbpasswd} = + $cipher->decrypt(unpack("u", $driver{mysql}->{dbpasswd})) if($driver{mysql}->{encrypt_passwd}); + &load_driver(); + }; + die "Could not connect to db: $@!\n" if($@); + } + else { + &load_driver(); + } + $db->use_crypt($key,$conf{cryptmethod}); + undef $key; + # verify correctness of passwd + my ($cnote, $cdate) = $db->get_single(1); + if ($cdate ne "") { + if ($cdate !~ /^\d+\.\d+?/) { + print "access denied.\n"; # decrypted $date is not a number! + exit(1); + } + } #else empty database! +} +elsif ($conf{useencryption} && $NOTEDB::crypt_supported == 0) { + print STDERR "WARNING: You enabled database encryption but neither Crypt::CBC\n"; + print STDERR "WARNING: or Crypt::$conf{cryptmethod} are installed! Please turn\n"; + print STDERR "WARNING: off encryption or install the desired modules! Thanks!\n"; + exit 1; +} +else { + # as of 1.3.5 we do not fall back to cleartext anymore + # I consider this as unsecure, if you don't, fix your installation! + + &load_driver(); + $db->no_crypt; + + # does: NOTEDB::crypt_supported = 0; + my ($cnote, $cdate) = $db->get_single(1); + if ($cdate ne "") { + if ($cdate !~ /^\d+\.\d+?/) { + print "notedb seems to be encrypted!\n"; + exit(1); + } + } +} + + +# do we use the db cache? +if ($conf{cache}) { + $db->use_cache(); +} + + +# add the backend version to the note version: +$version .= ", " . $conf{dbdriver} . " " . $db->version(); + + +# main loop: ############### +&$mode; +exit(0); +################## EOP ################ + + + + + + + + + + + + + + +############ encrypt a given password ############## +sub encrypt_passwd { + my($key, $crypt_string); + print "password: "; + eval { + local($|) = 1; + local(*TTY); + open(TTY,"/dev/tty") or die "No /dev/tty!"; + system ("stty -echo ); + print STDERR "\r\n"; + system ("stty echo ; + } + chomp $key; + eval { + require Crypt::CBC; + my $cipher = new Crypt::CBC($key, $conf{cryptmethod}); + $crypt_string = pack("u", $cipher->encrypt($clearstring)); + }; + if ($@) { + print "Something went wrong: $@\n"; + exit 1; + } else { + print "Encrypted password:\n$crypt_string\n"; + } +} + + +############################### DISPLAY ################################## +sub display { + my($N,$match,$note,$date,$num); + # display a certain note + print "\n"; + &num_bereich; # get @NumBlock from $numer + my $count = scalar @NumBlock; + foreach $N (@NumBlock) { + ($note, $date) = $db->get_single($N); + if ($note) { + if ($Raw) { + print "$N\n$date\n$note\n\n"; + } + else { + output($N, $note, $date, "SINGLE", $count); + print "\n"; + } + $match = 1; + } + $count--; + } + if (!$match) { + print "no note with that number found!\n"; + } + } + +############################### SEARCH ################################## +sub search { + my($n,$match,$note,$date,$num,%res); + if ($searchstring eq "") { + print "No searchstring specified!\n"; + } + else { + print "searching the database $conf{dbname} for \"$searchstring\"...\n\n"; + + %res = $db->get_search($searchstring); + my $nummatches = scalar keys %res; + foreach $num (sort { $a <=> $b } keys %res) { + if ($nummatches == 1) { + output($num, $res{$num}->{'note'}, $res{$num}->{'date'}, "SINGLE"); + } + else { + output($num, $res{$num}->{'note'}, $res{$num}->{'date'}, "search"); + } + $match = 1; + } + if (!$match) { + print "no matching note found!\n"; + } + print "\n"; + } + } + + +############################### LIST ################################## +sub list { + my(@topic,@RealTopic, $i,$t,$n,$num,@CurItem,$top,$in,%res); + if ($mode ne "interactive" && !$Raw) { + print "\nList of all existing notes:\n\n"; + } + else { + print "\n"; + } + + # list all available notes (number and firstline) + %res = $db->get_all(); + + if ($TOPIC) { + undef %TP; + } + + foreach $num (sort { $a <=> $b } keys %res) { + $n = $res{$num}->{'note'}; + $t = $res{$num}->{'date'}; + if ($TOPIC) { + # this allows us to have multiple topics (subtopics!) + my ($firstline,$dummy) = split /\n/, $n, 2; + if ($firstline =~ /^($conf{topicseparator})/) { + @topic = split(/$conf{topicseparator}/,$firstline); + } + else { + @topic = (); + } + # looks like: "\topic\" + # collect a list of topics under the current topic + if ($topic[$CurDepth-1] eq $CurTopic && $topic[$CurDepth] ne "") { + if (exists $TP{$topic[$CurDepth]}) { + $TP{$topic[$CurDepth]}++; + } + else { + # only if the next item *is* a topic! + $TP{$topic[$CurDepth]} = 1 if(($CurDepth) <= $#topic); + } + } + elsif ($topic[$CurDepth-1] eq $CurTopic || ($topic[$CurDepth] eq "" && $CurDepth ==1)) { + # cut the topic off the note-text + if ($n =~ /^($conf{topicseparator})/) { + $CurItem[$i]->{'note'} = $dummy; + } + else { + $CurItem[$i]->{'note'} = $n; + } + # save for later output() call + $CurItem[$i]->{'num'} = $num; + $CurItem[$i]->{'time'} = $t; + $i++; + # use this note for building the $PATH! + if ($RealTopic[0] eq "") { + @RealTopic = @topic; + } + } + } + else { + output($num, $n, $t); + } + } + if ($TOPIC) { + if ($CurTopic ne "") { + if ($i) { + # only if there were notes under current topic + undef $PATH; + foreach (@RealTopic) { + $PATH .= $_ . $conf{topicseparator}; + last if($_ eq $CurTopic); + } + } + else { + # it is an empty topic, no notes here + $PATH = join $conf{topicseparator}, @LastTopic; + $PATH .= $conf{topicseparator} . $CurTopic . $conf{topicseparator}; + $PATH =~ s/^\Q$conf{topicseparator}$conf{topicseparator}\E/$conf{topicseparator}/; + } + } + else { + $PATH = $conf{topicseparator}; + } + + @completion_topics = (); + @completion_notes = (); + # we are at top level, print a list of topics... + foreach $top (sort(keys %TP)) { + push @completion_topics, $top; + output("-", " => ". $top . "$conf{topicseparator} ($TP{$top} notes)", + " Sub Topic "); + } + #print Dumper(@CurItem); + for ($in=0;$in<$i;$in++) { + push @completion_notes, $CurItem[$in]->{'num'}; + output( $CurItem[$in]->{'num'}, + $CurItem[$in]->{'note'}, + $CurItem[$in]->{'time'} ); + } + } + + print "\n"; + } + +############################### NEW ################################## +sub new { + my($TEMP,$editor, $date, $note, $WARN, $c, $line, $num, @topic); + if ($conf{readonly}) { + print "readonly\n"; + return; + } + $date = &getdate; + return if $db->lock(); + if ($conf{alwayseditor}) { + $TEMP = &gettemp; + # security! + unlink $TEMP; + # let the user edit it... + $editor = &find_editor; + if ($editor) { + # create the temp file + open NEW, "> $TEMP" or die "Could not write $TEMP: $!\n"; + close NEW; + system "chattr", "+s", $TEMP; # ignore errors, since only on ext2 supported! + system $editor, $TEMP; + } + else { + print "Could not find an editor to use!\n"; + $db->unlock(); + exit(0); + } + # read it in ($note) + $note = ""; + open E, "<$TEMP" or $WARN = 1; + if ($WARN) { + print "...edit process interupted! No note has been saved.\n"; + undef $WARN; + $db->unlock(); + return; + } + $c = 0; + while () { + $note = $note . $_; + } + chomp $note; + close E; + # privacy! + unlink $TEMP; + } + else { + $note = ""; + $line = ""; + # create a new note + if ($NewType) { + # be silent! read from STDIN until EOF. + while () { + $note .= $_; + } + } + else { + print "enter the text of the note, end with a single .\n"; + do + { + $line = ; + $note = $note . $line; + } until $line eq ".\n"; + # remove the . ! + chop $note; + chop $note; + } + } + # look if the note was empty, so don't store it! + if ($note =~ /^\s*$/) { + print "...your note was empty and will not be saved.\n"; + $db->unlock(); + return; + } + # since we have not a number, look for the next one available: + $number = $db->get_nextnum(); + if ($TOPIC && $CurTopic ne "") { + @topic = split(/$conf{topicseparator}/,$note); + if ($topic[1] eq "") { + $note = $PATH . "\n$note"; + } + } + $note = &add_ticket($note); + + $db->set_new($number,$note,$date); + # everything ok until here! + print "note stored. it has been assigned the number $number.\n\n"; + $db->unlock(); + } + +sub add_ticket { + my $orignote = shift; + if ($conf{addticket}) { + my ($topic, $title, $rest) = split /\n/, $orignote, 3; + my $note = ""; + if ($topic =~ /^\//) { + # topic path, keep it + $note .= "$topic\n"; + } + else { + # no topic + $rest = "$title\n$rest"; + $title = $topic; + } + if ($title !~ /^\[[a-z0-9A-Z]+\]/) { + # no ticket number, so create one + my $ticket = &ticket(); + $title = "[" . ticket() . "] " . $title; + } + $note .= "$title\n$rest"; + return $note; + } + else { + return $orignote; + } +} + + +############################### DELETE ################################## +sub del { + my($i,@count, $setnum, $pos, $ERR); + if ($conf{readonly}) { + print "readonly\n"; + return; + } + # delete a note + &num_bereich; # get @NumBlock from $number + + return if $db->lock(); + + foreach $_ (@NumBlock) { + $ERR = $db->set_del($_); + if ($ERR) { + print "no note with number $_ found!\n"; + } + else { + print "note number $_ has been deleted.\n"; + } + } + # recount the notenumbers: + $db->set_recountnums(); + + $db->unlock(); + @NumBlock = (); + } + +############################### EDIT ################################## +sub edit { + my($keeptime, $date, $editor, $TEMP, $note, $t, $num, $match, $backup); + if ($conf{readonly}) { + print "readonly\n"; + return; + } + # edit a note + $date = &getdate; + + return if $db->lock(); + + ($note, $keeptime) = $db->get_single($number); + if ($keeptime eq "") { + print "no note with that number found ($number)!\n\n"; + if($mode ne "interactive") { + $db->unlock(); + exit(0); + } + else { + $db->unlock(); + return; + } + } + + $TEMP = &gettemp; + open NOTE,">$TEMP" or die "Could not open $TEMP\n"; + select NOTE; + + system "chattr", "+s", $TEMP; # ignore errors, like in new() + + print $note; + close NOTE; + select STDOUT; + $editor = &find_editor; + + $backup = $note; + + if ($editor) { + system ($editor, $TEMP) and die "Could not execute $editor: $!\n"; + } + else { + print "Could not find an editor to use!\n"; + exit(0); + } + $note = ""; + open NOTE,"<$TEMP" or die "Could not open $TEMP\n"; + + while () { + $note = $note . $_; + } + chomp $note; + close NOTE; + + unlink $TEMP || die $!; + + if ($note ne $backup) { + if ($conf{keeptimestamp}) { + $t = $keeptime; + } + else { + $t = $date; + } + # we got it, now save to db + $db->set_edit($number, $note, $t); + + print "note number $number has been changed.\n"; + } + else { + print "note number $number has not changed, no save done.\n"; + } + $db->unlock(); + } + + +sub dump { + my(%res, $num, $DUMP); + # $dump_file + if ($dump_file eq "-") { + $DUMP = *STDOUT; + } + else { + open (DUMPFILE, ">$dump_file") or die "could not open $dump_file\n"; + $DUMP = *DUMPFILE; + } + select $DUMP; + %res = $db->get_all(); + # FIXME: prepare hashing in NOTEDB class + foreach $num (sort { $a <=> $b } keys %res) { + print STDOUT "dumping note number $num to $dump_file\n" if($dump_file ne "-"); + my($title, $path, $body); + if ($res{$num}->{note} =~ /^\//) { + ($path, $title, $body) = split /\n/, $res{$num}->{note}, 3; + } + else { + ($title, $body) = split /\n/, $res{$num}->{note}, 2; + $path = ''; + } + my $date = $res{$num}->{date}; + $res{$num} = { body => $body, title => $title, path => $path, date => $date}; + } + close(DUMPFILE); + select STDOUT; + } + +sub import { + my($num, $start, $complete, $dummi, $note, $date, $time, $number, $stdin, $DUMP, %data); + # open $dump_file and import it into the notedb + $stdin = 1 if($dump_file eq "-"); + if ($stdin) { + $DUMP = *STDIN; + } + else { + open (DUMPFILE, "<$dump_file") or die "could not open $dump_file\n"; + $DUMP = *DUMPFILE; + } + + my $yaml = join '', <$DUMP>; + + my $res = Load($yaml); + + foreach my $number (keys %{$res}) { + my $note; + if ($res->{$number}->{path}) { + $note = "$res->{$number}->{path}\n$res->{$number}->{title}\n$res->{$number}->{body}"; + } + else { + $note = "$res->{$number}->{title}\n$res->{$number}->{body}"; + } + $data{$number} = { + date => $res->{$number}->{date}, + note => &add_ticket($note) + }; + print "fetched note number $number from $dump_file from $res->{$number}->{date}.\n" if(!$stdin); + $number++; + } + + $db->set_del_all() if($ImportType ne ""); + $db->import_data(\%data); +} + +sub OLDimport { + my($num, $start, $complete, $dummi, $note, $date, $time, $number, $stdin, $DUMP, %data); + # open $dump_file and import it into the notedb + $stdin = 1 if($dump_file eq "-"); + if ($stdin) { + $DUMP = *STDIN; + } + else { + open (DUMPFILE, "<$dump_file") or die "could not open $dump_file\n"; + $DUMP = *DUMPFILE; + } + + $complete = $start = 0; + $number = 1; + while (<$DUMP>) { + chomp $_; + if ($_ =~ /^Number:\s\d+/) { + if ($start == 0) { + # we have no previous record + $start = 1; + } + else { + # we got a complete record, save it! + $data{$number} = { + date => $date, + note => &add_ticket($note) + }; + print "fetched note number $number from $dump_file from $date.\n" if(!$stdin); + $complete = 0; + $note = ""; + $date = ""; + $number++; + } + } + elsif ($_ =~ /^Timestamp:\s\d+/ && $complete == 0) { + ($dummi,$date,$time) = split(/\s/,$_); + $date = "$date $time"; + $complete = 1; + } + else { + $note .= $_ . "\n"; + } + } + + if ($note ne "" && $date ne "") { + # the last record, if existent + $data{$number} = { + date => $date, + note => &add_ticket($note) + }; + print "fetched note number $number from $dump_file from $date.\n" if(!$stdin); + } + + $db->set_del_all() if($ImportType ne ""); + $db->import_data(\%data); +} + +sub determine_width { + # determine terminal wide, if possible + if ($maxlen eq "auto") { + eval { + my $wide = `stty -a`; + if ($wide =~ /columns (\d+?);/) { + $maxlen = $1 - 32; # (32 = timestamp + borders) + } + elsif ($wide =~ /; (\d+?) columns;/) { + # bsd + $maxlen = $1 - 32; # (32 = timestamp + borders) + } + else { + # stty didn't work + $maxlen = 80 - 32; + } + }; + } +} + +sub clear { + # first, try to determine the terminal height + return if(!$conf{autoclear}); + my $hoch; + eval { + my $height = `stty -a`; + if ($height =~ /rows (\d+?);/) { + $hoch = $1; + } + elsif ($height =~ /; (\d+?) rows;/) { + # bsd + $hoch = $1; + } + }; + if (!$hoch) { + # stty didn't work + $hoch = 25; + } + print "\n" x $hoch; +} + +sub interactive { + my($B, $BB, $menu, $char, $Channel); + $Channel = $|; + local $| = 1; + # create menu: + $B = ""; + $BB = ""; + $menu = "[" . $B . "L" . $BB . "-List "; + if ($TOPIC) { + $menu .= $B . "T" . $BB . "-Topics "; + } + $menu .= $B . "N" . $BB . "-New " + . $B . "D" . $BB . "-Delete " + . $B . "S" . $BB . "-Search " + . $B . "E" . $BB . "-Edit " + . $B . "?" . $BB . "-Help " + . $B . "Q" . $BB . "-Quit] "; # $CurTopic will be empty if $TOPIC is off! + + # per default let's list all the stuff: + # Initially do a list command! + &determine_width; + $ListType = ($conf{defaultlong}) ? "LONG" : ""; + &list; + + my ($term, $prompt, $attribs); + eval { require Term::ReadLine; }; + if (!$@) { + $term = new Term::ReadLine(''); + $attribs = $term->Attribs; + $attribs->{completion_function} = \&complete; + } + + for (;;) { + $ListType = ($conf{defaultlong}) ? "LONG" : ""; + undef $SetTitle; + if ($CurDepth > 2) { + print C $menu . $TOPICC . "../" . $CurTopic . $_TOPICC . ">"; + } + else { + print C $menu . $TOPICC . $CurTopic . $_TOPICC . ">"; + } + + # endless until user press "Q" or "q"! + if ($term) { + if (defined ($char = $term->readline(" "))) { + $term->addhistory($char) if $char =~ /\S/; + $char =~ s/\s*$//; # remove trailing whitespace (could come from auto-completion) + } + else { + # shutdown + $| = $Channel; + print "\n\ngood bye!\n"; + exit(0); + } + } + else { + $char = ; + chomp $char; + } + + &determine_width; + &clear; + + if ($char =~ /^\d+\s*[\di*?,*?\-*?]*$/) { + $ListType = ""; #overrun + # display notes + $number = $char; + &display; + } + elsif ($char =~ /^n$/i) { + # create a new one + &new; + } + elsif ($char =~ /^$/) { + &list; + } + elsif ($char =~ /^l$/) { + $ListType = ""; + &list; + } + elsif ($char =~ /^L$/) { + $ListType = "LONG"; + &list; + undef $SetTitle; + } + elsif ($char =~ /^h$/i || $char =~ /^\?/) { + # zu dumm der Mensch ;-) + &help; + } + elsif ($char =~ /^d\s+([\d*?,*?\-*?]*)$/i) { + # delete one! + $number = $1; + &del; + } + elsif ($char =~ /^d$/i) { + # we have to ask her: + print "enter number(s) of note(s) you want to delete: "; + $char = ; + chomp $char; + $number = $char; + &del; + } + elsif ($char =~ /^e\s+(\d+\-*\,*\d*)/i) { + # edit one! + $number = $1; + &edit; + } + elsif ($char =~ /^e$/i) { + # we have to ask her: + print "enter number of the note you want to edit: "; + $char = ; + chomp $char; + $number = $char; + &edit; + } + elsif ($char =~ /^s\s+/i) { + # she want's to search + $searchstring = $'; + chomp $searchstring; + &search; + } + elsif ($char =~ /^s$/i) { + # we have to ask her: + print "enter the string you want to search for: "; + $char = ; + chomp $char; + $char =~ s/^\n//; + $searchstring = $char; + &search; + } + elsif ($char =~ /^q$/i) { + # schade!!! + $| = $Channel; + print "\n\ngood bye!\n"; + exit(0); + } + elsif ($char =~ /^t$/) { + $TreeType = ""; + &display_tree; + } + elsif ($char =~ /^T$/) { + $TreeType = "LONG"; + &display_tree; + $TreeType = ""; + } + elsif ($char =~ /^c\s*$/) { + print "Missing parameter (parameter=value), available ones:\n"; + foreach my $var (sort keys %conf) { + if ($var !~ /^$hardparams/ && $var !~ /::/) { + printf "%20s = %s\n", $var, $conf{$var}; + } + } + } + elsif ($char =~ /^c\s*(.+?)\s*=\s*(.+?)/) { + # configure + my $param = $1; + my $value = $2; + if ($param !~ /^$hardparams/ && $param !~ /::/ && exists $conf{$param}) { + print "Changing $param from $conf{$param} to $value\n"; + $conf{$param} = $value; + } + else { + print "Unknown config parameter $param!\n"; + } + } + elsif ($char =~ /^\.\.$/ || $char =~ /^cd\s*\.\.$/) { + $CurDepth-- if ($CurDepth > 1); + $CurTopic = $LastTopic[$CurDepth]; + pop @LastTopic; # remove last element + &list; + } + elsif ($char =~ /^l\s+(\w+)$/) { + # list + $WantTopic = $1; + if (exists $TP{$WantTopic}) { + my %SaveTP = %TP; + $LastTopic[$CurDepth] = $CurTopic; + $CurTopic = $1; + $CurDepth++; + &list; + $CurTopic = $LastTopic[$CurDepth]; + $CurDepth--; + %TP = %SaveTP; + } + else { + print "\nunknown command!\n"; + } + } + else { + # unknown + my $unchar = $char; + $unchar =~ s/^cd //; # you may use cd now! + if ($unchar =~ /^\d+?$/ && $conf{short_cd}) { + # just a number! + my @topic; + my ($cnote, $cdate) = $db->get_single($unchar); + my ($firstline,$dummy) = split /\n/, $cnote, 2; + if ($firstline =~ /^($conf{topicseparator})/) { + @topic = split(/$conf{topicseparator}/,$firstline); + } + else { + @topic = (); + } + if (@topic) { + # only jump, if, and only if there were at least one topic! + $CurDepth = $#topic + 1; + $CurTopic = pop @topic; + @LastTopic = (""); + push @LastTopic, @topic; + } + &list; + } + elsif ($unchar eq $conf{topicseparator}) { + # cd / + $CurDepth = 1; + $CurTopic = ""; + &list; + } + elsif (exists $TP{$char} || exists $TP{$unchar}) { + $char = $unchar if(exists $TP{$unchar}); + $LastTopic[$CurDepth] = $CurTopic; + $CurTopic = $char; + $CurDepth++; + &list; + } + else { + # try incomplete match + my @matches; + foreach my $topic (keys %TP) { + if ($topic =~ /^$char/) { + push @matches, $topic; + } + } + my $nm = scalar @matches; + if ($nm == 1) { + # match on one incomplete topic, use this + $LastTopic[$CurDepth] = $CurTopic; + $CurTopic = $matches[0]; + $CurDepth++; + &list; + } + elsif ($nm > 1) { + print "available topics: " . join( "," , @matches) . "\n"; + } + else { + print "\nunknown command!\n"; + } + } + undef $unchar; + } + } + } + + +sub usage + { + print qq~This is the program note $version by Thomas Linden (c) 1999-2012. +It comes with absolutely NO WARRANTY. It is distributed under the +terms of the GNU General Public License. Use it at your own risk :-) + +Usage: note [ options ] [ number [,number...]] +Read the note(1) manpage for more details. +~; + exit 1; + } + +sub find_editor { + return $conf{preferrededitor} || $ENV{"VISUAL"} || $ENV{"EDITOR"} || "vi"; +} + +#/ + +sub format { + # make text bold/underlined/inverse using current $NOTEC + my($note) = @_; + if ($conf{formattext}) { + # prepare colors to be used for replacement + my $BN = uc($NOTEC); + my $_BN = uc($_NOTEC); + my $UN = $NOTEC; + $UN =~ s/<(.*)>/<$1_>/; + my $_UN = $UN; + $_UN =~ s/<(.*)>/<\/$1>/; + my $IN = $NOTEC; + my $_IN = $_NOTEC; + $IN =~ s/<(.*)>/<$1I>/; + $_IN =~ s/<(.*)>/<$1I>/; + + if ($conf{formattext} eq "simple") { + $note =~ s/\*([^\*]*)\*/$BN$1$_BN/g; + $note =~ s/_([^_]*)_/$UN$1$_UN/g; + $note =~ s/{([^}]*)}/$IN$1$_IN/g; + $note =~ s#(?$1#g; + } + else { + $note =~ s/\*\*([^\*]{2,})\*\*/$BN$1$_BN/g; + $note =~ s/__([^_]{2,})__/$UN$1$_UN/g; + $note =~ s/{{([^}]{2,})}}/$IN$1$_IN/g; + $note =~ s#//([^/]{2,})//#$1#g; + } + + $note =~ s/(<\/.*>)/$1$NOTEC/g; + } + $note; +} + +sub output { + my($SSS, $LINE, $num, $note, $time, $TYPE, $L, $LONGSPC, $R, $PathLen, $SP, $title, $CUTSPACE, + $VersionLen, $len, $diff, $Space, $nlen, $txtlen, $count); + ($num, $note, $time, $TYPE, $count) = @_; + + $txtlen = ($ListType eq "LONG") ? $maxlen : $timelen + $maxlen; + $note = &format($note); + + $SSS = "-" x ($maxlen + 30); + $nlen = length("$num"); + $LINE = "$BORDERC $SSS $_BORDERC\n"; + $LONGSPC = " " x (25 - $nlen); + if ($conf{printlines}) { + $L = $BORDERC . "[" . $_BORDERC; + $R = $BORDERC . "]" . $_BORDERC; + } + $PathLen = length($PATH); # will be ZERO, if not in TOPIC mode! + $VersionLen = length($version) + 7; + + if ($TYPE ne "SINGLE") { + if (!$SetTitle) { + $SP = ""; + # print only if it is the first line! + $SP = " " x ($maxlen - 2 - $PathLen - $VersionLen); + if (!$Raw) { + # no title in raw-mode! + print C $LINE if ($conf{printlines}); + print C "$L $NUMC#$_NUMC "; + if ($ListType eq "LONG") { + print C " $TIMEC" . "creation date$_TIMEC "; + } + else { + print $LONGSPC if ($conf{printlines}); + } + if ($TOPIC) { + print C $TOPICC . "$PATH $_TOPICC$SP" . " note $version $R\n"; + } + else { + print C $NOTEC . "note$_NOTEC$SP" . " note $version $R\n"; + } + print C $LINE if ($conf{printlines}); + } + $SetTitle = 1; + } + $title = ""; + $CUTSPACE = " " x $txtlen; + if ($TYPE eq "search") { + $note =~ s/^\Q$conf{topicseparator}\E.+?\Q$conf{topicseparator}\E\n//; + } + $note =~ s/\n/$CUTSPACE/g; + $len = length($note); + if ($len < ($txtlen - 2 - $nlen)) { + $diff = $txtlen - $len; + if (!$Raw) { + if ($num eq "-") { + $Space = " " x $diff; + $title = $BORDERC . $TOPICC . $note . " " . $_TOPICC . $Space . "$_BORDERC"; + } + else { + $Space = " " x ($diff - ($nlen - 1)); + $title = $BORDERC . $NOTEC . $note . " " . $_NOTEC . $Space . "$_BORDERC"; + } + } + else { + $title = $note; + } + } + else { + $title = substr($note,0,($txtlen - 2 - $nlen)); + if (!$Raw) { + $title = $BORDERC . $NOTEC . $title . " $_NOTEC$_BORDERC"; + } + } + if ($Raw) { + print "$num "; + print "$time " if($ListType eq "LONG"); + if ($title =~ /^ => (.*)$conf{topicseparator} (.*)$/) { + $title = "$1$conf{topicseparator} $2"; # seems to be a topic! + } + print "$title\n"; + } + else { + # $title should now look as: "A sample note " + print C "$L $NUMC$num$_NUMC $R"; + if ($ListType eq "LONG") { + print C "$L$TIMEC" . $time . " $_TIMEC$R"; + } + print C "$L $NOTEC" . $title . "$_NOTEC $R\n"; + print C $LINE if ($conf{printlines}); + } + } + else { + # we will not reach this in raw-mode, therefore no decision here! + chomp $note; + $Space = " " x (($maxlen + $timelen) - $nlen - 16); + + *CHANNEL = *STDOUT; + my $usecol = $conf{usecolors}; + + if ($conf{less}) { + my $less = "less"; + if ($conf{less} ne 1) { + # use given less command line + $less = $conf{less}; + } + if (open LESS, "|$less") { + *CHANNEL = *LESS; + $conf{usecolors} = 0; + } + } + + print CHANNEL C $LINE if ($conf{printlines}); + print CHANNEL C "$L $NUMC$num$_NUMC $R$L$TIMEC$time$_TIMEC $Space$R\n"; + print CHANNEL C "\n"; + print CHANNEL C $NOTEC . $note . $_NOTEC . "\n"; + print CHANNEL C $LINE if ($count == 1 && $conf{printlines}); + + if ($conf{less}) { + close LESS; + } + + $conf{usecolors} = $usecol; + } + } + + + +sub C { + my($default, $S, $Col, $NC, $T); + $default = "\033[0m"; + $S = $_[0]; + foreach $Col (%Color) { + if ($S =~ /<$Col>/g) { + if ($conf{usecolors}) { + $NC = "\033[" . $Color{$Col} . "m"; + $S =~ s/<$Col>/$NC/g; + $S =~ s/<\/$Col>/$default/g; + } + else { + $S =~ s/<$Col>//g; + $S =~ s/<\/$Col>//g; + } + } + } + return $S; + } + + + +sub num_bereich { + my($m,@LR,@Sorted_LR,$i); + # $number is the one we want to delete! + # But does it contain commas? + @NumBlock = (); #reset + $m = 0; + if ($number =~ /\,/) { + # accept -d 3,4,7 + @NumBlock = split(/\,/,$number); + } + elsif ($number =~ /^\d+\-\d+$/) { + # accept -d 3-9 + @LR = split(/\-/,$number); + @Sorted_LR = (); + + if ($LR[0] > $LR[1]) { + @Sorted_LR = ($LR[1], $LR[0]); + } + elsif ($LR[0] == $LR[1]) { + # 0 and 1 are the same + @Sorted_LR = ($LR[0], $LR[1]); + } + else { + @Sorted_LR = ($LR[0], $LR[1]); + } + + for ($i=$Sorted_LR[0]; $i<=$Sorted_LR[1]; $i++) { + # from 3-6 create @NumBlock (3,4,5,6) + $NumBlock[$m] = $i; + $m++; + } + } + else { + @NumBlock = ($number); + } + + } + +sub getdate { + my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); + $year += 1900; + $mon += 1; + $mon =~ s/^(\d)$/0$1/; + $hour =~ s/^(\d)$/0$1/; + $min =~ s/^(\d)$/0$1/; + $sec =~ s/^(\d)$/0$1/; + $mday =~ s/^(\d)$/0$1/; + if ($conf{timeformat}) { + my $back = $conf{timeformat}; + $back =~ s/YYYY/$year/; + $back =~ s/YY/substr($year, 1, 2)/e; + $back =~ s/MM/$mon/; + $back =~ s/DD/$mday/; + $back =~ s/mm/$min/; + $back =~ s/hh/$hour/; + $back =~ s/ss/$sec/; + return $back; + } + return "$mday.$mon.$year $hour:$min:$sec"; + } + + +sub gettemp { + my($random, @range); + @range=('0'..'9','a'..'z','A'..'Z'); + srand(time||$$); + for (0..10) { + $random .= $range[rand(int($#range)+1)]; + } + my $tempfile = File::Spec->catfile($conf{tempdirectory}, $USER . $random); + if (-e $tempfile) { + # avoid race conditions! + unlink $tempfile; + } + return $tempfile; + } + + + +sub help { + my $B = ""; + my $BB = ""; + my($S, $L, $T, $Q, $H, $N, $D, $E, $C); + $L = $B . "L" . $BB . $NOTEC; + $T = $B . "T" . $BB . $NOTEC; + $Q = $B . "Q" . $BB . $NOTEC; + $H = $B . "?" . $BB . $NOTEC; + $N = $B . "N" . $BB . $NOTEC; + $D = $B . "D" . $BB . $NOTEC; + $E = $B . "E" . $BB . $NOTEC; + $S = $B . "S" . $BB . $NOTEC; + $C = $B . "C" . $BB . $NOTEC; + + print C qq~$BORDERC +----------------------------------------------------------------------$_BORDERC $TOPICC +HELP for interactive note $version +$_TOPICC $NOTEC +The following commands are available: +$L List notes. L=long, with timestamp and l=short without timestamp. + You can also just hit for short list. + If you specify a subtopic, then list will display it's contents, + i.e.: "l mytopic" will dislpay notes under mytopic. +$N Create a new note. +$D Delete a note. You can either hit "d 1" or "d 1-4" or just hit "d". + If you don't specify a number, you will be asked for. +$S Search trough the notes database. Usage is similar to Delete, use + a string instead of a number to search for. +$E Edit a note. Usage is similar to Delete but you can only edit note + a time. +$C Change note config online. Use with care! +$H This help screen. +$Q Exit the program.~; + if ($TOPIC) { + print C qq~ +$T print a list of all existing topics as a tree. T prints the tree + with all notes under each topic.~; + } + print C qq~ + +All commands except the List and Topic commands are case insensitive. +Read the note(1) manpage for more details.$BORDERC +----------------------------------------------------------------------$_BORDERC +~; + } + + +sub display_tree { + # displays a tree of all topics + my(%TREE, %res, $n, $t, $num, @nodes, $firstline, $text, $untext); + %res = $db->get_all(); + foreach $num (keys %res) { + $n = $res{$num}->{'note'}; + $t = $res{$num}->{'date'}; + # this allows us to have multiple topics (subtopics!) + my ($firstline,$text,$untext) = split /\n/, $n, 3; + if ($firstline =~ /^($conf{topicseparator})/) { + $firstline =~ s/($conf{topicseparator})*$//; #remove Topicseparator + @nodes = split(/$conf{topicseparator}/,$firstline); + } + else { + @nodes = (); #("$conf{topicseparator}"); + $text = $firstline; + } + &determine_width; # ensure $maxlen values for &tree in non interactive modes + &tree($num, $text, \%TREE, @nodes); + } + #return if ($num == 0); + # now that we have build our tree (in %TREE) go on t display it: + print C $BORDERC . "\n[" . $conf{topicseparator} . $BORDERC . "]\n"; + &print_tree(\%{$TREE{''}},"") if(%TREE); + print C $BORDERC . $_BORDERC . "\n"; +} + + +sub tree { + my($num, $text, $LocalTree, $node, @nodes) = @_; + if (@nodes) { + if (! exists $LocalTree->{$node}->{$NoteKey}) { + $LocalTree->{$node}->{$NoteKey} = []; + } + &tree($num, $text, $LocalTree->{$node}, @nodes); + } else { + if (length($text) > ($maxlen - 5)) { + $text = substr($text, 0, ($maxlen -5)); + } + $text = $text . " (" . $NUMC . "#" . $num . $_NUMC . $NOTEC . ")" . $_NOTEC if($text ne ""); + push @{$LocalTree->{$node}->{$NoteKey}}, $text; + } +} + + +sub print_tree { + # thanks to Jens for his hints and this sub! + my $hashref=shift; + my $prefix=shift; + my @notes=@{$hashref->{$NoteKey}}; + my @subnotes=sort grep { ! /^$NoteKey$/ } keys %$hashref; + if ($TreeType eq "LONG") { + for my $note (@notes) { + if ($note ne "") { + print C $BORDERC ; # . $prefix. "|\n"; + print C "$prefix+---<" . $NOTEC . $note . $BORDERC . ">" . $_NOTEC . "\n"; + } + } + } + for my $index (0..$#subnotes) { + print C $BORDERC . $prefix. "|\n"; + print C "$prefix+---[" . $TOPICC . $subnotes[$index] . $BORDERC . "]\n"; + &print_tree($hashref->{$subnotes[$index]},($index == $#subnotes?"$prefix ":"$prefix| ")); + } +} + + +sub getconfig { + my($configfile) = @_; + my ($home, $value, $option); + # checks are already done, so trust myself and just open it! + open CONFIG, "<$configfile" || die $!; + while () { + chomp; + next if(/^\s*$/ || /^\s*#/); + my ($option,$value) = split /\s\s*=?\s*/, $_, 2; + + $value =~ s/\s*$//; + $value =~ s/\s*#.*$//; + if ($value =~ /^(~\/)(.*)$/) { + $value = File::Spec->catfile($ENV{HOME}, $2); + } + + if ($value =~ /^(yes|on|1)$/i) { + $value = 1; + } + elsif ($value =~ /^(no|off|0)$/i) { + $value = 0; + } + + $option = lc($option); + + if ($option =~ /^(.+)::(.*)$/) { + # driver option + $driver{$1}->{$2} = $value; + } + else { + # other option + $conf{$option} = $value; + } + } + + close CONFIG; +} + + +sub complete { + my ($text, $line, $start) = @_; + + if ($line =~ /^\s*$/) { + # notes or topics allowed + return @completion_topics, @completion_notes; + } + if ($line =~ /^cd/) { + # only topics allowed + return @completion_topics, ".."; + } + if ($line =~ /^l/i) { + # only topics allowed + return @completion_topics; + } + if ($line =~ /^[ed]/) { + # only notes allowed + return @completion_notes; + } + if ($line =~ /^[snt\?q]/i) { + # nothing allowed + return (); + } +} + +sub load_driver { + my ($parent) = @_; + + if ($parent) { + my $pkg = "NOTEDB"; + eval "use $pkg;"; + if ($@) { + die "Could not load the NOTEDB module: $@\n"; + } + } + else { + # load the backend driver + my $pkg = "NOTEDB::$conf{dbdriver}"; + eval "use $pkg;"; + if ($@) { + die "$conf{dbdriver} backend unsupported: $@\n"; + } + else { + $db = $pkg->new(%{$driver{$conf{dbdriver}}}); + } + } +} + +sub ticket { + return join "", (map { $randomlist[int(rand($#randomlist))] } (0 .. 10) ); +} +__END__ diff --git a/note.pod b/note.pod index 1741e26..6bd3249 100644 --- a/note.pod +++ b/note.pod @@ -431,7 +431,12 @@ input and overwrites an existing database. Before you use the B<-o> switch, I consider you to make a backup! +=head3 BACKUP FILE FORMAT +B: since version 1.3.8 note uses a new file format +for backups: YAML. The old format is only supported by the +B<-I> option to import old backups. New backups are always +created as YAML files. See L. @@ -530,6 +535,6 @@ Thomas Linden =head1 VERSION -1.3.6 (07/20/2009) +1.3.8 (02/01/2012) =cut