Compare commits

...

2 Commits

Author SHA1 Message Date
quenousimporte 77489ca4ab Merge branch 'main' into localonly 2024-03-19 10:26:12 +01:00
quenousimporte a2ca263fe7 drop sync feature 2024-03-19 10:07:22 +01:00
8 changed files with 14 additions and 570 deletions

View File

@ -1,174 +0,0 @@
import io
import json
import subprocess
import urllib.parse
import os
import sys
import time
import re
import shutil
def filename(note):
return re.sub(r'[\?\"<>|\*:\/\\]', '_', note["title"]) + ".md"
def listnotes(filter = "", checkcontent = False):
matching = []
for i in reversed(range(len(data))):
if filter.lower() in data[i]["title"].lower():
print("[" + str(i) + "]", data[i]["title"])
matching.append(data[i])
elif checkcontent and filter.lower() in data[i]["content"].lower():
print("[" + str(i) + "]", data[i]["title"])
lines = data[i]["content"].split("\n")
for j in range(len(lines)):
line = lines[j]
if filter.lower() in line.lower():
index = line.lower().index(filter.lower())
print("\t" + str(j) + ":", line[:100])
matching.append(data[i])
return matching
def readtextfile(path):
with io.open(path, mode = "r", encoding = "utf-8") as f:
return f.read()
def writetextfile(path, content):
with io.open(path, mode = "w", encoding = "utf-8") as f:
f.write(content)
def editnote(note):
content = note["content"]
backupfilepath = "session/" + filename(note) + str(time.time())
writetextfile(backupfilepath, content)
writetextfile("session/" + filename(note), content)
subprocess.call(settings["commands"]["editor"] + ["session/" + filename(note)])
newcontent = readtextfile("session/" + filename(note) )
if newcontent != content:
subprocess.call(settings["commands"]["diff"] + [backupfilepath, "session/" + filename(note)])
note["content"] = newcontent
data.remove(note)
data.insert(0, note)
savedata()
else:
print("no change")
def savedata():
if settings["mode"] == "remote":
writetextfile("session/data.json", json.dumps(data))
subprocess.call([settings["commands"]["gpg"], "-q", "--encrypt", "--yes", "--trust-model", "always", "--output", "session/data.acs", "--armor", "-r", settings["gpguser"], "session/data.json"]);
newdata = readtextfile("session/data.acs")
postdata = "action=push&password=" + settings["password"] + "&data=" + urllib.parse.quote_plus(newdata)
writetextfile("session/postdata", postdata)
output = subprocess.check_output(["curl", "-X", "POST", "-d", "@session/postdata", settings["url"] + "/handler.php"]).decode("utf-8")
print("curl output: " + output)
if output != '{"result": "ok"}':
if ask("Save failed. Try again?"):
savedata()
else:
writetextfile("session/local.json", json.dumps(data))
def loaddata():
if settings["mode"] == "remote":
subprocess.call(["curl", "-X", "POST", "-F", "action=fetch", "-F", "password=" + settings["password"], "-o", "session/data.acs", settings["url"] + "/handler.php"])
subprocess.call([settings["commands"]["gpg"], "-q", "--yes", "--output", "session/data.json", "--decrypt", "session/data.acs"])
return json.loads(readtextfile("session/data.json"))
else:
return json.loads(readtextfile("session/local.json"))
def ask(question):
answer = input(question + " [Y/n] ")
return answer == "y" or answer == "yes" or answer == ""
def initdatapath():
if not os.path.exists("history"):
os.mkdir("history")
if os.path.exists("session"):
if os.path.exists("session/postdata"):
os.remove("session/postdata")
shutil.make_archive("history/session" + str(time.time()), "zip", "session")
shutil.rmtree("session")
os.mkdir("session")
abspath = os.path.abspath(__file__)
dname = os.path.dirname(abspath)
os.chdir(dname)
initdatapath()
settings = json.loads(readtextfile("settings.json"))
data = loaddata()
command = ""
if len(sys.argv) > 1:
command = sys.argv[1]
if command.startswith("notes://"):
command = urllib.parse.unquote(command[8:-1])
while not (command == "quit" or command == "exit" or command == "q"):
action = None
if command[0:3] == "rm ":
action = "delete"
command = command[3:]
elif command[0:3] == "mv ":
action = "rename"
command = command[3:]
elif command[0:1] == "/":
action = "grep"
command = command[1:]
elif command[0:7] == "export ":
action = "export"
command = command[7:]
elif command == "settings":
action = "settings"
try:
index = int(command)
note = data[index]
except:
note = next((note for note in data if note["title"] == command), None)
if action == "delete":
if note and ask("delete '" + note["title"] + "'? "):
data.remove(note)
savedata()
elif action == "rename":
if note:
newname = input("new name: ")
if newname:
note["title"] = newname
savedata()
elif action == "export":
if note:
writetextfile("session/" + note["title"] + ".md", note["content"])
elif action == "settings":
subprocess.call(settings["commands"]["editor"] + ["settings.json"])
settings = json.loads(readtextfile("settings.json"))
elif note and not action == "grep":
editnote(note)
else:
matching = listnotes(command, action == "grep")
if len(matching) == 0 and not action == "grep":
if ask("create '" + command + "'? "):
note = {
"title": command,
"content": "---\ntitle: " + command + "\ndate: " + time.strftime("%Y-%m-%d") + "\ntags: \n---\n\n"
}
data.insert(0, note)
editnote(note)
elif len(matching) == 1:
note = matching.pop()
if ask("open '" + note["title"] + "'?"):
editnote(note)
command = input("> ")

