Fossil

Check-in [fd5298f027]
Login

Check-in [fd5298f027]

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

Overview
Comment:/chat: add embedded view support for .wiki/.md/.pikchr file attachments, with the caveat that the rendering happens in an iframe and thus has some limitations/usability quirks compared to non-iframed content. Added based on feedback from a /chat session.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: fd5298f027f473fb8f25859b97049ebd2315cc942c1216088a2c1625d4490b74
User & Date: stephan 2022-11-29 20:59:02.010
Context
2022-11-29
21:08
Remove JS console debug output added by the previous checkin. ... (check-in: 2755b31a87 user: stephan tags: trunk)
20:59
/chat: add embedded view support for .wiki/.md/.pikchr file attachments, with the caveat that the rendering happens in an iframe and thus has some limitations/usability quirks compared to non-iframed content. Added based on feedback from a /chat session. ... (check-in: fd5298f027 user: stephan tags: trunk)
04:11
Improve consistency of "fossil branch" command. I don't think this deserves more attention but others may decide to make it do more. ... (check-in: 761a39c025 user: andybradford tags: trunk)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/chat.c.
133
134
135
136
137
138
139
140
141
142

143
144
145
146
147
148
149
** SETTING: chat-timeline-user    width=10
**
** If this setting is defined and is not an empty string, then
** timeline events are posted to the chat as they arrive. The synthesized
** chat messages appear to come from the user identified by this setting,
** not the user on the timeline event.
**
** All chat messages that come from the chat-timeline-user are interpreted
** as text/x-fossil-wiki instead of as text/markdown.  For this reason,
** the chat-timeline-user name should probably not be a real user.

*/
/*
** WEBPAGE: chat loadavg-exempt
**
** Start up a browser-based chat session.
**
** This is the main page that humans use to access the chatroom.  Simply







|
|
|
>







133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
** SETTING: chat-timeline-user    width=10
**
** If this setting is defined and is not an empty string, then
** timeline events are posted to the chat as they arrive. The synthesized
** chat messages appear to come from the user identified by this setting,
** not the user on the timeline event.
**
** All chat messages that come from the chat-timeline-user are
** interpreted as text/x-fossil-wiki instead of as text/x-markdown.
** For this reason, the chat-timeline-user name should probably not be
** a real user.
*/
/*
** WEBPAGE: chat loadavg-exempt
**
** Start up a browser-based chat session.
**
** This is the main page that humans use to access the chatroom.  Simply
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
    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, int isWiki){
  Blob out;
  blob_init(&out, "", 0);
  if( zMsg==0 || zMsg[0]==0 ){
    /* No-op */
  }else if( isWiki ){
    /* Used for chat-timeline-user.  The zMsg is text/x-fossil-wiki. */
    Blob bIn;
    blob_init(&bIn, zMsg, (int)strlen(zMsg));
    wiki_convert(&bIn, &out, WIKI_INLINE);
  }else{
    /* The common case:  zMsg is text/markdown */
    Blob bIn;
    blob_init(&bIn, zMsg, (int)strlen(zMsg));
    markdown_to_html(&bIn, NULL, &out);
  }
  return blob_str(&out);
}








|
|
|
|















|







418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
    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 wiki_convert() or
** markdown_to_html() to convert wiki/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, int isWiki){
  Blob out;
  blob_init(&out, "", 0);
  if( zMsg==0 || zMsg[0]==0 ){
    /* No-op */
  }else if( isWiki ){
    /* Used for chat-timeline-user.  The zMsg is text/x-fossil-wiki. */
    Blob bIn;
    blob_init(&bIn, zMsg, (int)strlen(zMsg));
    wiki_convert(&bIn, &out, WIKI_INLINE);
  }else{
    /* The common case:  zMsg is text/x-markdown */
    Blob bIn;
    blob_init(&bIn, zMsg, (int)strlen(zMsg));
    markdown_to_html(&bIn, NULL, &out);
  }
  return blob_str(&out);
}

779
780
781
782
783
784
785
786
787
















788
789
790
791
792

793
794
795
796
797
798
799
800
801
802
803
804
805
806
807

































808
809
810
811
812
813
814

