|
1
|
/* |
|
2
|
** Copyright (c) 2020 D. Richard Hipp |
|
3
|
** |
|
4
|
** This program is free software; you can redistribute it and/or |
|
5
|
** modify it under the terms of the Simplified BSD License (also |
|
6
|
** known as the "2-Clause License" or "FreeBSD License".) |
|
7
|
** |
|
8
|
** This program is distributed in the hope that it will be useful, |
|
9
|
** but without any warranty; without even the implied warranty of |
|
10
|
** merchantability or fitness for a particular purpose. |
|
11
|
** |
|
12
|
** Author contact information: |
|
13
|
** [email protected] |
|
14
|
** http://www.hwaci.com/drh/ |
|
15
|
** |
|
16
|
******************************************************************************* |
|
17
|
** |
|
18
|
** This file contains code used to implement the Fossil chatroom. |
|
19
|
** |
|
20
|
** Initial design goals: |
|
21
|
** |
|
22
|
** * Keep it simple. This chatroom is not intended as a competitor |
|
23
|
** or replacement for IRC, Discord, Telegram, Slack, etc. The goal |
|
24
|
** is zero- or near-zero-configuration, not an abundance of features. |
|
25
|
** |
|
26
|
** * Intended as a place for insiders to have ephemeral conversations |
|
27
|
** about a project. This is not a public gather place. Think |
|
28
|
** "boardroom", not "corner pub". |
|
29
|
** |
|
30
|
** * One chatroom per repository. |
|
31
|
** |
|
32
|
** * Chat content lives in a single repository. It is never synced. |
|
33
|
** Content expires and is deleted after a set interval (a week or so). |
|
34
|
** |
|
35
|
** Notification is accomplished using the "hanging GET" or "long poll" design |
|
36
|
** in which a GET request is issued but the server does not send a reply until |
|
37
|
** new content arrives. Newer Web Sockets and Server Sent Event protocols are |
|
38
|
** more elegant, but are not compatible with CGI, and would thus complicate |
|
39
|
** configuration. |
|
40
|
*/ |
|
41
|
#include "config.h" |
|
42
|
#include <assert.h> |
|
43
|
#include "chat.h" |
|
44
|
|
|
45
|
/* |
|
46
|
** Outputs JS code to initialize a list of chat alert audio files for |
|
47
|
** use by the chat front-end client. A handful of builtin files |
|
48
|
** (from alerts/\*.wav) and all unversioned files matching |
|
49
|
** alert-sounds/\*.{mp3,ogg,wav} are included. |
|
50
|
*/ |
|
51
|
static void chat_emit_alert_list(void){ |
|
52
|
unsigned int i; |
|
53
|
const char * azBuiltins[] = { |
|
54
|
"builtin/alerts/plunk.wav", |
|
55
|
"builtin/alerts/bflat2.wav", |
|
56
|
"builtin/alerts/bflat3.wav", |
|
57
|
"builtin/alerts/bloop.wav" |
|
58
|
}; |
|
59
|
CX("window.fossil.config.chat.alerts = [\n"); |
|
60
|
for(i=0; i < sizeof(azBuiltins)/sizeof(azBuiltins[0]); ++i){ |
|
61
|
CX("%s%!j", i ? ", " : "", azBuiltins[i]); |
|
62
|
} |
|
63
|
if( db_table_exists("repository","unversioned") ){ |
|
64
|
Stmt q = empty_Stmt; |
|
65
|
db_prepare(&q, "SELECT 'uv/'||name FROM unversioned " |
|
66
|
"WHERE content IS NOT NULL " |
|
67
|
"AND (name LIKE 'alert-sounds/%%.wav' " |
|
68
|
"OR name LIKE 'alert-sounds/%%.mp3' " |
|
69
|
"OR name LIKE 'alert-sounds/%%.ogg')"); |
|
70
|
while(SQLITE_ROW==db_step(&q)){ |
|
71
|
CX(", %!j", db_column_text(&q, 0)); |
|
72
|
} |
|
73
|
db_finalize(&q); |
|
74
|
} |
|
75
|
CX("\n];\n"); |
|
76
|
} |
|
77
|
|
|
78
|
/* Settings that can be used to control chat */ |
|
79
|
/* |
|
80
|
** SETTING: chat-initial-history width=10 default=50 |
|
81
|
** |
|
82
|
** If this setting has an integer value of N, then when /chat first |
|
83
|
** starts up it initializes the screen with the N most recent chat |
|
84
|
** messages. If N is zero, then all chat messages are loaded. |
|
85
|
*/ |
|
86
|
/* |
|
87
|
** SETTING: chat-keep-count width=10 default=50 |
|
88
|
** |
|
89
|
** When /chat is cleaning up older messages, it will always keep |
|
90
|
** the most recent chat-keep-count messages, even if some of those |
|
91
|
** messages are older than the discard threshold. If this value |
|
92
|
** is zero, then /chat is free to delete all historic messages once |
|
93
|
** they are old enough. |
|
94
|
*/ |
|
95
|
/* |
|
96
|
** SETTING: chat-keep-days width=10 default=7 |
|
97
|
** |
|
98
|
** The /chat subsystem will try to discard messages that are older then |
|
99
|
** chat-keep-days. The value of chat-keep-days can be a floating point |
|
100
|
** number. So, for example, if you only want to keep chat messages for |
|
101
|
** 12 hours, set this value to 0.5. |
|
102
|
** |
|
103
|
** A value of 0.0 or less means that messages are retained forever. |
|
104
|
*/ |
|
105
|
/* |
|
106
|
** SETTING: chat-inline-images boolean default=on |
|
107
|
** |
|
108
|
** Specifies whether posted images in /chat should default to being |
|
109
|
** displayed inline or as downloadable links. Each chat user can |
|
110
|
** change this value for their current chat session in the UI. |
|
111
|
*/ |
|
112
|
/* |
|
113
|
** SETTING: chat-poll-timeout width=10 default=420 |
|
114
|
** |
|
115
|
** On an HTTP request to /chat-poll, if there is no new content available, |
|
116
|
** the reply is delayed waiting for new content to arrive. (This is the |
|
117
|
** "long poll" strategy of event delivery to the client.) This setting |
|
118
|
** determines approximately how long /chat-poll will delay before giving |
|
119
|
** up and returning an empty reply. The default value is about 7 minutes, |
|
120
|
** which works well for Fossil behind the althttpd web server. Other |
|
121
|
** server environments may choose a longer or shorter delay. |
|
122
|
** |
|
123
|
** For maximum efficiency, it is best to choose the longest delay that |
|
124
|
** does not cause timeouts in intermediate proxies or web server. |
|
125
|
*/ |
|
126
|
/* |
|
127
|
** SETTING: chat-alert-sound width=10 |
|
128
|
** |
|
129
|
** This is the name of the builtin sound file to use for the alert tone. |
|
130
|
** The value must be the name of a builtin WAV file. |
|
131
|
*/ |
|
132
|
/* |
|
133
|
** SETTING: chat-timeline-user width=10 |
|
134
|
** |
|
135
|
** If this setting is defined and is not an empty string, then |
|
136
|
** timeline events are posted to the chat as they arrive. The synthesized |
|
137
|
** chat messages appear to come from the user identified by this setting, |
|
138
|
** not the user on the timeline event. |
|
139
|
** |
|
140
|
** All chat messages that come from the chat-timeline-user are |
|
141
|
** interpreted as text/x-fossil-wiki instead of as text/x-markdown. |
|
142
|
** For this reason, the chat-timeline-user name should probably not be |
|
143
|
** a real user. |
|
144
|
*/ |
|
145
|
/* |
|
146
|
** WEBPAGE: chat loadavg-exempt |
|
147
|
** |
|
148
|
** Start up a browser-based chat session. |
|
149
|
** |
|
150
|
** This is the main page that humans use to access the chatroom. Simply |
|
151
|
** point a web-browser at /chat and the screen fills with the latest |
|
152
|
** chat messages, and waits for new ones. |
|
153
|
** |
|
154
|
** Other /chat-OP pages are used by XHR requests from this page to |
|
155
|
** send new chat message, delete older messages, or poll for changes. |
|
156
|
*/ |
|
157
|
void chat_webpage(void){ |
|
158
|
char *zAlert; |
|
159
|
char *zProjectName; |
|
160
|
char * zInputPlaceholder0; /* Common text input placeholder value */ |
|
161
|
|
|
162
|
login_check_credentials(); |
|
163
|
if( !g.perm.Chat ){ |
|
164
|
login_needed(g.anon.Chat); |
|
165
|
return; |
|
166
|
} |
|
167
|
zAlert = mprintf("%s/builtin/%s", g.zBaseURL, |
|
168
|
db_get("chat-alert-sound","alerts/plunk.wav")); |
|
169
|
zProjectName = db_get("project-name","Unnamed project"); |
|
170
|
zInputPlaceholder0 = |
|
171
|
mprintf("Type markdown-formatted message for %h.", zProjectName); |
|
172
|
style_set_current_feature("chat"); |
|
173
|
style_header("Chat"); |
|
174
|
@ <div id='chat-input-area'> |
|
175
|
@ <div id='chat-input-line-wrapper' class='compact'> |
|
176
|
@ <input type="text" id="chat-input-field-single" \ |
|
177
|
@ data-placeholder0="%h(zInputPlaceholder0)" \ |
|
178
|
@ data-placeholder="%h(zInputPlaceholder0)" \ |
|
179
|
@ class="chat-input-field"></input> |
|
180
|
@ <textarea id="chat-input-field-multi" \ |
|
181
|
@ data-placeholder0="%h(zInputPlaceholder0)" \ |
|
182
|
@ data-placeholder="%h(zInputPlaceholder0)" \ |
|
183
|
@ class="chat-input-field hidden"></textarea> |
|
184
|
@ <div contenteditable id="chat-input-field-x" \ |
|
185
|
@ data-placeholder0="%h(zInputPlaceholder0)" \ |
|
186
|
@ data-placeholder="%h(zInputPlaceholder0)" \ |
|
187
|
@ class="chat-input-field hidden"></div> |
|
188
|
@ <div id='chat-buttons-wrapper'> |
|
189
|
@ <span class='cbutton' id="chat-button-preview" \ |
|
190
|
@ title="Preview message (Shift-Enter)">👁</span> |
|
191
|
@ <span class='cbutton' id="chat-button-search" \ |
|
192
|
@ title="Search chat history">🔍</span> |
|
193
|
@ <span class='cbutton' id="chat-button-attach" \ |
|
194
|
@ title="Attach file to message">📎</span> |
|
195
|
@ <span class='cbutton' id="chat-button-settings" \ |
|
196
|
@ title="Configure chat">⚙</span> |
|
197
|
@ <span class='cbutton' id="chat-button-submit" \ |
|
198
|
@ title="Send message (Ctrl-Enter)">📤</span> |
|
199
|
@ </div> |
|
200
|
@ </div> |
|
201
|
@ <div id='chat-input-file-area'> |
|
202
|
@ <div class='file-selection-wrapper hidden'> |
|
203
|
@ <input type="file" name="file" id="chat-input-file"> |
|
204
|
@ </div> |
|
205
|
@ <div id="chat-drop-details"></div> |
|
206
|
@ </div> |
|
207
|
@ </div> |
|
208
|
@ <div id='chat-user-list-wrapper' class='hidden'> |
|
209
|
@ <div class='legend'> |
|
210
|
@ <span class='help-buttonlet'> |
|
211
|
@ Users who have messages in the currently-loaded list.<br><br> |
|
212
|
@ <strong>Tap a user name</strong> to filter messages |
|
213
|
@ on that user and tap again to clear the filter.<br><br> |
|
214
|
@ <strong>Tap the title</strong> of this widget to toggle |
|
215
|
@ the list on and off. |
|
216
|
@ </span> |
|
217
|
@ <span>Active users (sorted by last message time)</span> |
|
218
|
@ </div> |
|
219
|
@ <div id='chat-user-list'></div> |
|
220
|
@ </div> |
|
221
|
@ <div id='chat-preview' class='hidden chat-view'> |
|
222
|
@ <header>Preview: (<a href='%R/md_rules' target='_blank'>markdown reference</a>)</header> |
|
223
|
@ <div id='chat-preview-content'></div> |
|
224
|
@ <div class='button-bar'><button class='action-close'>Close Preview</button></div> |
|
225
|
@ </div> |
|
226
|
@ <div id='chat-config' class='hidden chat-view'> |
|
227
|
@ <div id='chat-config-options'></div> |
|
228
|
/* ^^^populated client-side */ |
|
229
|
@ <div class='button-bar'><button class='action-close'>Close Settings</button></div> |
|
230
|
@ </div> |
|
231
|
@ <div id='chat-search' class='hidden chat-view'> |
|
232
|
@ <div id='chat-search-content'></div> |
|
233
|
/* ^^^populated client-side */ |
|
234
|
@ <div class='button-bar'> |
|
235
|
@ <button class='action-clear'>Clear results</button> |
|
236
|
@ <button class='action-close'>Close Search</button> |
|
237
|
@ </div> |
|
238
|
@ </div> |
|
239
|
@ <div id='chat-messages-wrapper' class='chat-view'> |
|
240
|
/* New chat messages get inserted immediately after this element */ |
|
241
|
@ <span id='message-inject-point'></span> |
|
242
|
@ </div> |
|
243
|
@ <div id='chat-zoom' class='hidden chat-view'> |
|
244
|
@ <div id='chat-zoom-content'></div> |
|
245
|
@ <div class='button-bar'><button class='action-close'>Close Zoom</button></div> |
|
246
|
@ </div> |
|
247
|
@ <span id='chat-zoom-marker' class='hidden'><!-- placeholder marker for zoomed msg --></span> |
|
248
|
fossil_free(zProjectName); |
|
249
|
fossil_free(zInputPlaceholder0); |
|
250
|
builtin_fossil_js_bundle_or("popupwidget", "storage", "fetch", |
|
251
|
"pikchr", "confirmer", "copybutton", |
|
252
|
NULL); |
|
253
|
/* Always in-line the javascript for the chat page */ |
|
254
|
@ <script nonce="%h(style_nonce())">/* chat.c:%d(__LINE__) */ |
|
255
|
/* We need an onload handler to ensure that window.fossil is |
|
256
|
initialized before the chat init code runs. */ |
|
257
|
@ window.addEventListener('load', function(){ |
|
258
|
@ document.body.classList.add('chat'); |
|
259
|
@ /*^^^for skins which add their own BODY tag */; |
|
260
|
@ window.fossil.config.chat = { |
|
261
|
@ fromcli: %h(PB("cli")?"true":"false"), |
|
262
|
@ alertSound: "%h(zAlert)", |
|
263
|
@ initSize: %d(db_get_int("chat-initial-history",50)), |
|
264
|
@ imagesInline: !!%d(db_get_boolean("chat-inline-images",1)), |
|
265
|
@ pollTimeout: %d(db_get_int("chat-poll-timeout",420)) |
|
266
|
@ }; |
|
267
|
ajax_emit_js_preview_modes(0); |
|
268
|
chat_emit_alert_list(); |
|
269
|
@ }, false); |
|
270
|
@ </script> |
|
271
|
builtin_request_js("fossil.page.chat.js"); |
|
272
|
style_finish_page(); |
|
273
|
} |
|
274
|
|
|
275
|
/* |
|
276
|
** Definition of repository tables used by chat |
|
277
|
*/ |
|
278
|
static const char zChatSchema1[] = |
|
279
|
@ CREATE TABLE repository.chat( |
|
280
|
@ msgid INTEGER PRIMARY KEY AUTOINCREMENT, |
|
281
|
@ mtime JULIANDAY, -- Time for this entry - Julianday Zulu |
|
282
|
@ lmtime TEXT, -- Client YYYY-MM-DDZHH:MM:SS when message originally sent |
|
283
|
@ xfrom TEXT, -- Login of the sender |
|
284
|
@ xmsg TEXT, -- Raw, unformatted text of the message |
|
285
|
@ fname TEXT, -- Filename of the uploaded file, or NULL |
|
286
|
@ fmime TEXT, -- MIMEType of the upload file, or NULL |
|
287
|
@ mdel INT, -- msgid of another message to delete |
|
288
|
@ file BLOB -- Text of the uploaded file, or NULL |
|
289
|
@ ); |
|
290
|
; |
|
291
|
|
|
292
|
|
|
293
|
/* |
|
294
|
** Create or rebuild the /chat search index. Requires that the |
|
295
|
** repository.chat table exists. If bForce is true, it will drop the |
|
296
|
** chatfts1 table and recreate/reindex it. If bForce is 0, it will |
|
297
|
** only index the chat content if the chatfts1 table does not already |
|
298
|
** exist. |
|
299
|
*/ |
|
300
|
void chat_rebuild_index(int bForce){ |
|
301
|
if( !db_table_exists("repository","chat") ) return; |
|
302
|
if( bForce!=0 ){ |
|
303
|
db_multi_exec("DROP TABLE IF EXISTS chatfts1"); |
|
304
|
} |
|
305
|
if( bForce!=0 || !db_table_exists("repository", "chatfts1") ){ |
|
306
|
const int tokType = search_tokenizer_type(0); |
|
307
|
const char *zTokenizer = search_tokenize_arg_for_type( |
|
308
|
tokType==FTS5TOK_NONE ? FTS5TOK_PORTER : tokType |
|
309
|
/* Special case: if fts search is disabled for the main repo |
|
310
|
** content, use a default tokenizer here. */ |
|
311
|
); |
|
312
|
assert( zTokenizer && zTokenizer[0] ); |
|
313
|
db_multi_exec( |
|
314
|
"CREATE VIRTUAL TABLE repository.chatfts1 USING fts5(" |
|
315
|
" xmsg, content=chat, content_rowid=msgid%s" |
|
316
|
");" |
|
317
|
"INSERT INTO repository.chatfts1(chatfts1) VALUES('rebuild');", |
|
318
|
zTokenizer/*safe-for-%s*/ |
|
319
|
); |
|
320
|
} |
|
321
|
} |
|
322
|
|
|
323
|
/* |
|
324
|
** Make sure the repository data tables used by chat exist. Create |
|
325
|
** them if they do not. Set up TEMP triggers (if needed) to update the |
|
326
|
** chatfts1 table as the chat table is updated. |
|
327
|
*/ |
|
328
|
void chat_create_tables(void){ |
|
329
|
if( !db_table_exists("repository","chat") ){ |
|
330
|
db_multi_exec(zChatSchema1/*works-like:""*/); |
|
331
|
}else if( !db_table_has_column("repository","chat","lmtime") ){ |
|
332
|
if( !db_table_has_column("repository","chat","mdel") ){ |
|
333
|
db_multi_exec("ALTER TABLE chat ADD COLUMN mdel INT"); |
|
334
|
} |
|
335
|
db_multi_exec("ALTER TABLE chat ADD COLUMN lmtime TEXT"); |
|
336
|
} |
|
337
|
chat_rebuild_index(0); |
|
338
|
db_multi_exec( |
|
339
|
"CREATE TEMP TRIGGER IF NOT EXISTS chat_ai AFTER INSERT ON chat BEGIN " |
|
340
|
" INSERT INTO chatfts1(rowid, xmsg) VALUES(new.msgid, new.xmsg);" |
|
341
|
"END;" |
|
342
|
"CREATE TEMP TRIGGER IF NOT EXISTS chat_ad AFTER DELETE ON chat BEGIN " |
|
343
|
" INSERT INTO chatfts1(chatfts1, rowid, xmsg) " |
|
344
|
" VALUES('delete', old.msgid, old.xmsg);" |
|
345
|
"END;" |
|
346
|
); |
|
347
|
} |
|
348
|
|
|
349
|
/* |
|
350
|
** Delete old content from the chat table. |
|
351
|
*/ |
|
352
|
static void chat_purge(void){ |
|
353
|
int mxCnt = db_get_int("chat-keep-coun |
|
354
|
double mxDays = fossil_atof(db_get("chat-keep-days","7")); |
|
355
|
double rAge; |
|
356
|
int msgid; |
|
357
|
rAge = db_double(0.0, "SELECT julianday('now')-mtime FROM chat" |
|
358
|
" ORDER BY msgid LIMIT 1"); |
|
359
|
if( rAge>mxDays ){ |
|
360
|
msgid = db_int(0, "SELECT msgid FROM chat" |
|
361
|
" ORDER BY msgid DESC LIMIT 1 OFFSET %d", mxCnt); |
|
362
|
if( msgid>0 ){ |
|
363
|
Stmt s; |
|
364
|
db_multi_exec("PRAGMA secure_delete=ON;"); |
|
365
|
db_prepare(&s, |
|
366
|
"DELETE FROM chat WHERE mtime<julianday('now')-:mxage" |
|
367
|
" AND msgid<%d", msgid); |
|
368
|
db_bind_double(&s, ":mxage", mxDays); |
|
369
|
db_step(&s); |
|
370
|
db_finalize(&s); |
|
371
|
} |
|
372
|
} |
|
373
|
} |
|
374
|
|
|
375
|
/* |
|
376
|
** Sets the current CGI response type to application/json then emits a |
|
377
|
** JSON-format error message object. If fAsMessageList is true then |
|
378
|
** the object is output using the list format described for chat-poll, |
|
379
|
** else it is emitted as a single object in that same format. |
|
380
|
*/ |
|
381
|
static void chat_emit_permissions_error(int fAsMessageList){ |
|
382
|
char * zTime = cgi_iso8601_datestamp(); |
|
383
|
cgi_set_content_type("application/json"); |
|
384
|
if(fAsMessageList){ |
|
385
|
CX("{\"msgs\":[{"); |
|
386
|
}else{ |
|
387
|
CX("{"); |
|
388
|
} |
|
389
|
CX("\"isError\": true, \"xfrom\": null,"); |
|
390
|
CX("\"mtime\": %!j, \"lmtime\": %!j,", zTime, zTime); |
|
391
|
CX("\"xmsg\": \"Missing permissions or not logged in. " |
|
392
|
"Try <a href='%R/login?g=chat'>logging in</a>.\""); |
|
393
|
if(fAsMessageList){ |
|
394
|
CX("}]}"); |
|
395
|
}else{ |
|
396
|
CX("}"); |
|
397
|
} |
|
398
|
fossil_free(zTime); |
|
399
|
} |
|
400
|
|
|
401
|
/* |
|
402
|
** Like chat_emit_permissions_error() but emits a single |
|
403
|
** /chat-message-format JSON object about a CSRF violation. |
|
404
|
*/ |
|
405
|
static void chat_emit_csrf_error(void){ |
|
406
|
char * zTime = cgi_iso8601_datestamp(); |
|
407
|
cgi_set_content_type("application/json"); |
|
408
|
CX("{"); |
|
409
|
CX("\"isError\": true, \"xfrom\": null,"); |
|
410
|
CX("\"mtime\": %!j, \"lmtime\": %!j,", zTime, zTime); |
|
411
|
CX("\"xmsg\": \"CSRF validation failure.\""); |
|
412
|
CX("}"); |
|
413
|
fossil_free(zTime); |
|
414
|
} |
|
415
|
|
|
416
|
/* |
|
417
|
** WEBPAGE: chat-send hidden loadavg-exempt |
|
418
|
** |
|
419
|
** This page receives (via XHR) a new chat-message and/or a new file |
|
420
|
** to be entered into the chat history. |
|
421
|
** |
|
422
|
** On success it responds with an empty response: the new message |
|
423
|
** should be fetched via /chat-poll. On error, e.g. login expiry, |
|
424
|
** it emits a JSON response in the same form as described for |
|
425
|
** /chat-poll errors, but as a standalone object instead of a |
|
426
|
** list of objects. |
|
427
|
** |
|
428
|
** Requests to this page should be POST, not GET. POST parameters |
|
429
|
** include: |
|
430
|
** |
|
431
|
** msg The (Markdown) text of the message to be sent |
|
432
|
** |
|
433
|
** file The content of the file attachment |
|
434
|
** |
|
435
|
** lmtime ISO-8601 formatted date-time string showing the local time |
|
436
|
** of the sender. |
|
437
|
** |
|
438
|
** At least one of the "msg" or "file" POST parameters must be provided. |
|
439
|
*/ |
|
440
|
void chat_send_webpage(void){ |
|
441
|
int nByte; |
|
442
|
const char *zMsg; |
|
443
|
const char *zUserName; |
|
444
|
login_check_credentials(); |
|
445
|
if( 0==g.perm.Chat ) { |
|
446
|
chat_emit_permissions_error(0); |
|
447
|
return; |
|
448
|
}else if( g.eAuthMethod==AUTH_COOKIE && 0==cgi_csrf_safe(1) ){ |
|
449
|
chat_emit_csrf_error(); |
|
450
|
return; |
|
451
|
} |
|
452
|
zUserName = (g.zLogin && g.zLogin[0]) ? g.zLogin : "nobody"; |
|
453
|
nByte = atoi(PD("file:bytes","0")); |
|
454
|
zMsg = PD("msg",""); |
|
455
|
db_begin_write(); |
|
456
|
db_unprotect(PROTECT_READONLY); |
|
457
|
chat_create_tables(); |
|
458
|
chat_purge(); |
|
459
|
if( nByte==0 ){ |
|
460
|
if( zMsg[0] ){ |
|
461
|
db_multi_exec( |
|
462
|
"INSERT INTO chat(mtime,lmtime,xfrom,xmsg)" |
|
463
|
"VALUES(julianday('now'),%Q,%Q,%Q)", |
|
464
|
P("lmtime"), zUserName, zMsg |
|
465
|
); |
|
466
|
} |
|
467
|
}else{ |
|
468
|
Stmt q; |
|
469
|
Blob b; |
|
470
|
db_prepare(&q, |
|
471
|
"INSERT INTO chat(mtime,lmtime,xfrom,xmsg,file,fname,fmime)" |
|
472
|
"VALUES(julianday('now'),%Q,%Q,%Q,:file,%Q,%Q)", |
|
473
|
P("lmtime"), zUserName, zMsg, PD("file:filename",""), |
|
474
|
PD("file:mimetype","application/octet-stream")); |
|
475
|
blob_init(&b, P("file"), nByte); |
|
476
|
db_bind_blob(&q, ":file", &b); |
|
477
|
db_step(&q); |
|
478
|
db_finalize(&q); |
|
479
|
blob_reset(&b); |
|
480
|
} |
|
481
|
db_commit_transaction(); |
|
482
|
db_protect_pop(); |
|
483
|
} |
|
484
|
|
|
485
|
/* |
|
486
|
** This routine receives raw (user-entered) message text and |
|
487
|
** transforms it into HTML that is safe to insert using innerHTML. As |
|
488
|
** of 2021-09-19, it does so by using wiki_convert() or |
|
489
|
** markdown_to_html() to convert wiki/markdown-formatted zMsg to HTML. |
|
490
|
** |
|
491
|
** Space to hold the returned string is obtained from fossil_malloc() |
|
492
|
** and must be freed by the caller. |
|
493
|
*/ |
|
494
|
static char *chat_format_to_html(const char *zMsg, int isWiki){ |
|
495
|
Blob out; |
|
496
|
blob_init(&out, "", 0); |
|
497
|
if( zMsg==0 || zMsg[0]==0 ){ |
|
498
|
/* No-op */ |
|
499
|
}else if( isWiki ){ |
|
500
|
/* Used for chat-timeline-user. The zMsg is text/x-fossil-wiki. */ |
|
501
|
Blob bIn; |
|
502
|
blob_init(&bIn, zMsg, (int)strlen(zMsg)); |
|
503
|
wiki_convert(&bIn, &out, WIKI_INLINE); |
|
504
|
}else{ |
|
505
|
/* The common case: zMsg is text/x-markdown */ |
|
506
|
Blob bIn; |
|
507
|
blob_init(&bIn, zMsg, (int)strlen(zMsg)); |
|
508
|
markdown_to_html(&bIn, NULL, &out); |
|
509
|
} |
|
510
|
return blob_str(&out); |
|
511
|
} |
|
512
|
|
|
513
|
/* |
|
514
|
** COMMAND: test-chat-formatter |
|
515
|
** |
|
516
|
** Usage: %fossil test-chat-formatter ?OPTIONS? STRING ... |
|
517
|
** |
|
518
|
** Transform each argument string into HTML that will display the |
|
519
|
** chat message. This is used to test the formatter and to verify |
|
520
|
** that a malicious message text will not cause HTML or JS injection |
|
521
|
** into the chat display in a browser. |
|
522
|
** |
|
523
|
** Options: |
|
524
|
** |
|
525
|
** -w|--wiki Assume fossil wiki format instead of markdown |
|
526
|
*/ |
|
527
|
void chat_test_formatter_cmd(void){ |
|
528
|
int i; |
|
529
|
char *zOut; |
|
530
|
int const isWiki = find_option("w","wiki",0)!=0; |
|
531
|
db_find_and_open_repository(0,0); |
|
532
|
g.perm.Hyperlink = 1; |
|
533
|
for(i=2; i<g.argc; i++){ |
|
534
|
zOut = chat_format_to_html(g.argv[i], isWiki); |
|
535
|
fossil_print("[%d]: %s\n", i-1, zOut); |
|
536
|
fossil_free(zOut); |
|
537
|
} |
|
538
|
} |
|
539
|
|
|
540
|
/* |
|
541
|
** The SQL statement passed as the first argument should return zero or |
|
542
|
** more rows of data, each of which represents a single message from the |
|
543
|
** "chat" table. The rows returned should be similar to those returned |
|
544
|
** by: |
|
545
|
** |
|
546
|
** SELECT msgid, |
|
547
|
** datetime(mtime), |
|
548
|
** xfrom, |
|
549
|
** xmsg, |
|
550
|
** octet_length(file)," |
|
551
|
** fname, |
|
552
|
** fmime, |
|
553
|
** mdel, |
|
554
|
** lmtime |
|
555
|
** FROM chat; |
|
556
|
** |
|
557
|
** This function loops through all rows returned by statement p, adding |
|
558
|
** a record to the JSON stored in argument pJson for each. See comments |
|
559
|
** above function chat_poll_webpage() for a description of the JSON records |
|
560
|
** added to pJson. |
|
561
|
*/ |
|
562
|
static int chat_poll_rowstojson( |
|
563
|
Stmt *p, /* Statement to read rows from */ |
|
564
|
int bRaw, /* True to return raw format xmsg */ |
|
565
|
Blob *pJson /* Append json array entries here */ |
|
566
|
){ |
|
567
|
int cnt = 0; |
|
568
|
const char *zChatUser = db_get("chat-timeline-user",0); |
|
569
|
while( db_step(p)==SQLITE_ROW ){ |
|
570
|
int isWiki = 0; /* True if chat message is x-fossil-wiki */ |
|
571
|
int id = db_column_int(p, 0); |
|
572
|
const char *zDate = db_column_text(p, 1); |
|
573
|
const char *zFrom = db_column_text(p, 2); |
|
574
|
const char *zRawMsg = db_column_text(p, 3); |
|
575
|
int nByte = db_column_int(p, 4); |
|
576
|
const char *zFName = db_column_text(p, 5); |
|
577
|
const char *zFMime = db_column_text(p, 6); |
|
578
|
int iToDel = db_column_int(p, 7); |
|
579
|
const char *zLMtime = db_column_text(p, 8); |
|
580
|
char *zMsg; |
|
581
|
if(cnt++){ |
|
582
|
blob_append(pJson, ",\n", 2); |
|
583
|
} |
|
584
|
blob_appendf(pJson, "{\"msgid\":%d,", id); |
|
585
|
blob_appendf(pJson, "\"mtime\":\"%.10sT%sZ\",", zDate, zDate+11); |
|
586
|
if( zLMtime && zLMtime[0] ){ |
|
587
|
blob_appendf(pJson, "\"lmtime\":%!j,", zLMtime); |
|
588
|
} |
|
589
|
blob_append(pJson, "\"xfrom\":", -1); |
|
590
|
if(zFrom){ |
|
591
|
blob_appendf(pJson, "%!j,", zFrom); |
|
592
|
isWiki = fossil_strcmp(zFrom,zChatUser)==0; |
|
593
|
}else{ |
|
594
|
/* see https://fossil-scm.org/forum/forumpost/e0be0eeb4c */ |
|
595
|
blob_appendf(pJson, "null,"); |
|
596
|
isWiki = 0; |
|
597
|
} |
|
598
|
blob_appendf(pJson, "\"uclr\":%!j,", |
|
599
|
isWiki ? "transparent" : user_color(zFrom ? zFrom : "nobody")); |
|
600
|
|
|
601
|
if(bRaw){ |
|
602
|
blob_appendf(pJson, "\"xmsg\":%!j,", zRawMsg); |
|
603
|
}else{ |
|
604
|
zMsg = chat_format_to_html(zRawMsg ? zRawMsg : "", isWiki); |
|
605
|
blob_appendf(pJson, "\"xmsg\":%!j,", zMsg); |
|
606
|
fossil_free(zMsg); |
|
607
|
} |
|
608
|
|
|
609
|
if( nByte==0 ){ |
|
610
|
blob_appendf(pJson, "\"fsize\":0"); |
|
611
|
}else{ |
|
612
|
blob_appendf(pJson, "\"fsize\":%d,\"fname\":%!j,\"fmime\":%!j", |
|
613
|
nByte, zFName, zFMime); |
|
614
|
} |
|
615
|
|
|
616
|
if( iToDel ){ |
|
617
|
blob_appendf(pJson, ",\"mdel\":%d}", iToDel); |
|
618
|
}else{ |
|
619
|
blob_append(pJson, "}", 1); |
|
620
|
} |
|
621
|
} |
|
622
|
db_reset(p); |
|
623
|
|
|
624
|
return cnt; |
|
625
|
} |
|
626
|
|
|
627
|
/* |
|
628
|
** WEBPAGE: chat-poll hidden loadavg-exempt |
|
629
|
** |
|
630
|
** The chat page generated by /chat using an XHR to this page to |
|
631
|
** request new chat content. A typical invocation is: |
|
632
|
** |
|
633
|
** /chat-poll/N |
|
634
|
** /chat-poll?name=N |
|
635
|
** |
|
636
|
** The "name" argument should begin with an integer which is the largest |
|
637
|
** "msgid" that the chat page currently holds. If newer content is |
|
638
|
** available, this routine returns that content straight away. If no new |
|
639
|
** content is available, this webpage blocks until the new content becomes |
|
640
|
** available. In this way, the system implements "hanging-GET" or "long-poll" |
|
641
|
** style event notification. If no new content arrives after a delay of |
|
642
|
** approximately chat-poll-timeout seconds (default: 420), then reply is |
|
643
|
** sent with an empty "msg": field. |
|
644
|
** |
|
645
|
** If N is negative, then the return value is the N most recent messages. |
|
646
|
** Hence a request like /chat-poll/-100 can be used to initialize a new |
|
647
|
** chat session to just the most recent messages. |
|
648
|
** |
|
649
|
** Some webservers (althttpd) do not allow a term of the URL path to |
|
650
|
** begin with "-". Then /chat-poll/-100 cannot be used. Instead you |
|
651
|
** have to say "/chat-poll?name=-100". |
|
652
|
** |
|
653
|
** If the integer parameter "before" is passed in, it is assumed that |
|
654
|
** the client is requesting older messages, up to (but not including) |
|
655
|
** that message ID, in which case the next-oldest "n" messages |
|
656
|
** (default=chat-initial-history setting, equivalent to n=0) are |
|
657
|
** returned (negative n fetches all older entries). The client then |
|
658
|
** needs to take care to inject them at the end of the history rather |
|
659
|
** than the same place new messages go. |
|
660
|
** |
|
661
|
** If "before" is provided, "name" is ignored. |
|
662
|
** |
|
663
|
** If "raw" is provided, the "xmsg" text is sent back as-is, in |
|
664
|
** markdown format, rather than being HTML-ized. This is not used or |
|
665
|
** supported by fossil's own chat client but is intended for 3rd-party |
|
666
|
** clients. (Specifically, for Brad Harder's curses-based client.) |
|
667
|
** |
|
668
|
** The reply from this webpage is JSON that describes the new content. |
|
669
|
** Format of the json: |
|
670
|
** |
|
671
|
** | { |
|
672
|
** | "msgs":[ |
|
673
|
** | { |
|
674
|
** | "msgid": integer // message id |
|
675
|
** | "mtime": text // When sent: YYYY-MM-DDTHH:MM:SSZ |
|
676
|
** | "lmtime: text // Sender's client-side YYYY-MM-DDTHH:MM:SS |
|
677
|
** | "xfrom": text // Login name of sender |
|
678
|
** | "uclr": text // Color string associated with the user |
|
679
|
** | "xmsg": text // HTML text of the message |
|
680
|
** | "fsize": integer // file attachment size in bytes |
|
681
|
** | "fname": text // Name of file attachment |
|
682
|
** | "fmime": text // MIME-type of file attachment |
|
683
|
** | "mdel": integer // message id of prior message to delete |
|
684
|
** | } |
|
685
|
** | ] |
|
686
|
** | } |
|
687
|
** |
|
688
|
** The "fname" and "fmime" fields are only present if "fsize" is greater |
|
689
|
** than zero. The "xmsg" field may be an empty string if "fsize" is zero. |
|
690
|
** |
|
691
|
** The "msgid" values will be in increasing order. |
|
692
|
** |
|
693
|
** The "mdel" will only exist if "xmsg" is an empty string and "fsize" is zero. |
|
694
|
** |
|
695
|
** The "lmtime" value might be unknown, in which case it is omitted. |
|
696
|
** |
|
697
|
** The messages are ordered oldest first unless "before" is provided, in which |
|
698
|
** case they are sorted newest first (to facilitate the client-side UI update). |
|
699
|
** |
|
700
|
** As a special case, if this routine encounters an error, e.g. the user's |
|
701
|
** permissions cannot be verified because their login cookie expired, the |
|
702
|
** request returns a slightly modified structure: |
|
703
|
** |
|
704
|
** | { |
|
705
|
** | "msgs":[ |
|
706
|
** | { |
|
707
|
** | "isError": true, |
|
708
|
** | "xfrom": null, |
|
709
|
** | "xmsg": "error details" |
|
710
|
** | "mtime": as above, |
|
711
|
** | "ltime": same as mtime |
|
712
|
** | } |
|
713
|
** | ] |
|
714
|
** | } |
|
715
|
** |
|
716
|
** If the client gets such a response, it should display the message |
|
717
|
** in a prominent manner and then stop polling for new messages. |
|
718
|
*/ |
|
719
|
void chat_poll_webpage(void){ |
|
720
|
Blob json; /* The json to be constructed and returned */ |
|
721
|
sqlite3_int64 dataVersion; /* Data version. Used for polling. */ |
|
722
|
const int iDelay = 1000; /* Delay until next poll (milliseconds) */ |
|
723
|
int nDelay; /* Maximum delay.*/ |
|
724
|
int msgid = atoi(PD("name","0")); |
|
725
|
const int msgBefore = atoi(PD("before","0")); |
|
726
|
int nLimit = msgBefore>0 ? atoi(PD("n","0")) : 0; |
|
727
|
const int bRaw = P("raw")!=0; |
|
728
|
|
|
729
|
Blob sql = empty_blob; |
|
730
|
Stmt q1; |
|
731
|
nDelay = db_get_int("chat-poll-timeout",420); /* Default about 7 minutes */ |
|
732
|
login_check_credentials(); |
|
733
|
if( !g.perm.Chat ) { |
|
734
|
chat_emit_permissions_error(1); |
|
735
|
return; |
|
736
|
} |
|
737
|
chat_create_tables(); |
|
738
|
cgi_set_content_type("application/json"); |
|
739
|
dataVersion = db_int64(0, "PRAGMA data_version"); |
|
740
|
blob_append_sql(&sql, |
|
741
|
"SELECT msgid, datetime(mtime), xfrom, xmsg, octet_length(file)," |
|
742
|
" fname, fmime, %s, lmtime" |
|
743
|
" FROM chat ", |
|
744
|
msgBefore>0 ? "0 as mdel" : "mdel"); |
|
745
|
if( msgid<=0 || msgBefore>0 ){ |
|
746
|
db_begin_write(); |
|
747
|
chat_purge(); |
|
748
|
db_commit_transaction(); |
|
749
|
} |
|
750
|
if(msgBefore>0){ |
|
751
|
if(0==nLimit){ |
|
752
|
nLimit = db_get_int("chat-initial-history",50); |
|
753
|
} |
|
754
|
blob_append_sql(&sql, |
|
755
|
" WHERE msgid<%d" |
|
756
|
" ORDER BY msgid DESC " |
|
757
|
"LIMIT %d", |
|
758
|
msgBefore, nLimit>0 ? nLimit : -1 |
|
759
|
); |
|
760
|
}else{ |
|
761
|
if( msgid<0 ){ |
|
762
|
msgid = db_int(0, |
|
763
|
"SELECT msgid FROM chat WHERE mdel IS NOT true" |
|
764
|
" ORDER BY msgid DESC LIMIT 1 OFFSET %d", -msgid); |
|
765
|
} |
|
766
|
blob_append_sql(&sql, |
|
767
|
" WHERE msgid>%d" |
|
768
|
" ORDER BY msgid", |
|
769
|
msgid |
|
770
|
); |
|
771
|
} |
|
772
|
db_prepare(&q1, "%s", blob_sql_text(&sql)); |
|
773
|
blob_reset(&sql); |
|
774
|
blob_init(&json, "{\"msgs\":[\n", -1); |
|
775
|
while( nDelay>0 ){ |
|
776
|
int cnt = chat_poll_rowstojson(&q1, bRaw, &json); |
|
777
|
if( cnt || msgBefore>0 ){ |
|
778
|
break; |
|
779
|
} |
|
780
|
sqlite3_sleep(iDelay); nDelay--; |
|
781
|
while( nDelay>0 ){ |
|
782
|
sqlite3_int64 newDataVers = db_int64(0,"PRAGMA repository.data_version"); |
|
783
|
if( newDataVers!=dataVersion ){ |
|
784
|
dataVersion = newDataVers; |
|
785
|
break; |
|
786
|
} |
|
787
|
sqlite3_sleep(iDelay); nDelay--; |
|
788
|
} |
|
789
|
} /* Exit by "break" */ |
|
790
|
db_finalize(&q1); |
|
791
|
blob_append(&json, "\n]}", 3); |
|
792
|
cgi_set_content(&json); |
|
793
|
return; |
|
794
|
} |
|
795
|
|
|
796
|
|
|
797
|
/* |
|
798
|
** WEBPAGE: chat-query hidden loadavg-exempt |
|
799
|
*/ |
|
800
|
void chat_query_webpage(void){ |
|
801
|
Blob json; /* The json to be constructed and returned */ |
|
802
|
Blob sql = empty_blob; |
|
803
|
Stmt q1; |
|
804
|
int nLimit = atoi(PD("n","500")); |
|
805
|
int iFirst = atoi(PD("i","0")); |
|
806
|
const char *zQuery = PD("q", ""); |
|
807
|
i64 iMin = 0; |
|
808
|
i64 iMax = 0; |
|
809
|
|
|
810
|
login_check_credentials(); |
|
811
|
if( !g.perm.Chat ) { |
|
812
|
chat_emit_permissions_error(1); |
|
813
|
return; |
|
814
|
} |
|
815
|
chat_create_tables(); |
|
816
|
cgi_set_content_type("application/json"); |
|
817
|
|
|
818
|
if( zQuery[0] ){ |
|
819
|
iMax = db_int64(0, "SELECT max(msgid) FROM chat"); |
|
820
|
iMin = db_int64(0, "SELECT min(msgid) FROM chat"); |
|
821
|
if( '#'==zQuery[0] ){ |
|
822
|
/* Assume we're looking for an exact msgid match. */ |
|
823
|
++zQuery; |
|
824
|
blob_append_sql(&sql, |
|
825
|
"SELECT msgid, datetime(mtime), xfrom, " |
|
826
|
" xmsg, octet_length(file), fname, fmime, mdel, lmtime " |
|
827
|
" FROM chat WHERE msgid=+%Q", |
|
828
|
zQuery |
|
829
|
); |
|
830
|
}else{ |
|
831
|
char * zPat = search_simplify_pattern(zQuery); |
|
832
|
blob_append_sql(&sql, |
|
833
|
"SELECT * FROM (" |
|
834
|
"SELECT c.msgid, datetime(c.mtime), c.xfrom, " |
|
835
|
" highlight(chatfts1, 0, '<span class=\"match\">', '</span>'), " |
|
836
|
" octet_length(c.file), c.fname, c.fmime, c.mdel, c.lmtime " |
|
837
|
" FROM chatfts1(%Q) f, chat c " |
|
838
|
" WHERE f.rowid=c.msgid" |
|
839
|
" ORDER BY f.rowid DESC LIMIT %d" |
|
840
|
") ORDER BY 1 ASC", zPat, nLimit |
|
841
|
); |
|
842
|
fossil_free(zPat); |
|
843
|
} |
|
844
|
}else{ |
|
845
|
blob_append_sql(&sql, |
|
846
|
"SELECT msgid, datetime(mtime), xfrom, " |
|
847
|
" xmsg, octet_length(file), fname, fmime, mdel, lmtime" |
|
848
|
" FROM chat WHERE msgid>=%d LIMIT %d", |
|
849
|
iFirst, nLimit |
|
850
|
); |
|
851
|
} |
|
852
|
|
|
853
|
db_prepare(&q1, "%s", blob_sql_text(&sql)); |
|
854
|
blob_reset(&sql); |
|
855
|
blob_init(&json, "{\"msgs\":[\n", -1); |
|
856
|
chat_poll_rowstojson(&q1, 0, &json); |
|
857
|
db_finalize(&q1); |
|
858
|
blob_appendf(&json, "\n], \"first\":%lld, \"last\":%lld}", iMin, iMax); |
|
859
|
cgi_set_content(&json); |
|
860
|
return; |
|
861
|
} |
|
862
|
|
|
863
|
/* |
|
864
|
** WEBPAGE: chat-fetch-one hidden loadavg-exempt |
|
865
|
** |
|
866
|
** /chat-fetch-one/N |
|
867
|
** |
|
868
|
** Fetches a single message with the given ID, if available. |
|
869
|
** |
|
870
|
** Options: |
|
871
|
** |
|
872
|
** raw = the xmsg field will be returned unparsed. |
|
873
|
** |
|
874
|
** Response is either a single object in the format returned by |
|
875
|
** /chat-poll (without the wrapper array) or a JSON-format error |
|
876
|
** response, as documented for ajax_route_error(). |
|
877
|
*/ |
|
878
|
void chat_fetch_one(void){ |
|
879
|
Blob json = empty_blob; /* The json to be constructed and returned */ |
|
880
|
const int fRaw = PD("raw",0)!=0; |
|
881
|
const int msgid = atoi(PD("name","0")); |
|
882
|
const char *zChatUser; |
|
883
|
int isWiki; |
|
884
|
Stmt q; |
|
885
|
login_check_credentials(); |
|
886
|
if( !g.perm.Chat ) { |
|
887
|
chat_emit_permissions_error(0); |
|
888
|
return; |
|
889
|
} |
|
890
|
zChatUser = db_get("chat-timeline-user",0); |
|
891
|
chat_create_tables(); |
|
892
|
cgi_set_content_type("application/json"); |
|
893
|
db_prepare(&q, |
|
894
|
"SELECT datetime(mtime), xfrom, xmsg, octet_length(file)," |
|
895
|
" fname, fmime, lmtime" |
|
896
|
" FROM chat WHERE msgid=%d AND mdel IS NULL", |
|
897
|
msgid); |
|
898
|
if(SQLITE_ROW==db_step(&q)){ |
|
899
|
const char *zDate = db_column_text(&q, 0); |
|
900
|
const char *zFrom = db_column_text(&q, 1); |
|
901
|
const char *zRawMsg = db_column_text(&q, 2); |
|
902
|
const int nByte = db_column_int(&q, 3); |
|
903
|
const char *zFName = db_column_text(&q, 4); |
|
904
|
const char *zFMime = db_column_text(&q, 5); |
|
905
|
const char *zLMtime = db_column_text(&q, 7); |
|
906
|
blob_appendf(&json,"{\"msgid\": %d,", msgid); |
|
907
|
|
|
908
|
blob_appendf(&json, "\"mtime\":\"%.10sT%sZ\",", zDate, zDate+11); |
|
909
|
if( zLMtime && zLMtime[0] ){ |
|
910
|
blob_appendf(&json, "\"lmtime\":%!j,", zLMtime); |
|
911
|
} |
|
912
|
blob_append(&json, "\"xfrom\":", -1); |
|
913
|
if(zFrom){ |
|
914
|
blob_appendf(&json, "%!j,", zFrom); |
|
915
|
isWiki = fossil_strcmp(zFrom, zChatUser)==0; |
|
916
|
}else{ |
|
917
|
/* see https://fossil-scm.org/forum/forumpost/e0be0eeb4c */ |
|
918
|
blob_appendf(&json, "null,"); |
|
919
|
isWiki = 0; |
|
920
|
} |
|
921
|
blob_appendf(&json, "\"uclr\":%!j,", |
|
922
|
isWiki ? "transparent" : user_color(zFrom ? zFrom : "nobody")); |
|
923
|
blob_append(&json,"\"xmsg\":", 7); |
|
924
|
if(fRaw){ |
|
925
|
blob_appendf(&json, "%!j,", zRawMsg); |
|
926
|
}else{ |
|
927
|
char * zMsg = chat_format_to_html(zRawMsg ? zRawMsg : "", isWiki); |
|
928
|
blob_appendf(&json, "%!j,", zMsg); |
|
929
|
fossil_free(zMsg); |
|
930
|
} |
|
931
|
if( nByte==0 ){ |
|
932
|
blob_appendf(&json, "\"fsize\":0"); |
|
933
|
}else{ |
|
934
|
blob_appendf(&json, "\"fsize\":%d,\"fname\":%!j,\"fmime\":%!j", |
|
935
|
nByte, zFName, zFMime); |
|
936
|
} |
|
937
|
blob_append(&json,"}",1); |
|
938
|
cgi_set_content(&json); |
|
939
|
}else{ |
|
940
|
ajax_route_error(404,"Chat message #%d not found.", msgid); |
|
941
|
} |
|
942
|
db_finalize(&q); |
|
943
|
} |
|
944
|
|
|
945
|
/* |
|
946
|
** WEBPAGE: chat-download hidden loadavg-exempt |
|
947
|
** |
|
948
|
** Download the CHAT.FILE attachment associated with a single chat |
|
949
|
** entry. The "name" query parameter begins with an integer that |
|
950
|
** identifies the particular chat message. The integer may be followed |
|
951
|
** by a / and a filename, which will (A) indicate to the browser to |
|
952
|
** use the indicated name when saving the file and (B) be used to |
|
953
|
** guess the mimetype in some particular cases involving the "render" |
|
954
|
** flag. |
|
955
|
** |
|
956
|
** If the "render" URL parameter is provided, the blob has a size |
|
957
|
** greater than zero, and blob meets one of the following conditions |
|
958
|
** then the fossil-rendered form of that content is returned, rather |
|
959
|
** than the original: |
|
960
|
** |
|
961
|
** - Mimetype is text/x-markdown or text/markdown: emit HTML. |
|
962
|
** |
|
963
|
** - Mimetype is text/x-fossil-wiki or P("name") ends with ".wiki": |
|
964
|
** emit HTML. |
|
965
|
** |
|
966
|
** - Mimetype is text/x-pikchr or P("name") ends with ".pikchr": emit |
|
967
|
** image/svg+xml if rendering succeeds or text/html if rendering |
|
968
|
** fails. |
|
969
|
*/ |
|
970
|
void chat_download_webpage(void){ |
|
971
|
int msgid; |
|
972
|
int bCheckedMimetype = 0; /* true to bypass the text/... mimetype |
|
973
|
** check at the end */ |
|
974
|
Blob r; /* file content */ |
|
975
|
const char *zMime; /* file mimetype */ |
|
976
|
const char *zName = PD("name","0"); |
|
977
|
login_check_credentials(); |
|
978
|
if( !g.perm.Chat ){ |
|
979
|
style_header("Chat Not Authorized"); |
|
980
|
@ <h1>Not Authorized</h1> |
|
981
|
@ <p>You do not have permission to use the chatroom on this |
|
982
|
@ repository.</p> |
|
983
|
style_finish_page(); |
|
984
|
return; |
|
985
|
} |
|
986
|
chat_create_tables(); |
|
987
|
msgid = atoi(zName); |
|
988
|
blob_zero(&r); |
|
989
|
zMime = db_text(0, "SELECT fmime FROM chat wHERE msgid=%d", msgid); |
|
990
|
if( zMime==0 ) return; |
|
991
|
db_blob(&r, "SELECT file FROM chat WHERE msgid=%d", msgid); |
|
992
|
if( r.nUsed>0 && P("render")!=0 ){ |
|
993
|
/* Maybe return fossil-rendered form of the content. */ |
|
994
|
Blob r2 = BLOB_INITIALIZER; /* output target for rendering */ |
|
995
|
const char * zMime2 = 0; /* adjusted response mimetype */ |
|
996
|
if(fossil_strcmp(zMime, "text/x-markdown")==0 |
|
997
|
/* Firefox uploads md files with the mimetype text/markdown */ |
|
998
|
|| fossil_strcmp(zMime, "text/markdown")==0){ |
|
999
|
markdown_to_html(&r, 0, &r2); |
|
1000
|
safe_html(&r2); |
|
1001
|
zMime2 = "text/html"; |
|
1002
|
bCheckedMimetype = 1; |
|
1003
|
}else if(fossil_strcmp(zMime, "text/x-fossil-wiki")==0 |
|
1004
|
|| sqlite3_strglob("*.wiki", zName)==0){ |
|
1005
|
/* .wiki files get uploaded as application/octet-stream */ |
|
1006
|
wiki_convert(&r, &r2, 0); |
|
1007
|
zMime2 = "text/html"; |
|
1008
|
bCheckedMimetype = 1; |
|
1009
|
}else if(fossil_strcmp(zMime, "text/x-pikchr")==0 |
|
1010
|
|| sqlite3_strglob("*.pikchr",zName)==0){ |
|
1011
|
/* .pikchr files get uploaded as application/octet-stream */ |
|
1012
|
const char *zPikchr = blob_str(&r); |
|
1013
|
int w = 0, h = 0; |
|
1014
|
char *zOut = pikchr(zPikchr, "pikchr", 0, &w, &h); |
|
1015
|
if(zOut){ |
|
1016
|
blob_append(&r2, zOut, -1); |
|
1017
|
} |
|
1018
|
zMime2 = w>0 ? "image/svg+xml" : "text/html"; |
|
1019
|
free(zOut); |
|
1020
|
bCheckedMimetype = 1; |
|
1021
|
} |
|
1022
|
if(r2.aData!=0){ |
|
1023
|
blob_swap(&r, &r2); |
|
1024
|
blob_reset(&r2); |
|
1025
|
zMime = zMime2; |
|
1026
|
} |
|
1027
|
} |
|
1028
|
if( bCheckedMimetype==0 && sqlite3_strglob("text/*", zMime)==0 ){ |
|
1029
|
/* The problem: both Chrome and Firefox upload *.patch with |
|
1030
|
** the mimetype text/x-patch, whereas we very often use that |
|
1031
|
** name glob for fossil-format patches. That causes such files |
|
1032
|
** to attempt to render in the browser when clicked via |
|
1033
|
** download links in chat. |
|
1034
|
** |
|
1035
|
** The workaround: */ |
|
1036
|
if( looks_like_binary(&r) ){ |
|
1037
|
zMime = "application/octet-stream"; |
|
1038
|
} |
|
1039
|
} |
|
1040
|
cgi_set_content_type(zMime); |
|
1041
|
cgi_set_content(&r); |
|
1042
|
} |
|
1043
|
|
|
1044
|
|
|
1045
|
/* |
|
1046
|
** WEBPAGE: chat-delete hidden loadavg-exempt |
|
1047
|
** |
|
1048
|
** Delete the chat entry identified by the name query parameter. |
|
1049
|
** Invoking fetch("chat-delete/"+msgid) from javascript in the client |
|
1050
|
** will delete a chat entry from the CHAT table. |
|
1051
|
** |
|
1052
|
** This routine both deletes the identified chat entry and also inserts |
|
1053
|
** a new entry with the current timestamp and with: |
|
1054
|
** |
|
1055
|
** * xmsg = NULL |
|
1056
|
** |
|
1057
|
** * file = NULL |
|
1058
|
** |
|
1059
|
** * mdel = The msgid of the row that was deleted |
|
1060
|
** |
|
1061
|
** This new entry will then be propagated to all listeners so that they |
|
1062
|
** will know to delete their copies of the message too. |
|
1063
|
*/ |
|
1064
|
void chat_delete_webpage(void){ |
|
1065
|
int mdel; |
|
1066
|
char *zOwner; |
|
1067
|
login_check_credentials(); |
|
1068
|
if( !g.perm.Chat ) return; |
|
1069
|
chat_create_tables(); |
|
1070
|
mdel = atoi(PD("name","0")); |
|
1071
|
zOwner = db_text(0, "SELECT xfrom FROM chat WHERE msgid=%d", mdel); |
|
1072
|
if( zOwner==0 ) return; |
|
1073
|
if( fossil_strcmp(zOwner, g.zLogin)!=0 && !g.perm.Admin ) return; |
|
1074
|
db_multi_exec( |
|
1075
|
"PRAGMA secure_delete=ON;\n" |
|
1076
|
"BEGIN;\n" |
|
1077
|
"DELETE FROM chat WHERE msgid=%d;\n" |
|
1078
|
"INSERT INTO chat(mtime, xfrom, mdel)" |
|
1079
|
" VALUES(julianday('now'), %Q, %d);\n" |
|
1080
|
"COMMIT;", |
|
1081
|
mdel, g.zLogin, mdel |
|
1082
|
); |
|
1083
|
} |
|
1084
|
|
|
1085
|
/* |
|
1086
|
** WEBPAGE: chat-backup hidden |
|
1087
|
** |
|
1088
|
** Download an SQLite database containing all chat content with a |
|
1089
|
** message-id larger than the "msgid" query parameter. Setup |
|
1090
|
** privilege is required to use this URL. |
|
1091
|
** |
|
1092
|
** This is used to implement the "fossil chat pull" command. |
|
1093
|
*/ |
|
1094
|
void chat_backup_webpage(void){ |
|
1095
|
int msgid; |
|
1096
|
unsigned char *pDb = 0; |
|
1097
|
sqlite3_int64 szDb = 0; |
|
1098
|
Blob chatDb; |
|
1099
|
login_check_credentials(); |
|
1100
|
if( !g.perm.Setup ) return; |
|
1101
|
msgid = atoi(PD("msgid","0")); |
|
1102
|
db_multi_exec( |
|
1103
|
"ATTACH ':memory:' AS mem1;\n" |
|
1104
|
"PRAGMA mem1.page_size=512;\n" |
|
1105
|
"CREATE TABLE mem1.chat AS SELECT * FROM repository.chat WHERE msgid>%d;\n", |
|
1106
|
msgid |
|
1107
|
); |
|
1108
|
pDb = sqlite3_serialize(g.db, "mem1", &szDb, 0); |
|
1109
|
if( pDb==0 ){ |
|
1110
|
fossil_fatal("Out of memory"); |
|
1111
|
} |
|
1112
|
blob_init(&chatDb, (const char*)pDb, (int)szDb); |
|
1113
|
cgi_set_content_type("application/x-sqlite3"); |
|
1114
|
cgi_set_content(&chatDb); |
|
1115
|
} |
|
1116
|
|
|
1117
|
/* |
|
1118
|
** SQL Function: chat_msg_from_event(TYPE,OBJID,USER,MSG) |
|
1119
|
** |
|
1120
|
** This function returns HTML text that describes an entry from the EVENT |
|
1121
|
** table (that is, a timeline event) for display in chat. Parameters: |
|
1122
|
** |
|
1123
|
** TYPE The event type. 'ci', 'w', 't', 'g', and so forth |
|
1124
|
** OBJID EVENT.OBJID |
|
1125
|
** USER coalesce(EVENT.EUSER,EVENT.USER) |
|
1126
|
** MSG coalesce(EVENT.ECOMMENT, EVENT.COMMENT) |
|
1127
|
** |
|
1128
|
** This function is intended to be called by the temp.chat_trigger1 trigger |
|
1129
|
** which is created by alert_create_trigger() routine. |
|
1130
|
*/ |
|
1131
|
void chat_msg_from_event( |
|
1132
|
sqlite3_context *context, |
|
1133
|
int argc, |
|
1134
|
sqlite3_value **argv |
|
1135
|
){ |
|
1136
|
const char *zType = (const char*)sqlite3_value_text(argv[0]); |
|
1137
|
int rid = sqlite3_value_int(argv[1]); |
|
1138
|
const char *zUser = (const char*)sqlite3_value_text(argv[2]); |
|
1139
|
const char *zMsg = (const char*)sqlite3_value_text(argv[3]); |
|
1140
|
char *zRes = 0; |
|
1141
|
|
|
1142
|
if( zType==0 || zUser==0 || zMsg==0 ) return; |
|
1143
|
if( zType[0]=='c' ){ |
|
1144
|
/* Check-ins */ |
|
1145
|
char *zBranch; |
|
1146
|
char *zUuid; |
|
1147
|
|
|
1148
|
zBranch = db_text(0, |
|
1149
|
"SELECT value FROM tagxref" |
|
1150
|
" WHERE tagxref.rid=%d" |
|
1151
|
" AND tagxref.tagid=%d" |
|
1152
|
" AND tagxref.tagtype>0", |
|
1153
|
rid, TAG_BRANCH); |
|
1154
|
zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
|
1155
|
zRes = mprintf("%W (check-in: <a href='%R/info/%S'>%S</a>, " |
|
1156
|
"user: <a href='%R/timeline?u=%t&c=%S'>%h</a>, " |
|
1157
|
"branch: <a href='%R/timeline?r=%t&c=%S'>%h</a>)", |
|
1158
|
zMsg, |
|
1159
|
zUuid, zUuid, |
|
1160
|
zUser, zUuid, zUser, |
|
1161
|
zBranch, zUuid, zBranch |
|
1162
|
); |
|
1163
|
fossil_free(zBranch); |
|
1164
|
fossil_free(zUuid); |
|
1165
|
}else if( zType[0]=='w' ){ |
|
1166
|
/* Wiki page changes */ |
|
1167
|
char *zUuid = rid_to_uuid(rid); |
|
1168
|
wiki_hyperlink_override(zUuid); |
|
1169
|
if( zMsg[0]=='-' ){ |
|
1170
|
zRes = mprintf("Delete wiki page <a href='%R/whistory?name=%t'>%h</a>", |
|
1171
|
zMsg+1, zMsg+1); |
|
1172
|
}else if( zMsg[0]=='+' ){ |
|
1173
|
zRes = mprintf("Added wiki page <a href='%R/whistory?name=%t'>%h</a>", |
|
1174
|
zMsg+1, zMsg+1); |
|
1175
|
}else if( zMsg[0]==':' ){ |
|
1176
|
zRes = mprintf("<a href='%R/wdiff?id=%!S'>Changes</a> to wiki page " |
|
1177
|
"<a href='%R/whistory?name=%t'>%h</a>", |
|
1178
|
zUuid, zMsg+1, zMsg+1); |
|
1179
|
}else{ |
|
1180
|
zRes = mprintf("%W", zMsg); |
|
1181
|
} |
|
1182
|
wiki_hyperlink_override(0); |
|
1183
|
fossil_free(zUuid); |
|
1184
|
}else if( zType[0]=='f' ){ |
|
1185
|
/* Forum changes */ |
|
1186
|
char *zUuid = rid_to_uuid(rid); |
|
1187
|
zRes = mprintf( "%W (artifact: <a href='%R/info/%S'>%S</a>, " |
|
1188
|
"user: <a href='%R/timeline?u=%t&c=%S'>%h</a>)", |
|
1189
|
zMsg, zUuid, zUuid, zUser, zUuid, zUser); |
|
1190
|
fossil_free(zUuid); |
|
1191
|
}else{ |
|
1192
|
/* Anything else */ |
|
1193
|
zRes = mprintf("%W", zMsg); |
|
1194
|
} |
|
1195
|
if( zRes ){ |
|
1196
|
sqlite3_result_text(context, zRes, -1, fossil_free); |
|
1197
|
} |
|
1198
|
} |
|
1199
|
|
|
1200
|
|
|
1201
|
/* |
|
1202
|
** COMMAND: chat |
|
1203
|
** |
|
1204
|
** Usage: %fossil chat [SUBCOMMAND] [--remote URL] [ARGS...] |
|
1205
|
** |
|
1206
|
** This command performs actions associated with the /chat instance |
|
1207
|
** on the default remote Fossil repository (the Fossil repository whose |
|
1208
|
** URL shows when you run the "fossil remote" command) or to the URL |
|
1209
|
** specified by the --remote option. If there is no default remote |
|
1210
|
** Fossil repository and the --remote option is omitted, then this |
|
1211
|
** command fails with an error. |
|
1212
|
** |
|
1213
|
** Subcommands: |
|
1214
|
** |
|
1215
|
** > fossil chat |
|
1216
|
** |
|
1217
|
** When there is no SUBCOMMAND (when this command is simply "fossil chat") |
|
1218
|
** the response is to bring up a web-browser window to the chatroom |
|
1219
|
** on the default system web-browser. You can accomplish the same by |
|
1220
|
** typing the appropriate URL into the web-browser yourself. This |
|
1221
|
** command is merely a convenience for command-line oriented people. |
|
1222
|
** |
|
1223
|
** > fossil chat pull |
|
1224
|
** |
|
1225
|
** Copy chat content from the server down into the local clone, |
|
1226
|
** as a backup or archive. Setup privilege is required on the server. |
|
1227
|
** |
|
1228
|
** --all Download all chat content. Normally only |
|
1229
|
** previously undownloaded content is retrieved. |
|
1230
|
** --debug Additional debugging output |
|
1231
|
** --out DATABASE Store CHAT table in separate database file |
|
1232
|
** DATABASE rather than adding to local clone |
|
1233
|
** --unsafe Allow the use of unencrypted http:// |
|
1234
|
** |
|
1235
|
** > fossil chat send [ARGUMENTS] |
|
1236
|
** |
|
1237
|
** This command sends a new message to the chatroom. The message |
|
1238
|
** to be sent is determined by arguments as follows: |
|
1239
|
** |
|
1240
|
** -f|--file FILENAME File to attach to the message |
|
1241
|
** --as FILENAME2 Causes --file FILENAME to be sent with |
|
1242
|
** the attachment name FILENAME2 |
|
1243
|
** -m|--message TEXT Text of the chat message |
|
1244
|
** --remote URL Send to this remote URL |
|
1245
|
** --unsafe Allow the use of unencrypted http:// |
|
1246
|
** |
|
1247
|
** > fossil chat url |
|
1248
|
** |
|
1249
|
** Show the default URL used to access the chat server. |
|
1250
|
** |
|
1251
|
** > fossil chat purge |
|
1252
|
** |
|
1253
|
** Remove chat messages that are older than chat-keep-days and |
|
1254
|
** which are not one of the most recent chat-keep-count message. |
|
1255
|
** |
|
1256
|
** > fossil chat reindex |
|
1257
|
** |
|
1258
|
** Rebuild the full-text search index for chat |
|
1259
|
** |
|
1260
|
** Additional subcommands may be added in the future. |
|
1261
|
*/ |
|
1262
|
void chat_command(void){ |
|
1263
|
const char *zUrl = find_option("remote",0,1); |
|
1264
|
int urlFlags = 0; |
|
1265
|
int isDefaultUrl = 0; |
|
1266
|
int i; |
|
1267
|
|
|
1268
|
db_find_and_open_repository(0,0); |
|
1269
|
if( zUrl ){ |
|
1270
|
urlFlags = URL_PROMPT_PW; |
|
1271
|
}else{ |
|
1272
|
zUrl = db_get("last-sync-url",0); |
|
1273
|
if( zUrl==0 ){ |
|
1274
|
fossil_fatal("no \"remote\" repository defined"); |
|
1275
|
}else{ |
|
1276
|
isDefaultUrl = 1; |
|
1277
|
} |
|
1278
|
} |
|
1279
|
url_parse(zUrl, urlFlags); |
|
1280
|
if( g.url.isFile || g.url.isSsh ){ |
|
1281
|
fossil_fatal("chat only works for http:// and https:// URLs"); |
|
1282
|
} |
|
1283
|
i = (int)strlen(g.url.path); |
|
1284
|
while( i>0 && g.url.path[i-1]=='/' ) i--; |
|
1285
|
if( g.url.port==g.url.dfltPort ){ |
|
1286
|
zUrl = mprintf( |
|
1287
|
"%s://%T%.*T", |
|
1288
|
g.url.protocol, g.url.name, i, g.url.path |
|
1289
|
); |
|
1290
|
}else{ |
|
1291
|
zUrl = mprintf( |
|
1292
|
"%s://%T:%d%.*T", |
|
1293
|
g.url.protocol, g.url.name, g.url.port, i, g.url.path |
|
1294
|
); |
|
1295
|
} |
|
1296
|
if( g.argc==2 ){ |
|
1297
|
const char *zBrowser = fossil_web_browser(); |
|
1298
|
char *zCmd; |
|
1299
|
verify_all_options(); |
|
1300
|
if( zBrowser==0 ) return; |
|
1301
|
#ifdef _WIN32 |
|
1302
|
zCmd = mprintf("%s %s/chat?cli &", zBrowser, zUrl); |
|
1303
|
#else |
|
1304
|
zCmd = mprintf("%s \"%s/chat?cli\" &", zBrowser, zUrl); |
|
1305
|
#endif |
|
1306
|
fossil_system(zCmd); |
|
1307
|
}else if( strcmp(g.argv[2],"send")==0 ){ |
|
1308
|
const char *zFilename = find_option("file","f",1); |
|
1309
|
const char *zAs = find_option("as",0,1); |
|
1310
|
const char *zMsg = find_option("message","m",1); |
|
1311
|
int allowUnsafe = find_option("unsafe",0,0)!=0; |
|
1312
|
const int mFlags = HTTP_GENERIC | HTTP_QUIET | HTTP_NOCOMPRESS; |
|
1313
|
int i; |
|
1314
|
const char *zPw; |
|
1315
|
char *zLMTime; |
|
1316
|
Blob up, down, fcontent; |
|
1317
|
char zBoundary[80]; |
|
1318
|
sqlite3_uint64 r[3]; |
|
1319
|
if( zFilename==0 && zMsg==0 ){ |
|
1320
|
fossil_fatal("must have --message or --file or both"); |
|
1321
|
} |
|
1322
|
if( !g.url.isHttps && !allowUnsafe ){ |
|
1323
|
fossil_fatal("URL \"%s\" is unencrypted. Use https:// instead", zUrl); |
|
1324
|
} |
|
1325
|
verify_all_options(); |
|
1326
|
if( g.argc>3 ){ |
|
1327
|
fossil_fatal("unknown extra argument: \"%s\"", g.argv[3]); |
|
1328
|
} |
|
1329
|
i = (int)strlen(g.url.path); |
|
1330
|
while( i>0 && g.url.path[i-1]=='/' ) i--; |
|
1331
|
g.url.path = mprintf("%.*s/chat-send", i, g.url.path); |
|
1332
|
blob_init(&up, 0, 0); |
|
1333
|
blob_init(&down, 0, 0); |
|
1334
|
sqlite3_randomness(sizeof(r),r); |
|
1335
|
sqlite3_snprintf(sizeof(zBoundary),zBoundary, |
|
1336
|
"--------%016llu%016llu%016llu", r[0], r[1], r[2]); |
|
1337
|
blob_appendf(&up, "%s", zBoundary); |
|
1338
|
zLMTime = db_text(0, |
|
1339
|
"SELECT strftime('%%Y-%%m-%%dT%%H:%%M:%%S','now','localtime')"); |
|
1340
|
if( zLMTime ){ |
|
1341
|
blob_appendf(&up,"\r\nContent-Disposition: form-data; name=\"lmtime\"\r\n" |
|
1342
|
"\r\n%z\r\n%s", zLMTime, zBoundary); |
|
1343
|
} |
|
1344
|
if( g.url.user && g.url.user[0] ){ |
|
1345
|
blob_appendf(&up,"\r\nContent-Disposition: form-data; name=\"resid\"\r\n" |
|
1346
|
"\r\n%z\r\n%s", obscure(g.url.user), zBoundary); |
|
1347
|
} |
|
1348
|
zPw = g.url.passwd; |
|
1349
|
if( zPw==0 && isDefaultUrl ) zPw = unobscure(db_get("last-sync-pw", 0)); |
|
1350
|
if( zPw && zPw[0] ){ |
|
1351
|
blob_appendf(&up,"\r\nContent-Disposition: form-data; name=\"token\"\r\n" |
|
1352
|
"\r\n%z\r\n%s", obscure(zPw), zBoundary); |
|
1353
|
} |
|
1354
|
if( zMsg && zMsg[0] ){ |
|
1355
|
blob_appendf(&up,"\r\nContent-Disposition: form-data; name=\"msg\"\r\n" |
|
1356
|
"\r\n%s\r\n%s", zMsg, zBoundary); |
|
1357
|
} |
|
1358
|
if( zFilename && blob_read_from_file(&fcontent, zFilename, ExtFILE)>0 ){ |
|
1359
|
char *zFN = fossil_strdup(file_tail(zAs ? zAs : zFilename)); |
|
1360
|
int i; |
|
1361
|
const char *zMime = mimetype_from_name(zFN); |
|
1362
|
for(i=0; zFN[i]; i++){ |
|
1363
|
char c = zFN[i]; |
|
1364
|
if( fossil_isalnum(c) ) continue; |
|
1365
|
if( c=='.' ) continue; |
|
1366
|
if( c=='-' ) continue; |
|
1367
|
zFN[i] = '_'; |
|
1368
|
} |
|
1369
|
blob_appendf(&up,"\r\nContent-Disposition: form-data; name=\"file\";" |
|
1370
|
" filename=\"%s\"\r\n", zFN); |
|
1371
|
blob_appendf(&up,"Content-Type: %s\r\n\r\n", zMime); |
|
1372
|
blob_append(&up, fcontent.aData, fcontent.nUsed); |
|
1373
|
blob_appendf(&up,"\r\n%s", zBoundary); |
|
1374
|
} |
|
1375
|
blob_append(&up,"--\r\n", 4); |
|
1376
|
http_exchange(&up, &down, mFlags, 4, "multipart/form-data"); |
|
1377
|
blob_reset(&up); |
|
1378
|
if( sqlite3_strglob("{\"isError\": true,*", blob_str(&down))==0 ){ |
|
1379
|
if( strstr(blob_str(&down), "not logged in")!=0 ){ |
|
1380
|
fossil_print("ERROR: username and/or password is incorrect\n"); |
|
1381
|
}else{ |
|
1382
|
fossil_print("ERROR: %s\n", blob_str(&down)); |
|
1383
|
} |
|
1384
|
fossil_fatal("unable to send the chat message"); |
|
1385
|
} |
|
1386
|
blob_reset(&down); |
|
1387
|
}else if( strcmp(g.argv[2],"pull")==0 ){ |
|
1388
|
/* Pull the CHAT table from the default server down into the repository |
|
1389
|
** here on the local side */ |
|
1390
|
int allowUnsafe = find_option("unsafe",0,0)!=0; |
|
1391
|
int bDebug = find_option("debug",0,0)!=0; |
|
1392
|
const char *zOut = find_option("out",0,1); |
|
1393
|
int bAll = find_option("all",0,0)!=0; |
|
1394
|
int mFlags = HTTP_GENERIC | HTTP_QUIET | HTTP_NOCOMPRESS; |
|
1395
|
int msgid; |
|
1396
|
Blob reqUri; /* The REQUEST_URI: .../chat-backup?msgid=... */ |
|
1397
|
char *zObs; |
|
1398
|
const char *zPw; |
|
1399
|
Blob up, down; |
|
1400
|
int nChat; |
|
1401
|
int rc; |
|
1402
|
verify_all_options(); |
|
1403
|
chat_create_tables(); |
|
1404
|
msgid = bAll ? 0 : db_int(0,"SELECT max(msgid) FROM chat"); |
|
1405
|
if( !g.url.isHttps && !allowUnsafe ){ |
|
1406
|
fossil_fatal("URL \"%s\" is unencrypted. Use https:// instead", zUrl); |
|
1407
|
} |
|
1408
|
blob_init(&reqUri, g.url.path, -1); |
|
1409
|
blob_appendf(&reqUri, "/chat-backup?msgid=%d", msgid); |
|
1410
|
if( g.url.user && g.url.user[0] ){ |
|
1411
|
zObs = obscure(g.url.user); |
|
1412
|
blob_appendf(&reqUri, "&resid=%t", zObs); |
|
1413
|
fossil_free(zObs); |
|
1414
|
} |
|
1415
|
zPw = g.url.passwd; |
|
1416
|
if( zPw==0 && isDefaultUrl ){ |
|
1417
|
zPw = unobscure(db_get("last-sync-pw", 0)); |
|
1418
|
if( zPw==0 ){ |
|
1419
|
/* Can happen if "remember password" is not used. */ |
|
1420
|
g.url.flags |= URL_PROMPT_PW; |
|
1421
|
url_prompt_for_password(); |
|
1422
|
zPw = g.url.passwd; |
|
1423
|
} |
|
1424
|
} |
|
1425
|
if( zPw && zPw[0] ){ |
|
1426
|
zObs = obscure(zPw); |
|
1427
|
blob_appendf(&reqUri, "&token=%t", zObs); |
|
1428
|
fossil_free(zObs); |
|
1429
|
} |
|
1430
|
g.url.path = blob_str(&reqUri); |
|
1431
|
if( bDebug ){ |
|
1432
|
fossil_print("REQUEST_URI: %s\n", g.url.path); |
|
1433
|
mFlags &= ~HTTP_QUIET; |
|
1434
|
mFlags |= HTTP_VERBOSE; |
|
1435
|
|