View File

@ -1,12 +0,0 @@
{
"url": "http://localhost:8000",
"password": "",
"gpguser": "",
"commands":
{
"editor": ["vim"],
"gpg": "gpg",
"diff": ["diff", "--color"]
},
"mode": "remote"
}

View File

@ -1,41 +0,0 @@
<?php
require 'settings.php';
// check authent
if ($password && (!isset($_POST['password']) || $_POST['password'] != $password))
{
echo 'error: authent';
}
else if (isset($_POST['action']))
{
$action = $_POST['action'];
$path = $datadir . $_POST['name'];
switch ($action)
{
case 'fetch':
if (file_exists($path))
{
echo file_get_contents($path);
}
break;
case 'push':
$result = file_put_contents($path, $_POST['data']);
if ($result === false)
{
echo 'error: could not save ' . $_POST['name'];
}
break;
default:
echo 'error: unknown action ' . $action;
break;
}
}
else
{
echo 'error: missing action parameter';
}
?>

View File

@ -13,7 +13,6 @@
<body onload="init()" onkeydown="mainkeydownhandler()" onresize="resize()" onstorage="loadstorage()">
<script src="libs/showdown.min.js"></script>
<script src="libs/vis-network.min.js"></script>
<script src="libs/openpgp.min.js"></script>
<script src="libs/jszip.min.js"></script>
<script src="libs/FileSaver.js"></script>
<script src="main.js"></script>

17
libs/openpgp.min.js vendored

File diff suppressed because one or more lines are too long

307
main.js
View File