/*
** WEBPAGE: chat-download hidden loadavg-exempt
**
** Download the CHAT.FILE attachment associated with a single chat
** entry.  The "name" query parameter begins with an integer that
** identifies the particular chat message. The integer may be followed
** by a / and a filename, which will indicate to the browser to use
** the indicated name when saving the file.
















*/
void chat_download_webpage(void){
  int msgid;
  Blob r;
  const char *zMime;

  login_check_credentials();
  if( !g.perm.Chat ){
    style_header("Chat Not Authorized");
    @ <h1>Not Authorized</h1>
    @ <p>You do not have permission to use the chatroom on this
    @ repository.</p>
    style_finish_page();
    return;
  }
  chat_create_tables();
  msgid = atoi(PD("name","0"));
  blob_zero(&r);
  zMime = db_text(0, "SELECT fmime FROM chat wHERE msgid=%d", msgid);
  if( zMime==0 ) return;
  db_blob(&r, "SELECT file FROM chat WHERE msgid=%d", msgid);

































  cgi_set_content_type(zMime);
  cgi_set_content(&r);
}


/*
** WEBPAGE: chat-delete hidden loadavg-exempt







|
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>





>










|




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







780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865

/*
** WEBPAGE: chat-download hidden loadavg-exempt
**
** Download the CHAT.FILE attachment associated with a single chat
** entry.  The "name" query parameter begins with an integer that
** identifies the particular chat message. The integer may be followed
** by a / and a filename, which will (A) indicate to the browser to
** use the indicated name when saving the file and (B) be used to
** guess the mimetype in some particular cases involving the "render"
** flag.
**
** If the "render" URL parameter is provided, the blob has a size
** greater than zero, and blob meets one of the following conditions
** then the fossil-rendered form of that content is returned, rather
** than the original:
**
** - Mimetype is text/x-markdown or text/markdown: emit HTML.
**
** - Mimetype is text/x-fossil-wiki or P("name") ends with ".wiki":
**   emit HTML.
**
** - Mimetype is text/x-pikchr or P("name") ends with ".pikchr": emit
**   image/svg+xml if rendering succeeds or text/html if rendering
**   fails.
*/
void chat_download_webpage(void){
  int msgid;
  Blob r;
  const char *zMime;
  const char *zName = PD("name","0");
  login_check_credentials();
  if( !g.perm.Chat ){
    style_header("Chat Not Authorized");
    @ <h1>Not Authorized</h1>
    @ <p>You do not have permission to use the chatroom on this
    @ repository.</p>
    style_finish_page();
    return;
  }
  chat_create_tables();
  msgid = atoi(zName);
  blob_zero(&r);
  zMime = db_text(0, "SELECT fmime FROM chat wHERE msgid=%d", msgid);
  if( zMime==0 ) return;
  db_blob(&r, "SELECT file FROM chat WHERE msgid=%d", msgid);
  if( r.nUsed>0 && P("render")!=0 ){
    /* Maybe return fossil-rendered form of the content. */
    Blob r2 = BLOB_INITIALIZER;    /* output target for rendering */
    const char * zMime2 = 0;       /* adjusted response mimetype */
    if(fossil_strcmp(zMime, "text/x-markdown")==0
       /* Firefox uploads md files with the mimetype text/markdown */
       || fossil_strcmp(zMime, "text/markdown")==0){
      markdown_to_html(&r, 0, &r2);
      safe_html(&r2);
      zMime2 = "text/html";
    }else if(fossil_strcmp(zMime, "text/x-fossil-wiki")==0
             || sqlite3_strglob("*.wiki", zName)==0){
      /* .wiki files get uploaded as application/octet-stream */
      wiki_convert(&r, &r2, 0);
      zMime2 = "text/html";
    }else if(fossil_strcmp(zMime, "text/x-pikchr")==0
             || sqlite3_strglob("*.pikchr",zName)==0){
      /* .pikchr files get uploaded as application/octet-stream */
      const char *zPikchr = blob_str(&r);
      int w = 0, h = 0;
      char *zOut = pikchr(zPikchr, "pikchr", 0, &w, &h);
      if(zOut){
        blob_append(&r2, zOut, -1);
      }
      zMime2 = w>0 ? "image/svg+xml" : "text/html";
      free(zOut);
    }
    if(r2.aData!=0){
      blob_swap(&r, &r2);
      blob_reset(&r2);
      zMime = zMime2;
    }
  }
  cgi_set_content_type(zMime);
  cgi_set_content(&r);
}


