/* ** Copyright (c) 2020 D. Richard Hipp ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the Simplified BSD License (also ** known as the "2-Clause License" or "FreeBSD License".) ** ** 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. ** ** Author contact information: ** drh@hwaci.com ** http://www.hwaci.com/drh/ ** ******************************************************************************* ** ** This file contains code used to implement the Fossil chatroom. ** ** Initial design goals: ** ** * Keep it simple. This chatroom is not intended as a competitor ** or replacement for IRC, Discord, Telegram, Slack, etc. The goal ** is zero- or near-zero-configuration, not an abundance of features. ** ** * Intended as a place for insiders to have ephemeral conversations ** about a project. This is not a public gather place. Think ** "boardroom", not "corner pub". ** ** * One chatroom per repository. ** ** * Chat content lives in a single repository. It is never synced. ** Content expires and is deleted after a set interval (a week or so). ** ** Notification is accomplished using the "hanging GET" or "long poll" design ** in which a GET request is issued but the server does not send a reply until ** new content arrives. Newer Web Sockets and Server Sent Event protocols are ** more elegant, but are not compatible with CGI, and would thus complicate ** configuration. */ #include "config.h" #include #include "chat.h" /* ** Outputs JS code to initialize a list of chat alert audio files for ** use by the chat front-end client. A handful of builtin files ** (from alerts/\*.wav) and all unversioned files matching ** alert-sounds/\*.{mp3,ogg,wav} are included. */ static void chat_emit_alert_list(void){ unsigned int i; const char * azBuiltins[] = { "builtin/alerts/plunk.wav", "builtin/alerts/bflat2.wav", "builtin/alerts/bflat3.wav", "builtin/alerts/bloop.wav" }; CX("window.fossil.config.chat.alerts = [\n"); for(i=0; i < sizeof(azBuiltins)/sizeof(azBuiltins[0]); ++i){ CX("%s%!j", i ? ", " : "", azBuiltins[i]); } if( db_table_exists("repository","unversioned") ){ Stmt q = empty_Stmt; db_prepare(&q, "SELECT 'uv/'||name FROM unversioned " "WHERE content IS NOT NULL " "AND (name LIKE 'alert-sounds/%%.wav' " "OR name LIKE 'alert-sounds/%%.mp3' " "OR name LIKE 'alert-sounds/%%.ogg')"); while(SQLITE_ROW==db_step(&q)){ CX(", %!j", db_column_text(&q, 0)); } db_finalize(&q); } CX("\n];\n"); } /* Settings that can be used to control chat */ /* ** SETTING: chat-initial-history width=10 default=50 ** ** If this setting has an integer value of N, then when /chat first ** starts up it initializes the screen with the N most recent chat ** messages. If N is zero, then all chat messages are loaded. */ /* ** SETTING: chat-keep-count width=10 default=50 ** ** When /chat is cleaning up older messages, it will always keep ** the most recent chat-keep-count messages, even if some of those ** messages are older than the discard threshold. If this value ** is zero, then /chat is free to delete all historic messages once ** they are old enough. */ /* ** SETTING: chat-keep-days width=10 default=7 ** ** The /chat subsystem will try to discard messages that are older then ** chat-keep-days. The value of chat-keep-days can be a floating point ** number. So, for example, if you only want to keep chat messages for ** 12 hours, set this value to 0.5. ** ** A value of 0.0 or less means that messages are retained forever. */ /* ** SETTING: chat-inline-images boolean default=on ** ** Specifies whether posted images in /chat should default to being ** displayed inline or as downloadable links. Each chat user can ** change this value for their current chat session in the UI. */ /* ** SETTING: chat-poll-timeout width=10 default=420 ** ** On an HTTP request to /chat-poll, if there is no new content available, ** the reply is delayed waiting for new content to arrive. (This is the ** "long poll" strategy of event delivery to the client.) This setting ** determines approximately how long /chat-poll will delay before giving ** up and returning an empty reply. The default value is about 7 minutes, ** which works well for Fossil behind the althttpd web server. Other ** server environments may choose a longer or shorter delay. ** ** For maximum efficiency, it is best to choose the longest delay that ** does not cause timeouts in intermediate proxies or web server. */ /* ** SETTING: chat-alert-sound width=10 ** ** This is the name of the builtin sound file to use for the alert tone. ** The value must be the name of a builtin WAV file. */ /* ** WEBPAGE: chat ** ** Start up a browser-based chat session. ** ** This is the main page that humans use to access the chatroom. Simply ** point a web-browser at /chat and the screen fills with the latest ** chat messages, and waits for new one. ** ** Other /chat-OP pages are used by XHR requests from this page to ** send new chat message, delete older messages, or poll for changes. */ void chat_webpage(void){ char *zAlert; char *zProjectName; char * zInputPlaceholder0; /* Common text input placeholder value */ const char *zPaperclip = ""; login_check_credentials(); if( !g.perm.Chat ){ login_needed(g.anon.Chat); return; } zAlert = mprintf("%s/builtin/%s", g.zBaseURL, db_get("chat-alert-sound","alerts/plunk.wav")); zProjectName = db_get("project-name","Unnamed project"); zInputPlaceholder0 = mprintf("Type markdown-formatted message for %h.", zProjectName); style_set_current_feature("chat"); style_header("Chat"); @
@
@ @ @ @
@ 👁 @ %s(zPaperclip) @ @ 📤 @
@
@
@ @
@
@
@ @ @ @
/* New chat messages get inserted immediately after this element */ @ @
fossil_free(zProjectName); fossil_free(zInputPlaceholder0); builtin_fossil_js_bundle_or("popupwidget", "storage", "fetch", "pikchr", "confirmer", NULL); /* Always in-line the javascript for the chat page */ @ builtin_request_js("fossil.page.chat.js"); style_finish_page(); } /* Definition of repository tables used by chat */ static const char zChatSchema1[] = @ CREATE TABLE repository.chat( @ msgid INTEGER PRIMARY KEY AUTOINCREMENT, @ mtime JULIANDAY, -- Time for this entry - Julianday Zulu @ lmtime TEXT, -- Client YYYY-MM-DDZHH:MM:SS when message originally sent @ xfrom TEXT, -- Login of the sender @ xmsg TEXT, -- Raw, unformatted text of the message @ fname TEXT, -- Filename of the uploaded file, or NULL @ fmime TEXT, -- MIMEType of the upload file, or NULL @ mdel INT, -- msgid of another message to delete @ file BLOB -- Text of the uploaded file, or NULL @ ); ; /* ** Make sure the repository data tables used by chat exist. Create them ** if they do not. */ static void chat_create_tables(void){ if( !db_table_exists("repository","chat") ){ db_multi_exec(zChatSchema1/*works-like:""*/); }else if( !db_table_has_column("repository","chat","lmtime") ){ if( !db_table_has_column("repository","chat","mdel") ){ db_multi_exec("ALTER TABLE chat ADD COLUMN mdel INT"); } db_multi_exec("ALTER TABLE chat ADD COLUMN lmtime TEXT"); } } /* ** Delete old content from the chat table. */ static void chat_purge(void){ int mxCnt = db_get_int("chat-keep-count",50); double mxDays = atof(db_get("chat-keep-days","7")); double rAge; int msgid; rAge = db_double(0.0, "SELECT julianday('now')-mtime FROM chat" " ORDER BY msgid LIMIT 1"); if( rAge>mxDays ){ msgid = db_int(0, "SELECT msgid FROM chat" " ORDER BY msgid DESC LIMIT 1 OFFSET %d", mxCnt); if( msgid>0 ){ Stmt s; db_multi_exec("PRAGMA secure_delete=ON;"); db_prepare(&s, "DELETE FROM chat WHERE mtimelogging in.\""); if(fAsMessageList){ CX("}]}"); }else{ CX("}"); } fossil_free(zTime); } /* ** WEBPAGE: chat-send hidden ** ** This page receives (via XHR) a new chat-message and/or a new file ** to be entered into the chat history. ** ** On success it responds with an empty response: the new message ** should be fetched via /chat-poll. On error, e.g. login expiry, ** it emits a JSON response in the same form as described for ** /chat-poll errors, but as a standalone object instead of a ** list of objects. ** ** Requests to this page should be POST, not GET. POST parameters ** include: ** ** msg The (Markdown) text of the message to be sent ** ** file The content of the file attachment ** ** lmtime ISO-8601 formatted date-time string showing the local time ** of the sender. ** ** At least one of the "msg" or "file" POST parameters must be provided. */ void chat_send_webpage(void){ int nByte; const char *zMsg; const char *zUserName; login_check_credentials(); if( 0==g.perm.Chat ) { chat_emit_permissions_error(0); return; } chat_create_tables(); zUserName = (g.zLogin && g.zLogin[0]) ? g.zLogin : "nobody"; nByte = atoi(PD("file:bytes","0")); zMsg = PD("msg",""); db_begin_write(); chat_purge(); if( nByte==0 ){ if( zMsg[0] ){ db_multi_exec( "INSERT INTO chat(mtime,lmtime,xfrom,xmsg)" "VALUES(julianday('now'),%Q,%Q,%Q)", P("lmtime"), zUserName, zMsg ); } }else{ Stmt q; Blob b; db_prepare(&q, "INSERT INTO chat(mtime,lmtime,xfrom,xmsg,file,fname,fmime)" "VALUES(julianday('now'),%Q,%Q,%Q,:file,%Q,%Q)", P("lmtime"), zUserName, zMsg, PD("file:filename",""), PD("file:mimetype","application/octet-stream")); blob_init(&b, P("file"), nByte); db_bind_blob(&q, ":file", &b); db_step(&q); db_finalize(&q); blob_reset(&b); } db_commit_transaction(); } /* ** This routine receives raw (user-entered) message text and transforms ** it into HTML that is safe to insert using innerHTML. As of 2021-09-19, ** it does so by using markdown_to_html() to convert markdown-formatted ** zMsg to HTML. ** ** Space to hold the returned string is obtained from fossil_malloc() ** and must be freed by the caller. */ static char *chat_format_to_html(const char *zMsg){ Blob out; blob_init(&out, "", 0); if(*zMsg){ Blob bIn; blob_init(&bIn, zMsg, (int)strlen(zMsg)); markdown_to_html(&bIn, NULL, &out); } return blob_str(&out); } /* ** COMMAND: test-chat-formatter ** ** Usage: %fossil test-chat-formatter STRING ... ** ** Transform each argument string into HTML that will display the ** chat message. This is used to test the formatter and to verify ** that a malicious message text will not cause HTML or JS injection ** into the chat display in a browser. */ void chat_test_formatter_cmd(void){ int i; char *zOut; db_find_and_open_repository(0,0); g.perm.Hyperlink = 1; for(i=0; i