Compare commits
14 Commits
2e66666bdf
...
v0.0.24
Author | SHA1 | Date | |
---|---|---|---|
9614d03bef | |||
32a335c676 | |||
06e27fc1e0 | |||
1f40e8dcd9 | |||
77ff8cef1f | |||
ef844fbccb | |||
070dc5a4c0 | |||
177ef1cdcc | |||
4b1ebf02e1 | |||
863e50203e | |||
01b8c209de | |||
30e92f2bc1 | |||
02accabb4a | |||
fa00a41fe0 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,6 +4,7 @@ db.*
|
|||||||
deps/ios_toolchain/
|
deps/ios_toolchain/
|
||||||
deps/openssl/
|
deps/openssl/
|
||||||
dist/
|
dist/
|
||||||
|
.flatpak-builder
|
||||||
.keys
|
.keys
|
||||||
logs/
|
logs/
|
||||||
**/node_modules
|
**/node_modules
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
MAKEFLAGS += --warn-undefined-variables
|
MAKEFLAGS += --warn-undefined-variables
|
||||||
MAKEFLAGS += --no-builtin-rules
|
MAKEFLAGS += --no-builtin-rules
|
||||||
|
|
||||||
VERSION_CODE := 28
|
VERSION_CODE := 29
|
||||||
VERSION_NUMBER := 0.0.24-wip
|
VERSION_NUMBER := 0.0.24
|
||||||
VERSION_NAME := Honey bunches of boats.
|
VERSION_NAME := Honey bunches of boats.
|
||||||
|
|
||||||
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3470000.zip
|
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3470000.zip
|
||||||
@ -1105,6 +1105,11 @@ out/tildefriends-x86_64.AppImage: out/release/tildefriends out/data.zip
|
|||||||
appimage: out/tildefriends-x86_64.AppImage
|
appimage: out/tildefriends-x86_64.AppImage
|
||||||
.PHONY: appimage
|
.PHONY: appimage
|
||||||
|
|
||||||
|
flatpak: out/
|
||||||
|
flatpak-builder --force-clean --user --install-deps-from=flathub --install --repo=out/flatpak-repo out/flatpak src/com.unprompted.tildefriends.yml
|
||||||
|
flatpak build-bundle out/flatpak-repo out/tildefriends.flatpak com.unprompted.tildefriends
|
||||||
|
.PHONY: flatpak
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(BUILD_DIR)
|
rm -rf $(BUILD_DIR)
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "💽"
|
"emoji": "💽",
|
||||||
|
"previous": "&uQzkIe/Aj8yNhLKe3hEq+5fEJsGwIUx8NVBjJKwoV2U=.sha256"
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,19 @@ async function key_list(db) {
|
|||||||
app.setDocument(doc);
|
app.setDocument(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
if (core.user?.credentials?.session) {
|
||||||
|
database_list();
|
||||||
|
} else {
|
||||||
|
app.setDocument(`<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body style="background: #888">
|
||||||
|
<h1>Must be signed in to examine databases.</h1>
|
||||||
|
</body>
|
||||||
|
</html>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
core.register('message', async function (message) {
|
core.register('message', async function (message) {
|
||||||
if (message.event == 'hashChange') {
|
if (message.event == 'hashChange') {
|
||||||
let hash = message.hash.substring(1);
|
let hash = message.hash.substring(1);
|
||||||
@ -62,9 +75,9 @@ core.register('message', async function (message) {
|
|||||||
} else if (hash.length) {
|
} else if (hash.length) {
|
||||||
key_list(await database(hash.split(':').slice(1).join(':')));
|
key_list(await database(hash.split(':').slice(1).join(':')));
|
||||||
} else {
|
} else {
|
||||||
database_list();
|
load();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
database_list();
|
load();
|
||||||
|
249
core/core.js
249
core/core.js
@ -716,12 +716,12 @@ async function getProcessBlob(blobId, key, options) {
|
|||||||
Object.keys(db).map((x) => [x, db[x].bind(db)])
|
Object.keys(db).map((x) => [x, db[x].bind(db)])
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
imports.databases = function () {
|
imports.databases = async function () {
|
||||||
return [].concat(
|
return [].concat(
|
||||||
databases.list(
|
await databases.list(
|
||||||
':shared:' + process.credentials.session.name + ':%'
|
':shared:' + process.credentials.session.name + ':%'
|
||||||
),
|
),
|
||||||
databases.list(process.credentials.session.name + ':%')
|
await databases.list(process.credentials.session.name + ':%')
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -932,178 +932,83 @@ async function blobHandler(request, response, blobId, uri) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let process;
|
let process;
|
||||||
if (uri == '/save') {
|
let data;
|
||||||
let match;
|
let match;
|
||||||
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
|
let id;
|
||||||
let user = match[1];
|
let app_id = blobId;
|
||||||
let appName = match[2];
|
let packageOwner;
|
||||||
let credentials = await httpd.auth_query(request.headers);
|
let packageName;
|
||||||
if (
|
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
|
||||||
credentials &&
|
packageOwner = match[1];
|
||||||
credentials.session &&
|
packageName = match[2];
|
||||||
(credentials.session.name == user ||
|
let db = new Database(match[1]);
|
||||||
(credentials.permissions.administration && user == 'core'))
|
app_id = await db.get('path:' + match[2]);
|
||||||
) {
|
}
|
||||||
let database = new Database(user);
|
|
||||||
|
|
||||||
let app_object = JSON.parse(utf8Decode(request.body));
|
let app_object = JSON.parse(utf8Decode(await ssb.blobGet(app_id)));
|
||||||
let previous_id = await database.get('path:' + appName);
|
id = app_object?.files[uri.substring(1)];
|
||||||
if (previous_id) {
|
if (!id && app_object?.files['handler.js']) {
|
||||||
try {
|
let answer;
|
||||||
let previous_object = JSON.parse(
|
try {
|
||||||
utf8Decode(await ssb.blobGet(previous_id))
|
answer = await useAppHandler(
|
||||||
);
|
|
||||||
delete previous_object.previous;
|
|
||||||
delete app_object.previous;
|
|
||||||
if (JSON.stringify(previous_object) == JSON.stringify(app_object)) {
|
|
||||||
response.writeHead(200, {
|
|
||||||
'Content-Type': 'text/plain; charset=utf-8',
|
|
||||||
});
|
|
||||||
response.end('/' + previous_id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
app_object.previous = previous_id;
|
|
||||||
let newBlobId = await ssb.blobStore(JSON.stringify(app_object));
|
|
||||||
|
|
||||||
let apps = new Set();
|
|
||||||
let apps_original = await database.get('apps');
|
|
||||||
try {
|
|
||||||
apps = new Set(JSON.parse(apps_original));
|
|
||||||
} catch {}
|
|
||||||
if (!apps.has(appName)) {
|
|
||||||
apps.add(appName);
|
|
||||||
}
|
|
||||||
apps = JSON.stringify([...apps].sort());
|
|
||||||
if (apps != apps_original) {
|
|
||||||
await database.set('apps', apps);
|
|
||||||
}
|
|
||||||
await database.set('path:' + appName, newBlobId);
|
|
||||||
response.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
|
|
||||||
response.end('/' + newBlobId);
|
|
||||||
} else {
|
|
||||||
response.writeHead(401, {'Content-Type': 'text/plain; charset=utf-8'});
|
|
||||||
response.end('401 Unauthorized');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (blobId === '') {
|
|
||||||
let newBlobId = await ssb.blobStore(request.body);
|
|
||||||
response.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
|
|
||||||
response.end('/' + newBlobId);
|
|
||||||
} else {
|
|
||||||
response.writeHead(400, {'Content-Type': 'text/plain; charset=utf-8'});
|
|
||||||
response.end('Invalid name.');
|
|
||||||
}
|
|
||||||
} else if (uri == '/delete') {
|
|
||||||
let match;
|
|
||||||
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
|
|
||||||
let user = match[1];
|
|
||||||
let appName = match[2];
|
|
||||||
let credentials = await httpd.auth_query(request.headers);
|
|
||||||
if (
|
|
||||||
credentials &&
|
|
||||||
credentials.session &&
|
|
||||||
(credentials.session.name == user ||
|
|
||||||
(credentials.permissions.administration && user == 'core'))
|
|
||||||
) {
|
|
||||||
let database = new Database(user);
|
|
||||||
let apps = new Set();
|
|
||||||
try {
|
|
||||||
apps = new Set(JSON.parse(await database.get('apps')));
|
|
||||||
} catch {}
|
|
||||||
if (apps.delete(appName)) {
|
|
||||||
await database.set('apps', JSON.stringify([...apps].sort()));
|
|
||||||
}
|
|
||||||
database.remove('path:' + appName);
|
|
||||||
} else {
|
|
||||||
response.writeHead(401, {'Content-Type': 'text/plain; charset=utf-8'});
|
|
||||||
response.end('401 Unauthorized');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
|
|
||||||
response.end('OK');
|
|
||||||
} else {
|
|
||||||
let data;
|
|
||||||
let match;
|
|
||||||
let id;
|
|
||||||
let app_id = blobId;
|
|
||||||
let packageOwner;
|
|
||||||
let packageName;
|
|
||||||
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
|
|
||||||
packageOwner = match[1];
|
|
||||||
packageName = match[2];
|
|
||||||
let db = new Database(match[1]);
|
|
||||||
app_id = await db.get('path:' + match[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let app_object = JSON.parse(utf8Decode(await ssb.blobGet(app_id)));
|
|
||||||
id = app_object?.files[uri.substring(1)];
|
|
||||||
if (!id && app_object?.files['handler.js']) {
|
|
||||||
let answer;
|
|
||||||
try {
|
|
||||||
answer = await useAppHandler(
|
|
||||||
response,
|
|
||||||
app_id,
|
|
||||||
uri.substring(1),
|
|
||||||
request.query ? form.decodeForm(request.query) : undefined,
|
|
||||||
request.headers,
|
|
||||||
packageOwner,
|
|
||||||
packageName
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
data = utf8Encode(
|
|
||||||
`Internal Server Error\n\n${error?.message}\n${error?.stack}`
|
|
||||||
);
|
|
||||||
response.writeHead(500, {
|
|
||||||
'Content-Type': 'text/plain; charset=utf-8',
|
|
||||||
'Content-Length': data.length,
|
|
||||||
});
|
|
||||||
response.end(data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (answer && typeof answer.data == 'string') {
|
|
||||||
answer.data = utf8Encode(answer.data);
|
|
||||||
}
|
|
||||||
sendData(
|
|
||||||
response,
|
response,
|
||||||
answer?.data,
|
app_id,
|
||||||
answer?.content_type,
|
uri.substring(1),
|
||||||
Object.assign(answer?.headers ?? {}, {
|
request.query ? form.decodeForm(request.query) : undefined,
|
||||||
'Access-Control-Allow-Origin': '*',
|
request.headers,
|
||||||
'Content-Security-Policy': k_content_security_policy,
|
packageOwner,
|
||||||
}),
|
packageName
|
||||||
answer.status_code
|
|
||||||
);
|
);
|
||||||
} else if (id) {
|
} catch (error) {
|
||||||
if (
|
data = utf8Encode(
|
||||||
request.headers['if-none-match'] &&
|
`Internal Server Error\n\n${error?.message}\n${error?.stack}`
|
||||||
request.headers['if-none-match'] == '"' + id + '"'
|
);
|
||||||
) {
|
response.writeHead(500, {
|
||||||
let headers = {
|
'Content-Type': 'text/plain; charset=utf-8',
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Content-Length': data.length,
|
||||||
'Content-Security-Policy': k_content_security_policy,
|
});
|
||||||
'Content-Length': '0',
|
response.end(data);
|
||||||
};
|
return;
|
||||||
response.writeHead(304, headers);
|
|
||||||
response.end();
|
|
||||||
} else {
|
|
||||||
let headers = {
|
|
||||||
ETag: '"' + id + '"',
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
'Content-Security-Policy': k_content_security_policy,
|
|
||||||
};
|
|
||||||
data = await ssb.blobGet(id);
|
|
||||||
let type =
|
|
||||||
httpd.mime_type_from_extension(uri) ||
|
|
||||||
httpd.mime_type_from_magic_bytes(data);
|
|
||||||
sendData(response, data, type, headers);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
sendData(response, data, undefined, {});
|
|
||||||
}
|
}
|
||||||
|
if (answer && typeof answer.data == 'string') {
|
||||||
|
answer.data = utf8Encode(answer.data);
|
||||||
|
}
|
||||||
|
sendData(
|
||||||
|
response,
|
||||||
|
answer?.data,
|
||||||
|
answer?.content_type,
|
||||||
|
Object.assign(answer?.headers ?? {}, {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Content-Security-Policy': k_content_security_policy,
|
||||||
|
}),
|
||||||
|
answer.status_code
|
||||||
|
);
|
||||||
|
} else if (id) {
|
||||||
|
if (
|
||||||
|
request.headers['if-none-match'] &&
|
||||||
|
request.headers['if-none-match'] == '"' + id + '"'
|
||||||
|
) {
|
||||||
|
let headers = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Content-Security-Policy': k_content_security_policy,
|
||||||
|
'Content-Length': '0',
|
||||||
|
};
|
||||||
|
response.writeHead(304, headers);
|
||||||
|
response.end();
|
||||||
|
} else {
|
||||||
|
let headers = {
|
||||||
|
ETag: '"' + id + '"',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Content-Security-Policy': k_content_security_policy,
|
||||||
|
};
|
||||||
|
data = await ssb.blobGet(id);
|
||||||
|
let type =
|
||||||
|
httpd.mime_type_from_extension(uri) ||
|
||||||
|
httpd.mime_type_from_magic_bytes(data);
|
||||||
|
sendData(response, data, type, headers);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sendData(response, data, undefined, {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1175,8 +1080,6 @@ loadSettings()
|
|||||||
(match = /^\/([&\%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(request.uri))
|
(match = /^\/([&\%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(request.uri))
|
||||||
) {
|
) {
|
||||||
return blobHandler(request, response, match[1], match[2]);
|
return blobHandler(request, response, match[1], match[2]);
|
||||||
} else if ((match = /^(.*)(\/(?:save|delete)?)$/.exec(request.uri))) {
|
|
||||||
return blobHandler(request, response, match[1], match[2]);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let port = httpd.start(tildefriends.http_port);
|
let port = httpd.start(tildefriends.http_port);
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
* Command-line publishing of SSB messages.
|
|
||||||
* Minor SSB replication improvements.
|
|
||||||
* Disallow rich text paste on more browsers.
|
|
||||||
* The identity app lets you change the server account.
|
|
||||||
* Fix SSB profile icons stretching.
|
|
||||||
* Fixed the iPhone app missing data.
|
|
||||||
* Added a button to initiate sync.
|
|
||||||
* Fixed the AppImage.
|
|
||||||
* Android version compatibility fixes.
|
|
||||||
* Updates:
|
|
||||||
* CodeMirror
|
|
||||||
* CommonMark 0.31.2
|
|
||||||
* Lit 3.2.1
|
|
||||||
* OpenSSL 3.4.0
|
|
||||||
* c-ares 1.34.2
|
|
||||||
* libbacktrace
|
|
||||||
* libuv 1.49.2
|
|
||||||
* sqlite 3.47.0
|
|
19
metadata/en-US/changelogs/29.txt
Normal file
19
metadata/en-US/changelogs/29.txt
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
* Command-line publishing.
|
||||||
|
* SSB replication improvements.
|
||||||
|
* Disallow rich text paste more.
|
||||||
|
* identity app lets you change the server account.
|
||||||
|
* SSB profile icons stretching.
|
||||||
|
* Fixed iPhone app missing data.
|
||||||
|
* Added an initiate sync button.
|
||||||
|
* Fixed the AppImage.
|
||||||
|
* Flatpak WIP.
|
||||||
|
* Android compatibility fixes.
|
||||||
|
* Updates:
|
||||||
|
* CodeMirror
|
||||||
|
* CommonMark 0.31.2
|
||||||
|
* Lit 3.2.1
|
||||||
|
* OpenSSL 3.4.0
|
||||||
|
* c-ares 1.34.2
|
||||||
|
* libbacktrace
|
||||||
|
* libuv 1.49.2
|
||||||
|
* sqlite 3.47.0
|
@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.unprompted.tildefriends"
|
package="com.unprompted.tildefriends"
|
||||||
android:versionCode="28"
|
android:versionCode="29"
|
||||||
android:versionName="0.0.24-wip">
|
android:versionName="0.0.24">
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<application
|
<application
|
||||||
|
26
src/com.unprompted.tildefriends.yml
Normal file
26
src/com.unprompted.tildefriends.yml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
id: com.unprompted.tildefriends
|
||||||
|
runtime: org.freedesktop.Platform
|
||||||
|
runtime-version: '23.08'
|
||||||
|
sdk: org.freedesktop.Sdk
|
||||||
|
command: tildefriends-run.sh
|
||||||
|
finish-args:
|
||||||
|
- --share=network
|
||||||
|
- --filesystem=xdg-data/applications/tildefriends
|
||||||
|
modules:
|
||||||
|
- name: tildefriends
|
||||||
|
buildsystem: simple
|
||||||
|
build-commands:
|
||||||
|
- make release out/data.zip
|
||||||
|
- install -Dm755 out/release/tildefriends /app/bin/tildefriends
|
||||||
|
- install -D out/data.zip /app/share/data.zip
|
||||||
|
- install -Dm755 tildefriends-run.sh /app/bin/tildefriends-run.sh
|
||||||
|
sources:
|
||||||
|
- type: git
|
||||||
|
url: https://dev.tildefriends.net/cory/tildefriends.git
|
||||||
|
dest: .
|
||||||
|
commit: main
|
||||||
|
- type: script
|
||||||
|
dest-filename: tildefriends-run.sh
|
||||||
|
commands:
|
||||||
|
- mkdir -p ~/.local/share/applications/tildefriends/
|
||||||
|
- exec tildefriends run -z /app/share/data.zip -d ~/.local/share/applications/tildefriends/db.sqlite
|
@ -407,6 +407,11 @@ static void _http_add_body_bytes(tf_http_connection_t* connection, const void* d
|
|||||||
|
|
||||||
if (connection->body_length == connection->content_length)
|
if (connection->body_length == connection->content_length)
|
||||||
{
|
{
|
||||||
|
/* Null-terminate for convenience. */
|
||||||
|
if (connection->body)
|
||||||
|
{
|
||||||
|
((char*)connection->body)[connection->body_length] = '\0';
|
||||||
|
}
|
||||||
tf_http_request_t* request = tf_malloc(sizeof(tf_http_request_t));
|
tf_http_request_t* request = tf_malloc(sizeof(tf_http_request_t));
|
||||||
*request = (tf_http_request_t) {
|
*request = (tf_http_request_t) {
|
||||||
.http = connection->http,
|
.http = connection->http,
|
||||||
@ -500,7 +505,7 @@ static size_t _http_on_read_plain_internal(tf_http_connection_t* connection, con
|
|||||||
|
|
||||||
if (connection->content_length)
|
if (connection->content_length)
|
||||||
{
|
{
|
||||||
connection->body = tf_realloc(connection->body, connection->content_length);
|
connection->body = tf_realloc(connection->body, connection->content_length + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_http_find_handler(connection->http, connection->path, &connection->callback, &connection->trace_name, &connection->user_data) || !connection->callback)
|
if (!_http_find_handler(connection->http, connection->path, &connection->callback, &connection->trace_name, &connection->user_data) || !connection->callback)
|
||||||
|
318
src/httpd.js.c
318
src/httpd.js.c
@ -39,6 +39,7 @@ const int64_t k_refresh_interval = 1ULL * 7 * 24 * 60 * 60 * 1000;
|
|||||||
|
|
||||||
static JSValue _authenticate_jwt(tf_ssb_t* ssb, JSContext* context, const char* jwt);
|
static JSValue _authenticate_jwt(tf_ssb_t* ssb, JSContext* context, const char* jwt);
|
||||||
static JSValue _httpd_websocket_upgrade(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
|
static JSValue _httpd_websocket_upgrade(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
|
||||||
|
static bool _is_name_valid(const char* name);
|
||||||
static const char* _make_session_jwt(JSContext* context, tf_ssb_t* ssb, const char* name);
|
static const char* _make_session_jwt(JSContext* context, tf_ssb_t* ssb, const char* name);
|
||||||
static const char* _make_set_session_cookie_header(tf_http_request_t* request, const char* session_cookie);
|
static const char* _make_set_session_cookie_header(tf_http_request_t* request, const char* session_cookie);
|
||||||
const char** _form_data_decode(const char* data, int length);
|
const char** _form_data_decode(const char* data, int length);
|
||||||
@ -965,6 +966,56 @@ static void _httpd_endpoint_static(tf_http_request_t* request)
|
|||||||
tf_file_stat(task, path, _httpd_endpoint_static_stat, request);
|
tf_file_stat(task, path, _httpd_endpoint_static_stat, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef struct _user_app_t
|
||||||
|
{
|
||||||
|
const char* user;
|
||||||
|
const char* app;
|
||||||
|
} user_app_t;
|
||||||
|
|
||||||
|
static user_app_t* _parse_user_app_from_path(const char* path, const char* expected_suffix)
|
||||||
|
{
|
||||||
|
if (!path || path[0] != '/' || path[1] != '~')
|
||||||
|
{
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t length = strlen(path);
|
||||||
|
size_t suffix_length = expected_suffix ? strlen(expected_suffix) : 0;
|
||||||
|
if (length < suffix_length || strcmp(path + length - suffix_length, expected_suffix) != 0)
|
||||||
|
{
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* slash = strchr(path + 2, '/');
|
||||||
|
if (!slash)
|
||||||
|
{
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* user = path + 2;
|
||||||
|
size_t user_length = (size_t)(slash - user);
|
||||||
|
const char* app = slash + 1;
|
||||||
|
size_t app_length = (size_t)(length - suffix_length - user_length - 3);
|
||||||
|
user_app_t* result = tf_malloc(sizeof(user_app_t) + user_length + 1 + app_length + 1);
|
||||||
|
|
||||||
|
*result = (user_app_t) {
|
||||||
|
.user = (char*)(result + 1),
|
||||||
|
.app = (char*)(result + 1) + user_length + 1,
|
||||||
|
};
|
||||||
|
memcpy((char*)result->user, user, user_length);
|
||||||
|
((char*)result->user)[user_length] = '\0';
|
||||||
|
memcpy((char*)result->app, app, app_length);
|
||||||
|
((char*)result->app)[app_length] = '\0';
|
||||||
|
|
||||||
|
if (!_is_name_valid(result->user) || !_is_name_valid(result->app))
|
||||||
|
{
|
||||||
|
tf_free(result);
|
||||||
|
result = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
typedef struct _view_t
|
typedef struct _view_t
|
||||||
{
|
{
|
||||||
tf_http_request_t* request;
|
tf_http_request_t* request;
|
||||||
@ -995,24 +1046,23 @@ static void _httpd_endpoint_view_work(tf_ssb_t* ssb, void* user_data)
|
|||||||
view_t* view = user_data;
|
view_t* view = user_data;
|
||||||
tf_http_request_t* request = view->request;
|
tf_http_request_t* request = view->request;
|
||||||
char blob_id[256] = "";
|
char blob_id[256] = "";
|
||||||
if (request->path[0] == '/' && request->path[1] == '~')
|
|
||||||
|
user_app_t* user_app = _parse_user_app_from_path(request->path, "/view");
|
||||||
|
if (user_app)
|
||||||
{
|
{
|
||||||
char user[256] = "";
|
size_t app_path_length = strlen("path:") + strlen(user_app->app) + 1;
|
||||||
char path[1024] = "";
|
char* app_path = tf_malloc(app_path_length);
|
||||||
const char* slash = strchr(request->path + 2, '/');
|
snprintf(app_path, app_path_length, "path:%s", user_app->app);
|
||||||
if (slash)
|
const char* value = tf_ssb_db_get_property(ssb, user_app->user, app_path);
|
||||||
{
|
snprintf(blob_id, sizeof(blob_id), "%s", value);
|
||||||
snprintf(user, sizeof(user), "%.*s", (int)(slash - (request->path + 2)), request->path + 2);
|
tf_free(app_path);
|
||||||
snprintf(path, sizeof(path), "path:%.*s", (int)(strlen(slash + 1) - strlen("/view")), slash + 1);
|
tf_free((void*)value);
|
||||||
const char* value = tf_ssb_db_get_property(ssb, user, path);
|
|
||||||
snprintf(blob_id, sizeof(blob_id), "%s", value);
|
|
||||||
tf_free((void*)value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (request->path[0] == '/' && request->path[1] == '&')
|
else if (request->path[0] == '/' && request->path[1] == '&')
|
||||||
{
|
{
|
||||||
snprintf(blob_id, sizeof(blob_id), "%.*s", (int)(strlen(request->path) - strlen("/view") - 1), request->path + 1);
|
snprintf(blob_id, sizeof(blob_id), "%.*s", (int)(strlen(request->path) - strlen("/view") - 1), request->path + 1);
|
||||||
}
|
}
|
||||||
|
tf_free(user_app);
|
||||||
|
|
||||||
if (*blob_id)
|
if (*blob_id)
|
||||||
{
|
{
|
||||||
@ -1081,6 +1131,245 @@ static void _httpd_endpoint_view(tf_http_request_t* request)
|
|||||||
tf_ssb_run_work(ssb, _httpd_endpoint_view_work, _httpd_endpoint_view_after_work, view);
|
tf_ssb_run_work(ssb, _httpd_endpoint_view_work, _httpd_endpoint_view_after_work, view);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef struct _save_t
|
||||||
|
{
|
||||||
|
tf_http_request_t* request;
|
||||||
|
int response;
|
||||||
|
char blob_id[256];
|
||||||
|
} save_t;
|
||||||
|
|
||||||
|
static void _httpd_endpoint_save_work(tf_ssb_t* ssb, void* user_data)
|
||||||
|
{
|
||||||
|
save_t* save = user_data;
|
||||||
|
tf_http_request_t* request = save->request;
|
||||||
|
const char* session = tf_http_get_cookie(tf_http_request_get_header(request, "cookie"), "session");
|
||||||
|
|
||||||
|
JSMallocFunctions funcs = { 0 };
|
||||||
|
tf_get_js_malloc_functions(&funcs);
|
||||||
|
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
|
||||||
|
JSContext* context = JS_NewContext(runtime);
|
||||||
|
|
||||||
|
JSValue jwt = _authenticate_jwt(ssb, context, session);
|
||||||
|
JSValue user = JS_GetPropertyStr(context, jwt, "name");
|
||||||
|
const char* user_string = JS_ToCString(context, user);
|
||||||
|
|
||||||
|
if (user_string && _is_name_valid(user_string))
|
||||||
|
{
|
||||||
|
user_app_t* user_app = _parse_user_app_from_path(request->path, "/save");
|
||||||
|
if (user_app)
|
||||||
|
{
|
||||||
|
if (strcmp(user_string, user_app->user) == 0 || (strcmp(user_app->user, "core") == 0 && tf_ssb_db_user_has_permission(ssb, user_string, "administration")))
|
||||||
|
{
|
||||||
|
size_t path_length = strlen("path:") + strlen(user_app->app) + 1;
|
||||||
|
char* app_path = tf_malloc(path_length);
|
||||||
|
snprintf(app_path, path_length, "path:%s", user_app->app);
|
||||||
|
|
||||||
|
const char* old_blob_id = tf_ssb_db_get_property(ssb, user_app->user, app_path);
|
||||||
|
|
||||||
|
JSValue new_app = JS_ParseJSON(context, request->body, request->content_length, NULL);
|
||||||
|
tf_util_report_error(context, new_app);
|
||||||
|
if (JS_IsObject(new_app))
|
||||||
|
{
|
||||||
|
uint8_t* old_blob = NULL;
|
||||||
|
size_t old_blob_size = 0;
|
||||||
|
if (tf_ssb_db_blob_get(ssb, old_blob_id, &old_blob, &old_blob_size))
|
||||||
|
{
|
||||||
|
JSValue old_app = JS_ParseJSON(context, (const char*)old_blob, old_blob_size, NULL);
|
||||||
|
if (JS_IsObject(old_app))
|
||||||
|
{
|
||||||
|
JSAtom previous = JS_NewAtom(context, "previous");
|
||||||
|
JS_DeleteProperty(context, old_app, previous, 0);
|
||||||
|
JS_DeleteProperty(context, new_app, previous, 0);
|
||||||
|
|
||||||
|
JSValue old_app_json = JS_JSONStringify(context, old_app, JS_NULL, JS_NULL);
|
||||||
|
JSValue new_app_json = JS_JSONStringify(context, new_app, JS_NULL, JS_NULL);
|
||||||
|
const char* old_app_str = JS_ToCString(context, old_app_json);
|
||||||
|
const char* new_app_str = JS_ToCString(context, new_app_json);
|
||||||
|
|
||||||
|
if (old_app_str && new_app_str && strcmp(old_app_str, new_app_str) == 0)
|
||||||
|
{
|
||||||
|
snprintf(save->blob_id, sizeof(save->blob_id), "/%s", old_blob_id);
|
||||||
|
save->response = 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
JS_FreeCString(context, old_app_str);
|
||||||
|
JS_FreeCString(context, new_app_str);
|
||||||
|
JS_FreeValue(context, old_app_json);
|
||||||
|
JS_FreeValue(context, new_app_json);
|
||||||
|
JS_FreeAtom(context, previous);
|
||||||
|
}
|
||||||
|
JS_FreeValue(context, old_app);
|
||||||
|
tf_free(old_blob);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!save->response)
|
||||||
|
{
|
||||||
|
if (old_blob_id)
|
||||||
|
{
|
||||||
|
JS_SetPropertyStr(context, new_app, "previous", JS_NewString(context, old_blob_id));
|
||||||
|
}
|
||||||
|
JSValue new_app_json = JS_JSONStringify(context, new_app, JS_NULL, JS_NULL);
|
||||||
|
size_t new_app_length = 0;
|
||||||
|
const char* new_app_str = JS_ToCStringLen(context, &new_app_length, new_app_json);
|
||||||
|
|
||||||
|
char blob_id[250] = { 0 };
|
||||||
|
if (tf_ssb_db_blob_store(ssb, (const uint8_t*)new_app_str, new_app_length, blob_id, sizeof(blob_id), NULL) &&
|
||||||
|
tf_ssb_db_set_property(ssb, user_app->user, app_path, blob_id))
|
||||||
|
{
|
||||||
|
tf_ssb_db_add_value_to_array_property(ssb, user_app->user, "apps", user_app->app);
|
||||||
|
snprintf(save->blob_id, sizeof(save->blob_id), "/%s", blob_id);
|
||||||
|
save->response = 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
JS_FreeCString(context, new_app_str);
|
||||||
|
JS_FreeValue(context, new_app_json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
save->response = 400;
|
||||||
|
}
|
||||||
|
JS_FreeValue(context, new_app);
|
||||||
|
|
||||||
|
tf_free(app_path);
|
||||||
|
tf_free((void*)old_blob_id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
save->response = 401;
|
||||||
|
}
|
||||||
|
tf_free(user_app);
|
||||||
|
}
|
||||||
|
else if (strcmp(request->path, "/save") == 0)
|
||||||
|
{
|
||||||
|
char blob_id[250] = { 0 };
|
||||||
|
if (tf_ssb_db_blob_store(ssb, request->body, request->content_length, blob_id, sizeof(blob_id), NULL))
|
||||||
|
{
|
||||||
|
snprintf(save->blob_id, sizeof(save->blob_id), "/%s", blob_id);
|
||||||
|
save->response = 200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
save->response = 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tf_free((void*)session);
|
||||||
|
JS_FreeCString(context, user_string);
|
||||||
|
JS_FreeValue(context, user);
|
||||||
|
JS_FreeValue(context, jwt);
|
||||||
|
JS_FreeContext(context);
|
||||||
|
JS_FreeRuntime(runtime);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _httpd_endpoint_save_after_work(tf_ssb_t* ssb, int status, void* user_data)
|
||||||
|
{
|
||||||
|
save_t* save = user_data;
|
||||||
|
tf_http_request_t* request = save->request;
|
||||||
|
if (*save->blob_id)
|
||||||
|
{
|
||||||
|
tf_http_respond(request, 200, NULL, 0, save->blob_id, strlen(save->blob_id));
|
||||||
|
}
|
||||||
|
tf_http_request_unref(request);
|
||||||
|
tf_free(save);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _httpd_endpoint_save(tf_http_request_t* request)
|
||||||
|
{
|
||||||
|
tf_http_request_ref(request);
|
||||||
|
tf_task_t* task = request->user_data;
|
||||||
|
tf_ssb_t* ssb = tf_task_get_ssb(task);
|
||||||
|
save_t* save = tf_malloc(sizeof(save_t));
|
||||||
|
*save = (save_t) {
|
||||||
|
.request = request,
|
||||||
|
};
|
||||||
|
tf_ssb_run_work(ssb, _httpd_endpoint_save_work, _httpd_endpoint_save_after_work, save);
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef struct _delete_t
|
||||||
|
{
|
||||||
|
tf_http_request_t* request;
|
||||||
|
const char* session;
|
||||||
|
int response;
|
||||||
|
} delete_t;
|
||||||
|
|
||||||
|
static void _httpd_endpoint_delete_work(tf_ssb_t* ssb, void* user_data)
|
||||||
|
{
|
||||||
|
delete_t* delete = user_data;
|
||||||
|
tf_http_request_t* request = delete->request;
|
||||||
|
|
||||||
|
JSMallocFunctions funcs = { 0 };
|
||||||
|
tf_get_js_malloc_functions(&funcs);
|
||||||
|
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
|
||||||
|
JSContext* context = JS_NewContext(runtime);
|
||||||
|
|
||||||
|
JSValue jwt = _authenticate_jwt(ssb, context, delete->session);
|
||||||
|
JSValue user = JS_GetPropertyStr(context, jwt, "name");
|
||||||
|
const char* user_string = JS_ToCString(context, user);
|
||||||
|
if (user_string && _is_name_valid(user_string))
|
||||||
|
{
|
||||||
|
user_app_t* user_app = _parse_user_app_from_path(request->path, "/delete");
|
||||||
|
if (user_app)
|
||||||
|
{
|
||||||
|
if (strcmp(user_string, user_app->user) == 0 || (strcmp(user_app->user, "core") == 0 && tf_ssb_db_user_has_permission(ssb, user_string, "administration")))
|
||||||
|
{
|
||||||
|
size_t path_length = strlen("path:") + strlen(user_app->app) + 1;
|
||||||
|
char* app_path = tf_malloc(path_length);
|
||||||
|
snprintf(app_path, path_length, "path:%s", user_app->app);
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
changed = tf_ssb_db_remove_value_from_array_property(ssb, user_string, "apps", user_app->app) || changed;
|
||||||
|
changed = tf_ssb_db_remove_property(ssb, user_string, app_path) || changed;
|
||||||
|
delete->response = changed ? 200 : 404;
|
||||||
|
tf_free(app_path);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
delete->response = 401;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
delete->response = 404;
|
||||||
|
}
|
||||||
|
tf_free(user_app);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
delete->response = 401;
|
||||||
|
}
|
||||||
|
|
||||||
|
JS_FreeCString(context, user_string);
|
||||||
|
JS_FreeValue(context, user);
|
||||||
|
JS_FreeValue(context, jwt);
|
||||||
|
JS_FreeContext(context);
|
||||||
|
JS_FreeRuntime(runtime);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _httpd_endpoint_delete_after_work(tf_ssb_t* ssb, int status, void* user_data)
|
||||||
|
{
|
||||||
|
delete_t* delete = user_data;
|
||||||
|
const char* k_payload = tf_http_status_text(delete->response ? delete->response : 404);
|
||||||
|
tf_http_respond(delete->request, delete->response ? delete->response : 404, NULL, 0, k_payload, strlen(k_payload));
|
||||||
|
tf_http_request_unref(delete->request);
|
||||||
|
tf_free((void*)delete->session);
|
||||||
|
tf_free(delete);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _httpd_endpoint_delete(tf_http_request_t* request)
|
||||||
|
{
|
||||||
|
tf_http_request_ref(request);
|
||||||
|
tf_task_t* task = request->user_data;
|
||||||
|
tf_ssb_t* ssb = tf_task_get_ssb(task);
|
||||||
|
delete_t* delete = tf_malloc(sizeof(delete_t));
|
||||||
|
*delete = (delete_t) {
|
||||||
|
.request = request,
|
||||||
|
.session = tf_http_get_cookie(tf_http_request_get_header(request, "cookie"), "session"),
|
||||||
|
};
|
||||||
|
tf_ssb_run_work(ssb, _httpd_endpoint_delete_work, _httpd_endpoint_delete_after_work, delete);
|
||||||
|
}
|
||||||
|
|
||||||
static void _httpd_endpoint_root_callback(const char* path, void* user_data)
|
static void _httpd_endpoint_root_callback(const char* path, void* user_data)
|
||||||
{
|
{
|
||||||
tf_http_request_t* request = user_data;
|
tf_http_request_t* request = user_data;
|
||||||
@ -1402,7 +1691,7 @@ static JSValue _authenticate_jwt(tf_ssb_t* ssb, JSContext* context, const char*
|
|||||||
static bool _session_is_authenticated_as_user(JSContext* context, JSValue session)
|
static bool _session_is_authenticated_as_user(JSContext* context, JSValue session)
|
||||||
{
|
{
|
||||||
bool result = false;
|
bool result = false;
|
||||||
JSValue user = JS_GetPropertyStr(context, session, "user");
|
JSValue user = JS_GetPropertyStr(context, session, "name");
|
||||||
const char* user_string = JS_ToCString(context, user);
|
const char* user_string = JS_ToCString(context, user);
|
||||||
result = user_string && strcmp(user_string, "guest") != 0;
|
result = user_string && strcmp(user_string, "guest") != 0;
|
||||||
JS_FreeCString(context, user_string);
|
JS_FreeCString(context, user_string);
|
||||||
@ -1813,6 +2102,9 @@ void tf_httpd_register(JSContext* context)
|
|||||||
tf_http_add_handler(http, "/~*/*/", _httpd_endpoint_static, NULL, task);
|
tf_http_add_handler(http, "/~*/*/", _httpd_endpoint_static, NULL, task);
|
||||||
tf_http_add_handler(http, "/&*.sha256/", _httpd_endpoint_static, NULL, task);
|
tf_http_add_handler(http, "/&*.sha256/", _httpd_endpoint_static, NULL, task);
|
||||||
tf_http_add_handler(http, "/*/view", _httpd_endpoint_view, NULL, task);
|
tf_http_add_handler(http, "/*/view", _httpd_endpoint_view, NULL, task);
|
||||||
|
tf_http_add_handler(http, "/~*/*/save", _httpd_endpoint_save, NULL, task);
|
||||||
|
tf_http_add_handler(http, "/~*/*/delete", _httpd_endpoint_delete, NULL, task);
|
||||||
|
tf_http_add_handler(http, "/save", _httpd_endpoint_save, NULL, task);
|
||||||
|
|
||||||
tf_http_add_handler(http, "/robots.txt", _httpd_endpoint_robots_txt, NULL, NULL);
|
tf_http_add_handler(http, "/robots.txt", _httpd_endpoint_robots_txt, NULL, NULL);
|
||||||
tf_http_add_handler(http, "/debug", _httpd_endpoint_debug, NULL, task);
|
tf_http_add_handler(http, "/debug", _httpd_endpoint_debug, NULL, task);
|
||||||
|
32
src/ssb.c
32
src/ssb.c
@ -2450,6 +2450,20 @@ static void _tf_ssb_on_handle_close(uv_handle_t* handle)
|
|||||||
|
|
||||||
static void _tf_ssb_on_timer_close(uv_handle_t* handle)
|
static void _tf_ssb_on_timer_close(uv_handle_t* handle)
|
||||||
{
|
{
|
||||||
|
tf_ssb_timer_t* timer = handle->data;
|
||||||
|
for (int i = 0; i < timer->ssb->timers_count; i++)
|
||||||
|
{
|
||||||
|
if (timer->ssb->timers[i] == timer)
|
||||||
|
{
|
||||||
|
timer->ssb->timers[i] = timer->ssb->timers[--timer->ssb->timers_count];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (timer->ssb->shutting_down && !timer->ssb->timers_count)
|
||||||
|
{
|
||||||
|
tf_free(timer->ssb->timers);
|
||||||
|
timer->ssb->timers = NULL;
|
||||||
|
}
|
||||||
tf_free(handle->data);
|
tf_free(handle->data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2503,14 +2517,11 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
|
|||||||
{
|
{
|
||||||
uv_close((uv_handle_t*)&ssb->timers[i]->timer, _tf_ssb_on_timer_close);
|
uv_close((uv_handle_t*)&ssb->timers[i]->timer, _tf_ssb_on_timer_close);
|
||||||
}
|
}
|
||||||
ssb->timers_count = 0;
|
|
||||||
tf_free(ssb->timers);
|
|
||||||
ssb->timers = NULL;
|
|
||||||
|
|
||||||
tf_printf("Waiting for closes.\n");
|
tf_printf("Waiting for closes.\n");
|
||||||
|
|
||||||
while (ssb->broadcast_listener.data || ssb->broadcast_sender.data || ssb->broadcast_timer.data || ssb->broadcast_cleanup_timer.data || ssb->trace_timer.data ||
|
while (ssb->broadcast_listener.data || ssb->broadcast_sender.data || ssb->broadcast_timer.data || ssb->broadcast_cleanup_timer.data || ssb->trace_timer.data ||
|
||||||
ssb->server.data || ssb->ref_count || ssb->request_activity_timer.data)
|
ssb->server.data || ssb->ref_count || ssb->request_activity_timer.data || ssb->timers_count)
|
||||||
{
|
{
|
||||||
uv_run(ssb->loop, UV_RUN_ONCE);
|
uv_run(ssb->loop, UV_RUN_ONCE);
|
||||||
}
|
}
|
||||||
@ -4197,19 +4208,16 @@ static void _tf_ssb_scheduled_timer(uv_timer_t* handle)
|
|||||||
{
|
{
|
||||||
tf_ssb_timer_t* timer = handle->data;
|
tf_ssb_timer_t* timer = handle->data;
|
||||||
timer->callback(timer->ssb, timer->user_data);
|
timer->callback(timer->ssb, timer->user_data);
|
||||||
for (int i = 0; i < timer->ssb->timers_count; i++)
|
|
||||||
{
|
|
||||||
if (timer->ssb->timers[i] == timer)
|
|
||||||
{
|
|
||||||
timer->ssb->timers[i] = timer->ssb->timers[--timer->ssb->timers_count];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
uv_close((uv_handle_t*)handle, _tf_ssb_on_timer_close);
|
uv_close((uv_handle_t*)handle, _tf_ssb_on_timer_close);
|
||||||
}
|
}
|
||||||
|
|
||||||
void tf_ssb_schedule_work(tf_ssb_t* ssb, int delay_ms, void (*callback)(tf_ssb_t* ssb, void* user_data), void* user_data)
|
void tf_ssb_schedule_work(tf_ssb_t* ssb, int delay_ms, void (*callback)(tf_ssb_t* ssb, void* user_data), void* user_data)
|
||||||
{
|
{
|
||||||
|
if (ssb->shutting_down)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ssb->timers = tf_resize_vec(ssb->timers, sizeof(uv_timer_t*) * (ssb->timers_count + 1));
|
ssb->timers = tf_resize_vec(ssb->timers, sizeof(uv_timer_t*) * (ssb->timers_count + 1));
|
||||||
tf_ssb_timer_t* timer = tf_malloc(sizeof(tf_ssb_timer_t));
|
tf_ssb_timer_t* timer = tf_malloc(sizeof(tf_ssb_timer_t));
|
||||||
*timer = (tf_ssb_timer_t)
|
*timer = (tf_ssb_timer_t)
|
||||||
|
@ -21,7 +21,7 @@ typedef struct _tf_ssb_connections_t
|
|||||||
|
|
||||||
static void _tf_ssb_connections_changed_callback(tf_ssb_t* ssb, tf_ssb_change_t change, tf_ssb_connection_t* connection, void* user_data)
|
static void _tf_ssb_connections_changed_callback(tf_ssb_t* ssb, tf_ssb_change_t change, tf_ssb_connection_t* connection, void* user_data)
|
||||||
{
|
{
|
||||||
if (!connection)
|
if (!connection || tf_ssb_is_shutting_down(ssb))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
59
src/ssb.db.c
59
src/ssb.db.c
@ -1787,6 +1787,65 @@ bool tf_ssb_db_set_property(tf_ssb_t* ssb, const char* id, const char* key, cons
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool tf_ssb_db_remove_property(tf_ssb_t* ssb, const char* id, const char* key)
|
||||||
|
{
|
||||||
|
bool result = false;
|
||||||
|
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
|
||||||
|
sqlite3_stmt* statement = NULL;
|
||||||
|
if (sqlite3_prepare(db, "DELETE FROM properties WHERE id = ? AND key = ?", -1, &statement, NULL) == SQLITE_OK)
|
||||||
|
{
|
||||||
|
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, key, -1, NULL) == SQLITE_OK)
|
||||||
|
{
|
||||||
|
result = sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) != 0;
|
||||||
|
}
|
||||||
|
sqlite3_finalize(statement);
|
||||||
|
}
|
||||||
|
tf_ssb_release_db_writer(ssb, db);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool tf_ssb_db_remove_value_from_array_property(tf_ssb_t* ssb, const char* id, const char* key, const char* value)
|
||||||
|
{
|
||||||
|
bool result = false;
|
||||||
|
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
|
||||||
|
sqlite3_stmt* statement = NULL;
|
||||||
|
if (sqlite3_prepare(db,
|
||||||
|
"UPDATE properties SET value = json_remove(properties.value, entry.fullkey) FROM json_each(properties.value) AS entry WHERE properties.id = ? AND properties.key = ? "
|
||||||
|
"AND entry.value = ?",
|
||||||
|
-1, &statement, NULL) == SQLITE_OK)
|
||||||
|
{
|
||||||
|
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, key, -1, NULL) == SQLITE_OK &&
|
||||||
|
sqlite3_bind_text(statement, 3, value, -1, NULL) == SQLITE_OK)
|
||||||
|
{
|
||||||
|
result = sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) != 0;
|
||||||
|
}
|
||||||
|
sqlite3_finalize(statement);
|
||||||
|
}
|
||||||
|
tf_ssb_release_db_writer(ssb, db);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool tf_ssb_db_add_value_to_array_property(tf_ssb_t* ssb, const char* id, const char* key, const char* value)
|
||||||
|
{
|
||||||
|
bool result = false;
|
||||||
|
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
|
||||||
|
sqlite3_stmt* statement = NULL;
|
||||||
|
if (sqlite3_prepare(db,
|
||||||
|
"INSERT INTO properties (id, key, value) VALUES (?1, ?2, json_array(?3)) ON CONFLICT DO UPDATE SET value = json_insert(properties.value, '$[#]', ?3) WHERE "
|
||||||
|
"properties.id = ?1 AND properties.key = ?2 AND NOT EXISTS (SELECT 1 FROM json_each(properties.value) AS entry WHERE entry.value = ?3)",
|
||||||
|
-1, &statement, NULL) == SQLITE_OK)
|
||||||
|
{
|
||||||
|
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, key, -1, NULL) == SQLITE_OK &&
|
||||||
|
sqlite3_bind_text(statement, 3, value, -1, NULL) == SQLITE_OK)
|
||||||
|
{
|
||||||
|
result = sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) != 0;
|
||||||
|
}
|
||||||
|
sqlite3_finalize(statement);
|
||||||
|
}
|
||||||
|
tf_ssb_release_db_writer(ssb, db);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
bool tf_ssb_db_identity_get_active(sqlite3* db, const char* user, const char* package_owner, const char* package_name, char* out_identity, size_t out_identity_size)
|
bool tf_ssb_db_identity_get_active(sqlite3* db, const char* user, const char* package_owner, const char* package_name, char* out_identity, size_t out_identity_size)
|
||||||
{
|
{
|
||||||
sqlite3_stmt* statement = NULL;
|
sqlite3_stmt* statement = NULL;
|
||||||
|
29
src/ssb.db.h
29
src/ssb.db.h
@ -399,6 +399,35 @@ const char* tf_ssb_db_get_property(tf_ssb_t* ssb, const char* id, const char* ke
|
|||||||
*/
|
*/
|
||||||
bool tf_ssb_db_set_property(tf_ssb_t* ssb, const char* id, const char* key, const char* value);
|
bool tf_ssb_db_set_property(tf_ssb_t* ssb, const char* id, const char* key, const char* value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
** Remove an entry in the properties table.
|
||||||
|
** @param ssb The SSB instance.
|
||||||
|
** @param id The user.
|
||||||
|
** @param key The property key.
|
||||||
|
** @return true if the property was removed successfully.
|
||||||
|
*/
|
||||||
|
bool tf_ssb_db_remove_property(tf_ssb_t* ssb, const char* id, const char* key);
|
||||||
|
|
||||||
|
/**
|
||||||
|
** Remove a value from an entry in the properties table that is a JSON array.
|
||||||
|
** @param ssb The SSB instance.
|
||||||
|
** @param id The user.
|
||||||
|
** @param key The property key.
|
||||||
|
** @param value The value to remove from the JSON array.
|
||||||
|
** @return true if the property was updated.
|
||||||
|
*/
|
||||||
|
bool tf_ssb_db_remove_value_from_array_property(tf_ssb_t* ssb, const char* id, const char* key, const char* value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
** Ensure a value is in an entry in the properties table that is a JSON array.
|
||||||
|
** @param ssb The SSB instance.
|
||||||
|
** @param id The user.
|
||||||
|
** @param key The property key.
|
||||||
|
** @param value The value to add to the JSON array.
|
||||||
|
** @return true if the property was updated.
|
||||||
|
*/
|
||||||
|
bool tf_ssb_db_add_value_to_array_property(tf_ssb_t* ssb, const char* id, const char* key, const char* value);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
** Resolve a hostname to its index path by global settings.
|
** Resolve a hostname to its index path by global settings.
|
||||||
** @param ssb The SSB instance.
|
** @param ssb The SSB instance.
|
||||||
|
@ -158,6 +158,29 @@ void tf_ssb_test_ssb(const tf_test_options_t* options)
|
|||||||
tf_ssb_t* ssb1 = tf_ssb_create(&loop, NULL, "file:out/test_db1.sqlite", NULL);
|
tf_ssb_t* ssb1 = tf_ssb_create(&loop, NULL, "file:out/test_db1.sqlite", NULL);
|
||||||
tf_ssb_register(tf_ssb_get_context(ssb1), ssb1);
|
tf_ssb_register(tf_ssb_get_context(ssb1), ssb1);
|
||||||
|
|
||||||
|
const char* value = tf_ssb_db_get_property(ssb0, "user", "array");
|
||||||
|
assert(value == NULL);
|
||||||
|
assert(tf_ssb_db_add_value_to_array_property(ssb0, "user", "array", "1") == true);
|
||||||
|
value = tf_ssb_db_get_property(ssb0, "user", "array");
|
||||||
|
assert(strcmp(value, "[\"1\"]") == 0);
|
||||||
|
tf_free((void*)value);
|
||||||
|
assert(tf_ssb_db_add_value_to_array_property(ssb0, "user", "array", "2") == true);
|
||||||
|
value = tf_ssb_db_get_property(ssb0, "user", "array");
|
||||||
|
assert(strcmp(value, "[\"1\",\"2\"]") == 0);
|
||||||
|
tf_free((void*)value);
|
||||||
|
assert(tf_ssb_db_add_value_to_array_property(ssb0, "user", "array", "1") == false);
|
||||||
|
assert(tf_ssb_db_add_value_to_array_property(ssb0, "user", "array", "2") == false);
|
||||||
|
value = tf_ssb_db_get_property(ssb0, "user", "array");
|
||||||
|
assert(strcmp(value, "[\"1\",\"2\"]") == 0);
|
||||||
|
tf_free((void*)value);
|
||||||
|
assert(tf_ssb_db_remove_value_from_array_property(ssb0, "user", "array", "1") == true);
|
||||||
|
assert(tf_ssb_db_remove_value_from_array_property(ssb0, "user", "array", "1") == false);
|
||||||
|
assert(tf_ssb_db_remove_value_from_array_property(ssb0, "user", "array", "2") == true);
|
||||||
|
assert(tf_ssb_db_remove_value_from_array_property(ssb0, "user", "array", "2") == false);
|
||||||
|
value = tf_ssb_db_get_property(ssb0, "user", "array");
|
||||||
|
assert(strcmp(value, "[]") == 0);
|
||||||
|
tf_free((void*)value);
|
||||||
|
|
||||||
uv_idle_t idle0 = { .data = ssb0 };
|
uv_idle_t idle0 = { .data = ssb0 };
|
||||||
uv_idle_init(&loop, &idle0);
|
uv_idle_init(&loop, &idle0);
|
||||||
uv_idle_start(&idle0, _ssb_test_idle);
|
uv_idle_start(&idle0, _ssb_test_idle);
|
||||||
|
@ -1869,6 +1869,7 @@ void tf_task_destroy(tf_task_t* task)
|
|||||||
{
|
{
|
||||||
JSValue global = JS_GetGlobalObject(task->_context);
|
JSValue global = JS_GetGlobalObject(task->_context);
|
||||||
JS_SetPropertyStr(task->_context, global, "httpd", JS_UNDEFINED);
|
JS_SetPropertyStr(task->_context, global, "httpd", JS_UNDEFINED);
|
||||||
|
JS_SetPropertyStr(task->_context, global, "gProcesses", JS_NewObject(task->_context));
|
||||||
JS_FreeValue(task->_context, global);
|
JS_FreeValue(task->_context, global);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
#define VERSION_NUMBER "0.0.24-wip"
|
#define VERSION_NUMBER "0.0.24"
|
||||||
#define VERSION_NAME "Honey bunches of boats."
|
#define VERSION_NAME "Honey bunches of boats."
|
||||||
|
@ -67,6 +67,7 @@ try:
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
driver.switch_to.default_content()
|
||||||
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'edit').click()
|
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'edit').click()
|
||||||
editor = driver.find_element(By.ID, 'editor').find_element(By.CLASS_NAME, 'cm-content')
|
editor = driver.find_element(By.ID, 'editor').find_element(By.CLASS_NAME, 'cm-content')
|
||||||
editor.click()
|
editor.click()
|
||||||
@ -77,6 +78,17 @@ try:
|
|||||||
driver.switch_to.frame(driver.find_element(By.ID, 'document'))
|
driver.switch_to.frame(driver.find_element(By.ID, 'document'))
|
||||||
wait.until(expected_conditions.presence_of_element_located((By.ID, 'test-div')))
|
wait.until(expected_conditions.presence_of_element_located((By.ID, 'test-div')))
|
||||||
|
|
||||||
|
driver.switch_to.default_content()
|
||||||
|
editor = driver.find_element(By.ID, 'editor').find_element(By.CLASS_NAME, 'cm-content')
|
||||||
|
editor.click()
|
||||||
|
editor.clear()
|
||||||
|
editor.send_keys('app.setDocument("<div id=\'test-div2\'>Hello, world, again!</div>")');
|
||||||
|
driver.find_element(By.ID, 'save').click()
|
||||||
|
|
||||||
|
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
|
||||||
|
driver.switch_to.frame(driver.find_element(By.ID, 'document'))
|
||||||
|
wait.until(expected_conditions.presence_of_element_located((By.ID, 'test-div2')))
|
||||||
|
|
||||||
driver.switch_to.default_content()
|
driver.switch_to.default_content()
|
||||||
driver.find_element(By.ID, 'delete').click()
|
driver.find_element(By.ID, 'delete').click()
|
||||||
wait.until(expected_conditions.alert_is_present()).accept()
|
wait.until(expected_conditions.alert_is_present()).accept()
|
||||||
@ -193,7 +205,13 @@ try:
|
|||||||
|
|
||||||
wait.until(expected_conditions.presence_of_element_located((By.ID, 'content')))
|
wait.until(expected_conditions.presence_of_element_located((By.ID, 'content')))
|
||||||
driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))))
|
driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))))
|
||||||
wait.until(expected_conditions.presence_of_element_located((By.TAG_NAME, 'tf-app'))).shadow_root
|
# NoSuchShadowRootException
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
tf_app = wait.until(expected_conditions.presence_of_element_located((By.TAG_NAME, 'tf-app'))).shadow_root
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
driver.switch_to.default_content()
|
driver.switch_to.default_content()
|
||||||
|
|
||||||
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'logout').click()
|
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'logout').click()
|
||||||
|
Reference in New Issue
Block a user