/*
** WEBPAGE: chat-delete hidden loadavg-exempt
Changes to src/fossil.page.chat.js.
912
913
914
915
916
917
918




919
920
921
922
923
924
































925
926
927
928
929
930
931
932
933
934
935
936
937
938
        //'-',pad2(d.getDate()), ' ',
        d.getHours(),":",
        (d.getMinutes()+100).toString().slice(1,3),
        ' ', dowMap[d.getDay()]
      ].join('');
    };





    const canEmbedFile = function f(msg){
      if(!f.$rx){
        f.$rx = /\.((html?)|(txt))$/i;
        f.$specificTypes = [
          'text/plain',
          'text/html'
































          // add more as we discover which ones Firefox won't
          // force the user to try to download.
        ];
      }
      if(msg.fmime){
        return (msg.fmime.startsWith("image/")
                || f.$specificTypes.indexOf(msg.fmime)>=0);
      }
      return msg.fname && f.$rx.test(msg.fname);
    };

    const adjustIFrameSize = function(msgObj){
      const iframe = msgObj.e.iframe;
      const body = iframe.contentWindow.document.querySelector('body');







>
>
>
>


|


|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>





<
|







912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965

966
967
968
969
970
971
972
973
        //'-',pad2(d.getDate()), ' ',
        d.getHours(),":",
        (d.getMinutes()+100).toString().slice(1,3),
        ' ', dowMap[d.getDay()]
      ].join('');
    };

    /**
       Returns true if this page believes it can embed a view of the
       file wrapped by the given message object, else returns false.
    */
    const canEmbedFile = function f(msg){
      if(!f.$rx){
        f.$rx = /\.((html?)|(txt)|(md)|(wiki)|(pikchr))$/i;
        f.$specificTypes = [
          'text/plain',
          'text/html',
          'text/x-markdown',
          /* Firefox sends text/markdown when uploading .md files */
          'text/markdown',
          'text/x-pikchr',
          'text/x-fossil-wiki'
          // add more as we discover which ones Firefox won't
          // force the user to try to download.
        ];
      }
      if(msg.fmime){
        if(msg.fmime.startsWith("image/")
           || f.$specificTypes.indexOf(msg.fmime)>=0){
          return true;
        }
      }
      return (msg.fname && f.$rx.test(msg.fname));
    };

    /**
      Returns true if the given message object "should"
      be embedded in fossil-rendered form instead of
      raw content form. This is only intended to be passed
      message objects for which canEmbedFile() returns true.
    */
    const shouldWikiRenderEmbed = function f(msg){
      if(!f.$rx){
        f.$rx = /\.((md)|(wiki)|(pikchr))$/i;
        f.$specificTypes = [
          'text/x-markdown',
          'text/markdown' /* Firefox-uploaded md files */,
          'text/x-pikchr',
          'text/x-fossil-wiki'
          // add more as we discover which ones Firefox won't
          // force the user to try to download.
        ];
      }
      if(msg.fmime){

        if(f.$specificTypes.indexOf(msg.fmime)>=0) return true;
      }
      return msg.fname && f.$rx.test(msg.fname);
    };

    const adjustIFrameSize = function(msgObj){
      const iframe = msgObj.e.iframe;
      const body = iframe.contentWindow.document.querySelector('body');
1009
1010
1011
1012
1013
1014
1015

1016
1017
1018



1019
1020
1021
1022
1023
1024

1025
1026
1027
1028
1029
1030
1031
            const w = D.addClass(D.div(), 'attachment-link');
            const a = D.a(downloadUri,
              // ^^^ add m.fname to URL to cause downloaded file to have that name.
              "(" + m.fname + " " + m.fsize + " bytes)"
            )
            D.attr(a,'target','_blank');
            D.append(w, a);

            if(canEmbedFile(m)){
              /* Add an option to embed HTML attachments in an iframe. The primary
                 use case is attached diffs. */



              D.addClass(contentTarget, 'wide');
              const embedTarget = this.e.content;
              const self = this;
              const btnEmbed = D.attr(D.checkbox("1", false), 'id',
                                      'embed-'+ds.msgid);
              const btnLabel = D.label(btnEmbed, "Embed");

              /* Maintenance reminder: do not disable the toggle
                 button while the content is loading because that will
                 cause it to get stuck in disabled mode if the browser
                 decides that loading the content should prompt the
                 user to download it, rather than embed it in the
                 iframe. */
              btnEmbed.addEventListener('change',function(){







>



>
>
>





|
>







1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
            const w = D.addClass(D.div(), 'attachment-link');
            const a = D.a(downloadUri,
              // ^^^ add m.fname to URL to cause downloaded file to have that name.
              "(" + m.fname + " " + m.fsize + " bytes)"
            )
            D.attr(a,'target','_blank');
            D.append(w, a);
            console.warn("canEmbedFile(",m,") =",canEmbedFile(m));
            if(canEmbedFile(m)){
              /* Add an option to embed HTML attachments in an iframe. The primary
                 use case is attached diffs. */
              const shouldWikiRender = shouldWikiRenderEmbed(m);
              const downloadArgs = shouldWikiRender ? '?render' : '';
              console.warn("downloadArgs",downloadArgs,m);
              D.addClass(contentTarget, 'wide');
              const embedTarget = this.e.content;
              const self = this;
              const btnEmbed = D.attr(D.checkbox("1", false), 'id',
                                      'embed-'+ds.msgid);
              const btnLabel = D.label(btnEmbed, shouldWikiRender
                                       ? "Embed (fossil-rendered)" : "Embed");
              /* Maintenance reminder: do not disable the toggle
                 button while the content is loading because that will
                 cause it to get stuck in disabled mode if the browser
                 decides that loading the content should prompt the
                 user to download it, rather than embed it in the
                 iframe. */
              btnEmbed.addEventListener('change',function(){
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
                }
                const iframe = self.e.iframe = document.createElement('iframe');
                D.append(embedTarget, iframe);
                iframe.addEventListener('load', function(){
                  self.e.$iframeLoaded = true;
                  adjustIFrameSize(self);
                });
                iframe.setAttribute('src', downloadUri);
              });
              D.append(w, btnEmbed, btnLabel);
            }
            contentTarget.appendChild(w);
          }
        }
        if(m.xmsg){







|







1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
                }
                const iframe = self.e.iframe = document.createElement('iframe');
                D.append(embedTarget, iframe);
                iframe.addEventListener('load', function(){
                  self.e.$iframeLoaded = true;
                  adjustIFrameSize(self);
                });
                iframe.setAttribute('src', downloadUri + downloadArgs);
              });
              D.append(w, btnEmbed, btnLabel);
            }
            contentTarget.appendChild(w);
          }
        }
        if(m.xmsg){
Changes to www/changes.wiki.
1
2
3
4
5




6
7
8
9
10
11
12
<title>Change Log</title>

<h2 id='v2_21'>Changes for version 2.21 (pending)</h2>
  *  Add the ability to put text descriptions on ticket report formats.
  *  Upgrade the test-find-pivot command to the [/help/merge-base|merge-base command].





<h2 id='v2_20'>Changes for version 2.20 (2022-11-16)</h2>
  *  Added the [/help?cmd=chat-timeline-user|chat-timeline-user setting].  If
     it is not an empty string, then any changes that would appear on the timeline
     are announced in [./chat.md|the chat room].
  *  The /unsubscribe page now requests confirmation. [./alerts.md|Email notifications]
     now contain only an "Unsubscribe" link, and not a link to subscription management.





>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<title>Change Log</title>

<h2 id='v2_21'>Changes for version 2.21 (pending)</h2>
  *  Add the ability to put text descriptions on ticket report formats.
  *  Upgrade the test-find-pivot command to the [/help/merge-base|merge-base command].
  *  The [/help?cmd=/chat|/chat page] can now embed fossil-rendered
     views of wiki/markdown/pikchr file attachments with the caveat that such
     embedding happens in an iframe and thus does not inherit styles and such
     from the containing browser window.

<h2 id='v2_20'>Changes for version 2.20 (2022-11-16)</h2>
  *  Added the [/help?cmd=chat-timeline-user|chat-timeline-user setting].  If
     it is not an empty string, then any changes that would appear on the timeline
     are announced in [./chat.md|the chat room].
  *  The /unsubscribe page now requests confirmation. [./alerts.md|Email notifications]
     now contain only an "Unsubscribe" link, and not a link to subscription management.