Fossil

Check-in [822653c269]
Login

Check-in [822653c269]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Hyperlink processing for chat messages is now handled on the server side, where we have knowledge of interwiki links, wiki page names, and valid artifact hashes.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 822653c26952d70c3482227e73f11cb84aa39331d6717d6ea2758ec3062b15fc
User & Date: drh 2020-12-24 13:44:30.140
Context
2020-12-24
15:10
The ping=TCPPORT query parameter to /chat causes a call to /chat-ping on localhost and the given port whenever new chat content arrives. Can be used for notifications. ... (check-in: ebdd91b92f user: drh tags: trunk)
13:44
Hyperlink processing for chat messages is now handled on the server side, where we have knowledge of interwiki links, wiki page names, and valid artifact hashes. ... (check-in: 822653c269 user: drh tags: trunk)
08:10
chat: revert title to its originalv alue when the tab becomes active after having been inactive. ... (check-in: 0c0be4b763 user: stephan tags: trunk)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/chat.c.
241
242
243
244
245
246
247





























































































248
249
250
251
252
253
254
    blob_init(&b, P("file"), nByte);
    db_bind_blob(&q, ":file", &b);
    db_step(&q);
    db_finalize(&q);
    blob_reset(&b);
  }
}






























































































/*
** WEBPAGE: chat-poll
**
** The chat page generated by /chat using a XHR to this page in order
** to ask for new chat content.  The "name" argument should begin with
** an integer which is the largest "msgid" that the chat page currently







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
    blob_init(&b, P("file"), nByte);
    db_bind_blob(&q, ":file", &b);
    db_step(&q);
    db_finalize(&q);
    blob_reset(&b);
  }
}

/*
** This routine receives raw (user-entered) message text and transforms
** it into HTML that is safe to insert using innerHTML.
**
**    *   HTML in the original text is escaped.
**
**    *   Hyperlinks are identified and tagged.  Hyperlinks are:
**
**          -  Undelimited text of the form https:... or http:...
**          -  Any text enclosed within [...]
**
** 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){
  char *zSafe = mprintf("%h", zMsg);
  int i, j, k;
  Blob out;
  char zClose[20];
  blob_init(&out, 0, 0);
  for(i=j=0; zSafe[i]; i++){
    if( zSafe[i]=='[' ){
      for(k=i+1; zSafe[k] && zSafe[k]!=']'; k++){}
      if( zSafe[k]==']' ){
        zSafe[k] = 0;
        if( j<i ){
          blob_append(&out, zSafe + j, i-j);
          j = i;
        }
        wiki_resolve_hyperlink(&out, WIKI_NOBADLINKS|WIKI_TARGET_BLANK,
                               zSafe+i+1, zClose, sizeof(zClose), zSafe, 0);
        zSafe[k] = ']';
        j++;
        blob_append(&out, zSafe + j, k - j);
        blob_append(&out, zClose, -1);
        i = k;
        j = k+1;
        continue;
      }
    }else if( zSafe[i]=='h' 
           && (strncmp(zSafe+i,"http:",5)==0
               || strncmp(zSafe+i,"https:",6)==0) ){
      for(k=i+1; zSafe[k] && !fossil_isspace(zSafe[k]); k++){}
      if( zSafe[k] && k>i+7 ){
        char c = zSafe[k];
        if( !fossil_isalnum(zSafe[k-1]) && zSafe[k-1]!='/' ){
          k--;
          c = zSafe[k];
        }
        if( j<i ){
          blob_append(&out, zSafe + j, i-j);
          j = i;
        }
        zSafe[k] = 0;        
        wiki_resolve_hyperlink(&out, WIKI_NOBADLINKS|WIKI_TARGET_BLANK,
                               zSafe+i, zClose, sizeof(zClose), zSafe, 0);
        zSafe[k] = c;
        blob_append(&out, zSafe + j, k - j);
        blob_append(&out, zClose, -1);
        i = j = k;
        continue;
      }
    }
  }
  if( j<i ){
    blob_append(&out, zSafe+j, j-i);
  }
  fossil_free(zSafe);
  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<g.argc; i++){
    zOut = chat_format_to_html(g.argv[i]);
    fossil_print("[%d]: %s\n", i, zOut);
    fossil_free(zOut);
  }
}

/*
** WEBPAGE: chat-poll
**
** The chat page generated by /chat using a XHR to this page in order
** to ask for new chat content.  The "name" argument should begin with
** an integer which is the largest "msgid" that the chat page currently
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
      cnt++;
      blob_append(&json, zSep, -1);
      zSep = ",\n";
      blob_appendf(&json, "{\"msgid\":%d,\"mtime\":%!j,", id, zDate);
      blob_appendf(&json, "\"xfrom\":%!j,", zFrom);
      blob_appendf(&json, "\"uclr\":%!j,", hash_color(zFrom));

      /* TBD:  Convert the raw message into HTML, perhaps by running it
      ** through a text formatter, or putting markup on @name phrases,
      ** etc. */
      zMsg = mprintf("%h", zRawMsg ? zRawMsg : "");
      blob_appendf(&json, "\"xmsg\":%!j,", zMsg);
      fossil_free(zMsg);

      if( nByte==0 ){
        blob_appendf(&json, "\"fsize\":0");
      }else{
        blob_appendf(&json, "\"fsize\":%d,\"fname\":%!j,\"fmime\":%!j",







<
<
<
|







427
428
429
430
431
432
433



434
435
436
437
438
439
440
441
      cnt++;
      blob_append(&json, zSep, -1);
      zSep = ",\n";
      blob_appendf(&json, "{\"msgid\":%d,\"mtime\":%!j,", id, zDate);
      blob_appendf(&json, "\"xfrom\":%!j,", zFrom);
      blob_appendf(&json, "\"uclr\":%!j,", hash_color(zFrom));




      zMsg = chat_format_to_html(zRawMsg ? zRawMsg : "");
      blob_appendf(&json, "\"xmsg\":%!j,", zMsg);
      fossil_free(zMsg);

      if( nByte==0 ){
        blob_appendf(&json, "\"fsize\":0");
      }else{
        blob_appendf(&json, "\"fsize\":%d,\"fname\":%!j,\"fmime\":%!j",
Changes to src/chat.js.
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
      // truncation off the right edge of the page.
      const pRect = f.popup.e.getBoundingClientRect();
      x -= pRect.width/3*2;
    }
    f.popup.show(x, y);
  };

  /**
     Parses the given chat message string for hyperlinks and @NAME
     references, replacing them with markup, then stuffs the parsed
     content into the given target DOM element.

     This is an intermediary step until we get some basic markup
     support coming from the server side.
  */
  const messageToDOM = function f(str, tgtElem){
    "use strict";
    if(!f.rxUrl){
      f.rxUrl = /\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim;
      f.rxAt = /@\w+/gmi;
      f.rxNS = /\S/;
      f.ce = (T)=>document.createElement(T);
      f.ct = (T)=>document.createTextNode(T);
      f.replaceUrls = function ff(sub, offset, whole){
        if(offset > ff.prevStart){
          f.accum.push((ff.prevStart?' ':'')+whole.substring(ff.prevStart, offset-1)+' ');
        }
        const a = f.ce('a');
        a.setAttribute('href',sub);
        a.setAttribute('target','_blank');
        a.appendChild(f.ct(sub));
        f.accum.push(a);
        ff.prevStart = offset + sub.length + 1;
      };
      f.replaceAtName = function ff(sub, offset,whole){
        if(offset > ff.prevStart){
          ff.accum.push((ff.prevStart?' ':'')+whole.substring(ff.prevStart, offset-1)+' ');
        }else if(offset && f.rxNS.test(whole[offset-1])){
          // Sigh: https://stackoverflow.com/questions/52655367
          ff.accum.push(sub);
          return;
        }
        const e = f.ce('span');
        e.classList.add('at-name');
        e.appendChild(f.ct(sub));
        ff.accum.push(e);
        ff.prevStart = offset + sub.length + 1;
      };
    }
    f.accum = []; // accumulate strings and DOM elements here.
    f.rxUrl.lastIndex = f.replaceUrls.prevStart = 0; // reset regex cursor
    str.replace(f.rxUrl, f.replaceUrls);
    // Push remaining non-URL part of the string to the queue...
    if(f.replaceUrls.prevStart < str.length){
      f.accum.push((f.replaceUrls.prevStart?' ':'')+str.substring(f.replaceUrls.prevStart));
    }
    // Pass 2: process @NAME references...
    // TODO: only match NAME if it's the name of a currently participating
    // user. Add a second class if NAME == current user, and style that one
    // differently so that people can more easily see when they're spoken to.
    const accum2 = f.replaceAtName.accum = [];
    f.accum.forEach(function(v){
      if('string'===typeof v){
        f.rxAt.lastIndex = f.replaceAtName.prevStart = 0;
        v.replace(f.rxAt, f.replaceAtName);
        if(f.replaceAtName.prevStart < v.length){
          accum2.push((f.replaceAtName.prevStart?' ':'')+v.substring(f.replaceAtName.prevStart));
        }
      }else{
        accum2.push(v);
      }
    });
    delete f.accum;
    const theTgt = tgtElem || f.ce('div');
    const strings = [];
    accum2.forEach(function(e){
      if('string'===typeof e) strings.push(e);
      else strings.push(e.outerHTML);
    });
    D.parseHtml(theTgt, strings.join(''));
    return theTgt;
  }/*end messageToDOM()*/;

  /** Callback for poll() to inject new content into the page. */
  function newcontent(jx){
    var i;
    if('visible'===document.visibilityState){
      if(Chat.changesSincePageHidden){
        Chat.changesSincePageHidden = 0;
        Chat.pageTitle.innerText = Chat.pageTitleOrig;







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







279
280
281
282
283
284
285












































































286
287
288
289
290
291
292
      // truncation off the right edge of the page.
      const pRect = f.popup.e.getBoundingClientRect();
      x -= pRect.width/3*2;
    }
    f.popup.show(x, y);
  };













































































  /** Callback for poll() to inject new content into the page. */
  function newcontent(jx){
    var i;
    if('visible'===document.visibilityState){
      if(Chat.changesSincePageHidden){
        Chat.changesSincePageHidden = 0;
        Chat.pageTitle.innerText = Chat.pageTitleOrig;
421
422
423
424
425
426
427









428
429
430
431
432
433
434
435
          ));
        }
        const br = D.br();
        br.style.clear = "both";
        eContent.appendChild(br);
      }
      if(m.xmsg){









        messageToDOM(m.xmsg, eContent);
      }
      eContent.classList.add('chat-message');
    }
  }
  async function poll(){
    if(poll.running) return;
    poll.running = true;







>
>
>
>
>
>
>
>
>
|







345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
          ));
        }
        const br = D.br();
        br.style.clear = "both";
        eContent.appendChild(br);
      }
      if(m.xmsg){
        // The m.xmsg text comes from the same server as this script and
        // is guaranteed by that server to be "safe" HTML - safe in the
        // sense that it is not possible for a malefactor to inject HTML
        // or javascript or CSS.  The m.xmsg content might contain
        // hyperlinks, but otherwise it will be markup-free.  See the
        // chat_format_to_html() routine in the server for details.
        //
        // Hence, even though innerHTML is normally frowned upon, it is
        // perfectly safe to use in this context.
        eContent.innerHTML += m.xmsg
      }
      eContent.classList.add('chat-message');
    }
  }
  async function poll(){
    if(poll.running) return;
    poll.running = true;
Changes to src/wikiformat.c.
30
31
32
33
34
35
36

37
38
39
40
41
42
43
#define WIKI_NOBLOCK        0x004  /* No block markup of any kind */
#define WIKI_BUTTONS        0x008  /* Allow sub-menu buttons */
#define WIKI_NOBADLINKS     0x010  /* Ignore broken hyperlinks */
#define WIKI_LINKSONLY      0x020  /* No markup.  Only decorate links */
#define WIKI_NEWLINE        0x040  /* Honor \n - break lines at each \n */
#define WIKI_MARKDOWNLINKS  0x080  /* Resolve hyperlinks as in markdown */
#define WIKI_SAFE           0x100  /* Make the result safe for embedding */

#endif


/*
** These are the only markup attributes allowed.
*/
enum allowed_attr_t {







>







30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#define WIKI_NOBLOCK        0x004  /* No block markup of any kind */
#define WIKI_BUTTONS        0x008  /* Allow sub-menu buttons */
#define WIKI_NOBADLINKS     0x010  /* Ignore broken hyperlinks */
#define WIKI_LINKSONLY      0x020  /* No markup.  Only decorate links */
#define WIKI_NEWLINE        0x040  /* Honor \n - break lines at each \n */
#define WIKI_MARKDOWNLINKS  0x080  /* Resolve hyperlinks as in markdown */
#define WIKI_SAFE           0x100  /* Make the result safe for embedding */
#define WIKI_TARGET_BLANK   0x200  /* Hyperlinks go to a new window */
#endif


/*
** These are the only markup attributes allowed.
*/
enum allowed_attr_t {
1254
1255
1256
1257
1258
1259
1260



1261
1262
1263
1264
1265
1266
1267
  const char *z;
  char *zExtra = 0;
  const char *zExtraNS = 0;
  char *zRemote = 0;

  if( zTitle ){
    zExtra = mprintf(" title='%h'", zTitle);



    zExtraNS = zExtra+1;
  }
  assert( nClose>=20 );
  if( strncmp(zTarget, "http:", 5)==0
   || strncmp(zTarget, "https:", 6)==0
   || strncmp(zTarget, "ftp:", 4)==0
   || strncmp(zTarget, "mailto:", 7)==0







>
>
>







1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
  const char *z;
  char *zExtra = 0;
  const char *zExtraNS = 0;
  char *zRemote = 0;

  if( zTitle ){
    zExtra = mprintf(" title='%h'", zTitle);
    zExtraNS = zExtra+1;
  }else if( mFlags & WIKI_TARGET_BLANK ){
    zExtra = mprintf(" target='_blank'");
    zExtraNS = zExtra+1;
  }
  assert( nClose>=20 );
  if( strncmp(zTarget, "http:", 5)==0
   || strncmp(zTarget, "https:", 6)==0
   || strncmp(zTarget, "ftp:", 4)==0
   || strncmp(zTarget, "mailto:", 7)==0