@ -11,8 +11,6 @@ var defaultsettings =
titlebydefault: false,
linksinnewtab: true,
colors: true,
password: "",
sync: false,
tagsinlists: true,
uselinkpopup: true,
downloadextension: ".md"
@ -31,7 +29,6 @@ var workerid = null;
var backup = "";
var settings = null;
var tags = null;
var pending = {};
var commands = [
{
@ -195,14 +192,6 @@ var commands = [
hint: "Insert text in todo",
action: promptinserttodo
},
{
hint: "Edit pgp keys",
action: editpgpkeys
},
{
hint: "Decrypt note",
action: decryptnote
},
{
hint: "Replace",
shortcut: "ctrl+h",
@ -314,56 +303,6 @@ function seteditorcontent(content)
resize();
}
function encryptstring(str)
{
var key = localStorage.getItem("pgpkeys").split("-----BEGIN PGP PRIVATE KEY BLOCK-----")[0];
var publicKey = null;
return openpgp.readKey({ armoredKey: key })
.then(res =>
{
publicKey = res;
return openpgp.createMessage({ text: str });
})
.then(message =>
{
return openpgp.encrypt({
message: message,
encryptionKeys: publicKey });
});
}
function decryptstring(str)
{
if (!str.startsWith("-----BEGIN PGP MESSAGE-----"))
{
// console.log(str + ": string is not encrypted");
return Promise.resolve(str);
}
var key = localStorage.getItem("pgpkeys").split("-----END PGP PUBLIC KEY BLOCK-----")[1];
var privateKey = null;
return openpgp.readKey({ armoredKey: key })
.then(res =>
{
privateKey = res;
return openpgp.readMessage({ armoredMessage: str })
})
.then(message =>
{
return openpgp.decrypt({
message: message,
decryptionKeys: privateKey });
})
.then(decrypted =>
{
const chunks = [];
for (const chunk of decrypted.data) {
chunks.push(chunk);
}
return chunks.join('');
})
}
function getrangecontent(range)
{
return md.value.substring(range.start, range.end);
@ -377,44 +316,12 @@ function currentrange()
};
}
function pushitem(key, value)
{
unsavedmark.hidden = false;
pending[key] = true;
var name = metadata && metadata[key] ? metadata[key].title : key;
queryremote({action: "push", name: key, data: value})
.then( () =>
{
console.log("'" + name + "' pushed to server");
delete pending[key];
})
.catch( err =>
{
console.error("error while pushing '" + name + "': " + err);
showtemporaryinfo(msg);
setTimeout( () =>
{
serialize(key, value);
}, settings.savedelay);
})
.finally( () =>
{
unsavedmark.hidden = Object.keys(pending) == 0;
});
}
function serialize(key, value)
{
localStorage.setItem(key, value);
var name = metadata && metadata[key] ? metadata[key].title : key;
console.log("'" + name + "' serialized locally");
if (settings.sync)
{
pushitem(key, value);
}
}
function createsubnote()
@ -510,8 +417,6 @@ function showinfo()
var tags = gettags(md.value);
showtemporaryinfo(
[
"sync: " + (settings.sync ? "en" : "dis") + "abled",
"pending: " + Object.keys(pending).join(),
"title: " + title.value,
"line count: " + md.value.split("\n").length,
"word count: " + getwords(),
@ -903,21 +808,6 @@ function changesetting()
});
}
function decryptnote()
{
decryptstring(md.value)
.then(decrypted =>
{
seteditorcontent(decrypted);
resize();
});
}
function editpgpkeys()
{
bind("pgpkeys", localStorage.getItem("pgpkeys") || "");
}
function showtemporaryinfo(info)
{
alert(info);
@ -1338,102 +1228,6 @@ function migratelegacystorage()
}
}
function pushall()
{
var index = JSON.parse(localStorage.getItem("index"));
var list = [queryremote({action: "push", name: "index", data: localStorage.getItem("index")})];
Object.keys(index).forEach(guid =>
{
list.push(queryremote({action: "push", name: guid, data: localStorage.getItem(guid)}));
});
return Promise.all(list);
}
function fetchandserialize(guid)
{
var name = (metadata && metadata[guid]) ? metadata[guid] : guid;
console.log("fetching '" + name + "'");
return queryremote({action: "fetch", name: guid})
.then(content =>
{
localStorage.setItem(guid, content);
console.log("'" + name + "' fetched and serialized");
});
}
function fetch()
{
return new Promise(function(resolve)
{
if (settings.sync)
{
var pgpkeys = localStorage.getItem("pgpkeys");
if (!pgpkeys)
{
loadstorage();
editpgpkeys();
showtemporaryinfo("Pgp key empty or invalid. Enter PGP keys and refresh.");
}
else
{
queryremote({action: "fetch", name: "index"})
.then(filecontent =>
{
if (!filecontent)
{
if (confirm("No remote index found. Init remote now?"))
{
pushall().then(resolve);
}
else
{
resolve();
}
}
else
{
var localindex = JSON.parse(localStorage.getItem("index"));
var remoteindex = JSON.parse(filecontent);
var list = [];
Object.keys(remoteindex).forEach(guid =>
{
if (!localindex[guid] || localindex[guid].lastchanged < remoteindex[guid].lastchanged)
{
list.push(fetchandserialize(guid));
}
});
Promise.all(list).then( () =>
{
localStorage.setItem("index", JSON.stringify(remoteindex));
resolve();
});
}
})
.catch(err =>
{
if (err == "error: authent")
{
settings.password = prompt("Password: ", settings.password);
savesettings();
init();
}
else
{
showtemporaryinfo(err);
}
});
}
}
else
{
resolve();
}
});
}
function init()
{
migratelegacystorage();
@ -1445,94 +1239,19 @@ function init()
initsnippets();
fetch()
.then( () =>
{
loadstorage();
loadstorage();
if (issplit())
if (issplit())
{
if (settings.defaultpreviewinsplit && name == "right")
{
if (settings.defaultpreviewinsplit && name == "right")
{
togglepreview();
}
else if (name == "left")
{
md.focus();
}
togglepreview();
}
});
}
function encryptdata(params)
{
if (params.data)
{
return encryptstring(params.data)
.then(encrypted =>
else if (name == "left")
{
params.data = encrypted;
return Promise.resolve(params);
});
md.focus();
}
}
else
{
return Promise.resolve(params);
}
}
function queryremote(params)
{
return encryptdata(params)
.then(encparams =>
{
return new Promise ( (resolve, reject) =>
{
encparams.password = settings.password;
var paramlist = [];
for (var i in encparams)
{
paramlist.push(i + "=" + encodeURIComponent(encparams[i]));
}
var xhr = new XMLHttpRequest();
xhr.open("POST", "handler.php");
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.onerror = function()
{
reject("XMLHttpRequest error");
}
xhr.onload = function()
{
if (xhr.status !== 200)
{
reject("Http status " + xhr.status);
}
else
{
decryptstring(xhr.responseText)
.then(decrypted =>
{
if (decrypted.startsWith("error: "))
{
reject(decrypted);
}
else
{
resolve(decrypted);
}
});
}
}
var paramstring = paramlist.join("&");
console.log("http '" + encparams.action + "' request length: " + formatsize(paramstring.length));
xhr.send(paramstring);
});
});
}
function getlinesrange()
@ -1741,7 +1460,7 @@ function commandpalette()
return {
prefix: "setting ",
text: s,
suffix: s == "password" ? null : [settings[s]]
suffix: [settings[s]]
};
})))
.then(selected =>
@ -1935,12 +1654,7 @@ function flush()
savesettings();
loadsettings();
}
else if (title.value == "pgpkeys")
{
localStorage.setItem("pgpkeys", md.value);
}
unsavedmark.hidden = Object.keys(pending).length == 0;
unsavedmark.hidden = true;
}
function escapeHtml(unsafe) {
@ -2243,7 +1957,6 @@ function showhelp()
help.push("## Libs");
help.push("[Showdown](https://showdownjs.com/)");
help.push("[vis-network](https://visjs.org/)");
help.push("[openpgpjs](https://openpgpjs.org/)");
help.push("[jszip](https://stuk.github.io/jszip/)");
help.push("[FileSaver](http://eligrey.com)");

View File

@ -2,41 +2,21 @@
## Getting started
Launch index.html from your web server or try https://notes.ouvaton.org.
Call index.html or try https://notes.ouvaton.org.
Your notes are stored in your browser local storage.
You can import a folder of markdown files.
## Usage
* command palette: ctrl+shift+p
* notes list: ctrl+p
## Sync feature
To sync your notes in the cloud:
* put the source files on your php server
* browse index.html
* paste your public and private PGP keys as a single file (passphrase is not supported)
* refresh the page
Your data is always encrypted before reaching the server.
To protect your data file access by a password, edit settings.php and change `$password` variable. The password is sent from browser to server through a post http query, encrypted with ssl if enabled. It is stored unencrypted in your browser local storage and in the settings.php file on server side.
## Cli tool
```
cd cli
python3 app.py
```
Requires python3, curl, gnupg and a text editor.
## Web clipper
Add `clipper\clipper.js` as bookmarklet on your web browser bookmark toolbar. Click to add current URL to a note named *todo*.
## Export your data
You can download your notes in a single json data file, or as flat markdown files in a zip archive.
You can download your notes as flat markdown files in a zip archive.

View File

@ -1,4 +0,0 @@
<?php
$datadir = '../data/';
$password = '';
?>