Fossil SCM

Initial work on attachment list for /wikiedit. Still requires the ability to update the list to see attachments made since edits were last locally loaded and stashed.

stephan 2021-07-16 16:51 trunk
Commit 74c6b9c5be2c104807953603dbff9958f40c831fad7b21f61539c69db79fa812
--- src/default.css
+++ src/default.css
@@ -1795,10 +1795,14 @@
17951795
display: none;
17961796
}
17971797
body.branch .submenu > a.timeline-link.selected {
17981798
display: inline;
17991799
}
1800
+
1801
+.monospace {
1802
+ font-family: monospace;
1803
+}
18001804
18011805
/* Objects in the "desktoponly" class are invisible on mobile */
18021806
@media screen and (max-width: 600px) {
18031807
.desktoponly {
18041808
display: none;
18051809
--- src/default.css
+++ src/default.css
@@ -1795,10 +1795,14 @@
1795 display: none;
1796 }
1797 body.branch .submenu > a.timeline-link.selected {
1798 display: inline;
1799 }
 
 
 
 
1800
1801 /* Objects in the "desktoponly" class are invisible on mobile */
1802 @media screen and (max-width: 600px) {
1803 .desktoponly {
1804 display: none;
1805
--- src/default.css
+++ src/default.css
@@ -1795,10 +1795,14 @@
1795 display: none;
1796 }
1797 body.branch .submenu > a.timeline-link.selected {
1798 display: inline;
1799 }
1800
1801 .monospace {
1802 font-family: monospace;
1803 }
1804
1805 /* Objects in the "desktoponly" class are invisible on mobile */
1806 @media screen and (max-width: 600px) {
1807 .desktoponly {
1808 display: none;
1809
--- src/fossil.copybutton.js
+++ src/fossil.copybutton.js
@@ -21,11 +21,11 @@
2121
2222
One of copyFromElement or copyFromId must be provided, but copyFromId
2323
may optionally be provided via e.dataset.copyFromId.
2424
2525
.extractText: optional callback which is triggered when the copy
26
- button is clicked. I tmust return the text to copy to the
26
+ button is clicked. It must return the text to copy to the
2727
clipboard. The default is to extract it from the copy-from
2828
element, using its [value] member, if it has one, else its
2929
[innerText]. A client-provided callback may use any data source
3030
it likes, so long as it's synchronous. If this function returns a
3131
falsy value then the clipboard is not modified. This function is
@@ -81,11 +81,11 @@
8181
});
8282
*/
8383
F.copyButton = function f(e, opt){
8484
if('string'===typeof e){
8585
e = document.querySelector(e);
86
- }
86
+ }
8787
opt = F.mergeLastWins(f.defaultOptions, opt);
8888
if(opt.cssClass){
8989
D.addClass(e, opt.cssClass);
9090
}
9191
var srcId, srcElem;
9292
--- src/fossil.copybutton.js
+++ src/fossil.copybutton.js
@@ -21,11 +21,11 @@
21
22 One of copyFromElement or copyFromId must be provided, but copyFromId
23 may optionally be provided via e.dataset.copyFromId.
24
25 .extractText: optional callback which is triggered when the copy
26 button is clicked. I tmust return the text to copy to the
27 clipboard. The default is to extract it from the copy-from
28 element, using its [value] member, if it has one, else its
29 [innerText]. A client-provided callback may use any data source
30 it likes, so long as it's synchronous. If this function returns a
31 falsy value then the clipboard is not modified. This function is
@@ -81,11 +81,11 @@
81 });
82 */
83 F.copyButton = function f(e, opt){
84 if('string'===typeof e){
85 e = document.querySelector(e);
86 }
87 opt = F.mergeLastWins(f.defaultOptions, opt);
88 if(opt.cssClass){
89 D.addClass(e, opt.cssClass);
90 }
91 var srcId, srcElem;
92
--- src/fossil.copybutton.js
+++ src/fossil.copybutton.js
@@ -21,11 +21,11 @@
21
22 One of copyFromElement or copyFromId must be provided, but copyFromId
23 may optionally be provided via e.dataset.copyFromId.
24
25 .extractText: optional callback which is triggered when the copy
26 button is clicked. It must return the text to copy to the
27 clipboard. The default is to extract it from the copy-from
28 element, using its [value] member, if it has one, else its
29 [innerText]. A client-provided callback may use any data source
30 it likes, so long as it's synchronous. If this function returns a
31 falsy value then the clipboard is not modified. This function is
@@ -81,11 +81,11 @@
81 });
82 */
83 F.copyButton = function f(e, opt){
84 if('string'===typeof e){
85 e = document.querySelector(e);
86 }
87 opt = F.mergeLastWins(f.defaultOptions, opt);
88 if(opt.cssClass){
89 D.addClass(e, opt.cssClass);
90 }
91 var srcId, srcElem;
92
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -182,10 +182,11 @@
182182
record.type = winfo.type;
183183
record.parent = winfo.parent;
184184
record.version = winfo.version;
185185
record.stashTime = new Date().getTime();
186186
record.isEmpty = !!winfo.isEmpty;
187
+ record.attachments = winfo.attachments;
187188
this.storeIndex();
188189
if(arguments.length>1){
189190
if(content) delete record.isEmpty;
190191
F.storage.set(this.contentKey(key), content);
191192
}
@@ -818,11 +819,12 @@
818819
if(!ajaxState.toDisable){
819820
ajaxState.toDisable = document.querySelectorAll(
820821
['button:not([disabled])',
821822
'input:not([disabled])',
822823
'select:not([disabled])',
823
- 'textarea:not([disabled])'
824
+ 'textarea:not([disabled])',
825
+ 'fieldset:not([disabled])'
824826
].join(',')
825827
);
826828
}
827829
if(1===++ajaxState.count){
828830
D.addClass(document.body, 'waiting');
@@ -853,10 +855,11 @@
853855
cbAutoPreview: E('#cb-preview-autorefresh'),
854856
previewTarget: E('#wikiedit-tab-preview-wrapper'),
855857
diffTarget: E('#wikiedit-tab-diff-wrapper'),
856858
editStatus: E('#wikiedit-edit-status'),
857859
tabContainer: E('#wikiedit-tabs'),
860
+ attachmentContainer: E("#attachment-wrapper"),
858861
tabs:{
859862
pageList: E('#wikiedit-tab-pages'),
860863
content: E('#wikiedit-tab-content'),
861864
preview: E('#wikiedit-tab-preview'),
862865
diff: E('#wikiedit-tab-diff'),
@@ -1122,10 +1125,68 @@
11221125
*/
11231126
const affirmPageLoaded = function(quiet){
11241127
if(!P.winfo && !quiet) F.error("No wiki page is loaded.");
11251128
return !!P.winfo;
11261129
};
1130
+
1131
+ /**
1132
+ Updates the attachments list from this.winfo.
1133
+ */
1134
+ P.updateAttachmentsView = function f(){
1135
+ if(!f.eAttach){
1136
+ f.eAttach = P.e.attachmentContainer.querySelector('div');
1137
+ }
1138
+ D.clearElement(f.eAttach);
1139
+ const wi = this.winfo;
1140
+ if(!wi){
1141
+ D.append(f.eAttach,"No page loaded.");
1142
+ return;
1143
+ }else if(!wi.attachments || !wi.attachments.length){
1144
+ D.append(f.eAttach,"No attachments found for the current page.");
1145
+ return;
1146
+ }
1147
+ D.append(
1148
+ f.eAttach,
1149
+ D.append(D.p(),
1150
+ D.a(F.repoUrl('attachlist',{page:wi.name}),
1151
+ "Attachments for page ["+wi.name+"]"))
1152
+ );
1153
+ wi.attachments.forEach(function(a){
1154
+ const wrap = D.div();
1155
+ D.append(f.eAttach, wrap);
1156
+ D.append(wrap,
1157
+ D.append(D.div(),
1158
+ "Attachment ",
1159
+ D.addClass(
1160
+ D.a(F.repoUrl('ainfo',{name:a.uuid}),
1161
+ F.hashDigits(a.uuid,true)),
1162
+ 'monospace'),
1163
+ " ",
1164
+ a.filename,
1165
+ (a.isLatest ? " (latest)" : "")
1166
+ )
1167
+ );
1168
+ //D.append(wrap,D.append(D.div(), "URLs:"));
1169
+ const ul = D.ul();
1170
+ D.append(wrap, ul);
1171
+ [ // List download URL variants for each attachment:
1172
+ [
1173
+ "attachdownload?page=",
1174
+ encodeURIComponent(wi.name),
1175
+ "&file=",
1176
+ encodeURIComponent(a.filename)
1177
+ ].join(''),
1178
+ "raw/"+a.src
1179
+ ].forEach(function(url){
1180
+ const imgUrl = D.append(D.addClass(D.span(), 'monospace'), url);
1181
+ const urlCopy = D.span();
1182
+ const li = D.li(ul);
1183
+ D.append(li, urlCopy, " ", imgUrl);
1184
+ F.copyButton(urlCopy, {copyFromElement: imgUrl});
1185
+ });
1186
+ });
1187
+ };
11271188
11281189
/** Updates the in-tab title/edit status information */
11291190
P.updateEditStatus = function f(){
11301191
if(!f.eLinks){
11311192
f.eName = P.e.editStatus.querySelector('span.name');
@@ -1133,24 +1194,24 @@
11331194
}
11341195
const wi = this.winfo;
11351196
D.clearElement(f.eName, f.eLinks);
11361197
if(!wi){
11371198
D.append(f.eName, '(no page loaded)');
1199
+ this.updateAttachmentsView();
11381200
return;
11391201
}
1140
- var marker = getEditMarker(wi, false);
1141
- D.append(f.eName,marker,wi.name);
1142
- if(wi.version){
1143
- D.append(
1144
- f.eLinks,
1145
- D.a(F.repoUrl('wiki',{name:wi.name}),"viewer"),
1146
- D.a(F.repoUrl('whistory',{name:wi.name}),'history'),
1147
- D.a(F.repoUrl('attachlist',{page:wi.name}),"attachments"),
1148
- D.a(F.repoUrl('attachadd',{page:wi.name,from: F.repoUrl('wikiedit',{name: wi.name})}), "attach"),
1149
- D.a(F.repoUrl('wikiedit',{name:wi.name}),"editor permalink")
1150
- );
1151
- }
1202
+ D.append(f.eName,getEditMarker(wi, false),wi.name);
1203
+ if(!wi.version) return;
1204
+ D.append(
1205
+ f.eLinks,
1206
+ D.a(F.repoUrl('wiki',{name:wi.name}),"viewer"),
1207
+ D.a(F.repoUrl('whistory',{name:wi.name}),'history'),
1208
+ D.a(F.repoUrl('attachlist',{page:wi.name}),"attachments"),
1209
+ D.a(F.repoUrl('attachadd',{page:wi.name,from: F.repoUrl('wikiedit',{name: wi.name})}), "attach"),
1210
+ D.a(F.repoUrl('wikiedit',{name:wi.name}),"editor permalink")
1211
+ );
1212
+ this.updateAttachmentsView();
11521213
};
11531214
11541215
/**
11551216
Update the page title and header based on the state of
11561217
this.winfo. A no-op if this.winfo is not set. Returns this.
@@ -1313,11 +1374,12 @@
13131374
mimetype: stashWinfo.mimetype,
13141375
type: stashWinfo.type,
13151376
version: stashWinfo.version,
13161377
parent: stashWinfo.parent,
13171378
isEmpty: !!stashWinfo.isEmpty,
1318
- content: $stash.stashedContent(stashWinfo)
1379
+ content: $stash.stashedContent(stashWinfo),
1380
+ attachments: stashWinfo.attachments
13191381
});
13201382
this._isDirty = true/*b/c loading normally clears that flag*/;
13211383
return this;
13221384
}
13231385
F.message(
13241386
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -182,10 +182,11 @@
182 record.type = winfo.type;
183 record.parent = winfo.parent;
184 record.version = winfo.version;
185 record.stashTime = new Date().getTime();
186 record.isEmpty = !!winfo.isEmpty;
 
187 this.storeIndex();
188 if(arguments.length>1){
189 if(content) delete record.isEmpty;
190 F.storage.set(this.contentKey(key), content);
191 }
@@ -818,11 +819,12 @@
818 if(!ajaxState.toDisable){
819 ajaxState.toDisable = document.querySelectorAll(
820 ['button:not([disabled])',
821 'input:not([disabled])',
822 'select:not([disabled])',
823 'textarea:not([disabled])'
 
824 ].join(',')
825 );
826 }
827 if(1===++ajaxState.count){
828 D.addClass(document.body, 'waiting');
@@ -853,10 +855,11 @@
853 cbAutoPreview: E('#cb-preview-autorefresh'),
854 previewTarget: E('#wikiedit-tab-preview-wrapper'),
855 diffTarget: E('#wikiedit-tab-diff-wrapper'),
856 editStatus: E('#wikiedit-edit-status'),
857 tabContainer: E('#wikiedit-tabs'),
 
858 tabs:{
859 pageList: E('#wikiedit-tab-pages'),
860 content: E('#wikiedit-tab-content'),
861 preview: E('#wikiedit-tab-preview'),
862 diff: E('#wikiedit-tab-diff'),
@@ -1122,10 +1125,68 @@
1122 */
1123 const affirmPageLoaded = function(quiet){
1124 if(!P.winfo && !quiet) F.error("No wiki page is loaded.");
1125 return !!P.winfo;
1126 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1127
1128 /** Updates the in-tab title/edit status information */
1129 P.updateEditStatus = function f(){
1130 if(!f.eLinks){
1131 f.eName = P.e.editStatus.querySelector('span.name');
@@ -1133,24 +1194,24 @@
1133 }
1134 const wi = this.winfo;
1135 D.clearElement(f.eName, f.eLinks);
1136 if(!wi){
1137 D.append(f.eName, '(no page loaded)');
 
1138 return;
1139 }
1140 var marker = getEditMarker(wi, false);
1141 D.append(f.eName,marker,wi.name);
1142 if(wi.version){
1143 D.append(
1144 f.eLinks,
1145 D.a(F.repoUrl('wiki',{name:wi.name}),"viewer"),
1146 D.a(F.repoUrl('whistory',{name:wi.name}),'history'),
1147 D.a(F.repoUrl('attachlist',{page:wi.name}),"attachments"),
1148 D.a(F.repoUrl('attachadd',{page:wi.name,from: F.repoUrl('wikiedit',{name: wi.name})}), "attach"),
1149 D.a(F.repoUrl('wikiedit',{name:wi.name}),"editor permalink")
1150 );
1151 }
1152 };
1153
1154 /**
1155 Update the page title and header based on the state of
1156 this.winfo. A no-op if this.winfo is not set. Returns this.
@@ -1313,11 +1374,12 @@
1313 mimetype: stashWinfo.mimetype,
1314 type: stashWinfo.type,
1315 version: stashWinfo.version,
1316 parent: stashWinfo.parent,
1317 isEmpty: !!stashWinfo.isEmpty,
1318 content: $stash.stashedContent(stashWinfo)
 
1319 });
1320 this._isDirty = true/*b/c loading normally clears that flag*/;
1321 return this;
1322 }
1323 F.message(
1324
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -182,10 +182,11 @@
182 record.type = winfo.type;
183 record.parent = winfo.parent;
184 record.version = winfo.version;
185 record.stashTime = new Date().getTime();
186 record.isEmpty = !!winfo.isEmpty;
187 record.attachments = winfo.attachments;
188 this.storeIndex();
189 if(arguments.length>1){
190 if(content) delete record.isEmpty;
191 F.storage.set(this.contentKey(key), content);
192 }
@@ -818,11 +819,12 @@
819 if(!ajaxState.toDisable){
820 ajaxState.toDisable = document.querySelectorAll(
821 ['button:not([disabled])',
822 'input:not([disabled])',
823 'select:not([disabled])',
824 'textarea:not([disabled])',
825 'fieldset:not([disabled])'
826 ].join(',')
827 );
828 }
829 if(1===++ajaxState.count){
830 D.addClass(document.body, 'waiting');
@@ -853,10 +855,11 @@
855 cbAutoPreview: E('#cb-preview-autorefresh'),
856 previewTarget: E('#wikiedit-tab-preview-wrapper'),
857 diffTarget: E('#wikiedit-tab-diff-wrapper'),
858 editStatus: E('#wikiedit-edit-status'),
859 tabContainer: E('#wikiedit-tabs'),
860 attachmentContainer: E("#attachment-wrapper"),
861 tabs:{
862 pageList: E('#wikiedit-tab-pages'),
863 content: E('#wikiedit-tab-content'),
864 preview: E('#wikiedit-tab-preview'),
865 diff: E('#wikiedit-tab-diff'),
@@ -1122,10 +1125,68 @@
1125 */
1126 const affirmPageLoaded = function(quiet){
1127 if(!P.winfo && !quiet) F.error("No wiki page is loaded.");
1128 return !!P.winfo;
1129 };
1130
1131 /**
1132 Updates the attachments list from this.winfo.
1133 */
1134 P.updateAttachmentsView = function f(){
1135 if(!f.eAttach){
1136 f.eAttach = P.e.attachmentContainer.querySelector('div');
1137 }
1138 D.clearElement(f.eAttach);
1139 const wi = this.winfo;
1140 if(!wi){
1141 D.append(f.eAttach,"No page loaded.");
1142 return;
1143 }else if(!wi.attachments || !wi.attachments.length){
1144 D.append(f.eAttach,"No attachments found for the current page.");
1145 return;
1146 }
1147 D.append(
1148 f.eAttach,
1149 D.append(D.p(),
1150 D.a(F.repoUrl('attachlist',{page:wi.name}),
1151 "Attachments for page ["+wi.name+"]"))
1152 );
1153 wi.attachments.forEach(function(a){
1154 const wrap = D.div();
1155 D.append(f.eAttach, wrap);
1156 D.append(wrap,
1157 D.append(D.div(),
1158 "Attachment ",
1159 D.addClass(
1160 D.a(F.repoUrl('ainfo',{name:a.uuid}),
1161 F.hashDigits(a.uuid,true)),
1162 'monospace'),
1163 " ",
1164 a.filename,
1165 (a.isLatest ? " (latest)" : "")
1166 )
1167 );
1168 //D.append(wrap,D.append(D.div(), "URLs:"));
1169 const ul = D.ul();
1170 D.append(wrap, ul);
1171 [ // List download URL variants for each attachment:
1172 [
1173 "attachdownload?page=",
1174 encodeURIComponent(wi.name),
1175 "&file=",
1176 encodeURIComponent(a.filename)
1177 ].join(''),
1178 "raw/"+a.src
1179 ].forEach(function(url){
1180 const imgUrl = D.append(D.addClass(D.span(), 'monospace'), url);
1181 const urlCopy = D.span();
1182 const li = D.li(ul);
1183 D.append(li, urlCopy, " ", imgUrl);
1184 F.copyButton(urlCopy, {copyFromElement: imgUrl});
1185 });
1186 });
1187 };
1188
1189 /** Updates the in-tab title/edit status information */
1190 P.updateEditStatus = function f(){
1191 if(!f.eLinks){
1192 f.eName = P.e.editStatus.querySelector('span.name');
@@ -1133,24 +1194,24 @@
1194 }
1195 const wi = this.winfo;
1196 D.clearElement(f.eName, f.eLinks);
1197 if(!wi){
1198 D.append(f.eName, '(no page loaded)');
1199 this.updateAttachmentsView();
1200 return;
1201 }
1202 D.append(f.eName,getEditMarker(wi, false),wi.name);
1203 if(!wi.version) return;
1204 D.append(
1205 f.eLinks,
1206 D.a(F.repoUrl('wiki',{name:wi.name}),"viewer"),
1207 D.a(F.repoUrl('whistory',{name:wi.name}),'history'),
1208 D.a(F.repoUrl('attachlist',{page:wi.name}),"attachments"),
1209 D.a(F.repoUrl('attachadd',{page:wi.name,from: F.repoUrl('wikiedit',{name: wi.name})}), "attach"),
1210 D.a(F.repoUrl('wikiedit',{name:wi.name}),"editor permalink")
1211 );
1212 this.updateAttachmentsView();
 
1213 };
1214
1215 /**
1216 Update the page title and header based on the state of
1217 this.winfo. A no-op if this.winfo is not set. Returns this.
@@ -1313,11 +1374,12 @@
1374 mimetype: stashWinfo.mimetype,
1375 type: stashWinfo.type,
1376 version: stashWinfo.version,
1377 parent: stashWinfo.parent,
1378 isEmpty: !!stashWinfo.isEmpty,
1379 content: $stash.stashedContent(stashWinfo),
1380 attachments: stashWinfo.attachments
1381 });
1382 this._isDirty = true/*b/c loading normally clears that flag*/;
1383 return this;
1384 }
1385 F.message(
1386
+126 -1
--- src/wiki.c
+++ src/wiki.c
@@ -759,10 +759,98 @@
759759
}
760760
}
761761
ajax_route_error(403, "%s", zErr);
762762
return 0;
763763
}
764
+
765
+
766
+/*
767
+** Emits an array of attachment info records for the given wiki page
768
+** artifact.
769
+**
770
+** Output format:
771
+**
772
+** [{
773
+** "uuid": attachment artifact hash,
774
+** "src": hash of the attachment blob,
775
+** "target": wiki page name or ticket/event ID,
776
+** "filename": filename of attachment,
777
+** "mtime": ISO-8601 timestamp UTC,
778
+** "isLatest": true this is the latest version of this file
779
+** else false,
780
+** }, ...once per attachment]
781
+**
782
+** If there are no matching attachments then it will emit a JSON
783
+** null (if nullIfEmpty) or an empty JSON array.
784
+**
785
+** If latestOnly is true then only the most recent entry for a given
786
+** attachment is emitted, else all versions are emitted in descending
787
+** mtime order.
788
+*/
789
+static void wiki_ajax_emit_page_attachments(Manifest * pWiki,
790
+ int latestOnly,
791
+ int nullIfEmpty){
792
+ int i = 0;
793
+ Stmt q = empty_Stmt;
794
+ db_prepare(&q,
795
+ "SELECT datetime(mtime), src, target, filename, isLatest,"
796
+ " (SELECT uuid FROM blob WHERE rid=attachid) uuid"
797
+ " FROM attachment"
798
+ " WHERE target=%Q"
799
+ " AND (isLatest OR %d)"
800
+ " ORDER BY target, isLatest DESC, mtime DESC",
801
+ pWiki->zWikiTitle, !latestOnly
802
+ );
803
+ while(SQLITE_ROW == db_step(&q)){
804
+ const char * zTime = db_column_text(&q, 0);
805
+ const char * zSrc = db_column_text(&q, 1);
806
+ const char * zTarget = db_column_text(&q, 2);
807
+ const char * zName = db_column_text(&q, 3);
808
+ const int isLatest = db_column_int(&q, 4);
809
+ const char * zUuid = db_column_text(&q, 5);
810
+ if(!i++){
811
+ CX("[");
812
+ }else{
813
+ CX(",");
814
+ }
815
+ CX("{");
816
+ CX("\"uuid\": %!j, \"src\": %!j, \"target\": %!j, "
817
+ "\"filename\": %!j, \"mtime\": %!j, \"isLatest\": %s}",
818
+ zUuid, zSrc, zTarget,
819
+ zName, zTime, isLatest ? "true" : "false");
820
+ }
821
+ db_finalize(&q);
822
+ if(!i){
823
+ if(nullIfEmpty){
824
+ CX("null");
825
+ }else{
826
+ CX("[]");
827
+ }
828
+ }else{
829
+ CX("]");
830
+ }
831
+}
832
+
833
+/*
834
+ ** Proxy for wiki_ajax_emit_page_attachments2() which attempts to
835
+ ** load the given wiki page artifact and fails if it cannot.
836
+ ** Returns true if it loads the page, else false.
837
+ */
838
+static int wiki_ajax_emit_page_attachments2(const char *zPageName,
839
+ int latestOnly,
840
+ int nullIfEmpty){
841
+ Manifest * pWiki = 0;
842
+ if( !wiki_fetch_by_name(zPageName, 0, 0, &pWiki) ){
843
+ ajax_route_error(404, "Wiki page could not be loaded: %s",
844
+ zPageName);
845
+ return 0;
846
+ }
847
+ wiki_ajax_emit_page_attachments(pWiki, latestOnly, nullIfEmpty);
848
+ manifest_destroy(pWiki);
849
+ return 1;
850
+}
851
+
764852
765853
/*
766854
** Loads the given wiki page, sets the response type to
767855
** application/json, and emits it as a JSON object. If zPageName is a
768856
** sandbox page then a "fake" object is emitted, as the wikiajax API
@@ -778,10 +866,11 @@
778866
** mimetype: "mimetype",
779867
** version: UUID string or null for a sandbox page,
780868
** parent: "parent uuid" or null if no parent,
781869
** isDeleted: true if the page has no content (is "deleted")
782870
** else not set (making it "falsy" in JS),
871
+** attachments: see wiki_ajax_emit_page_attachments()
783872
** content: "page content" (only if includeContent is true)
784873
** }
785874
**
786875
** If includeContent is false then the content member is elided.
787876
*/
@@ -828,10 +917,12 @@
828917
CX(", \"isEmpty\": true");
829918
}
830919
if(includeContent){
831920
CX(", \"content\": %!j", pWiki->zWiki);
832921
}
922
+ CX(", \"attachments\": ");
923
+ wiki_ajax_emit_page_attachments(pWiki, 0, 1);
833924
CX("}");
834925
fossil_free(zUuid);
835926
manifest_destroy(pWiki);
836927
return 2;
837928
}
@@ -919,10 +1010,39 @@
9191010
return;
9201011
}
9211012
cgi_set_content_type("application/json");
9221013
wiki_ajax_emit_page_object(zPageName, 1);
9231014
}
1015
+
1016
+/*
1017
+** Ajax route handler for /wikiajax/attachments.
1018
+**
1019
+** URL params:
1020
+**
1021
+** page = the wiki page name
1022
+** latestOnly = if set, only latest version of each attachment
1023
+** is emitted.
1024
+**
1025
+** Responds with JSON: see wiki_ajax_emit_page_attachments()
1026
+**
1027
+** If there are no attachments it emits an empty array instead of null
1028
+** so that the output can be used as a top-level JSON response.
1029
+**
1030
+** On error, an object in the form documented by
1031
+** ajax_route_error(). On success, an object in the form documented
1032
+** for wiki_ajax_emit_page_object().
1033
+*/
1034
+static void wiki_ajax_route_attachments(void){
1035
+ const char * zPageName = P("page");
1036
+ const int fLatestOnly = P("latestOnly")!=0;
1037
+ if( zPageName==0 || zPageName[0]==0 ){
1038
+ ajax_route_error(400,"Missing page name.");
1039
+ return;
1040
+ }
1041
+ cgi_set_content_type("application/json");
1042
+ wiki_ajax_emit_page_attachments2(zPageName, fLatestOnly, 0);
1043
+}
9241044
9251045
/*
9261046
** Ajax route handler for /wikiajax/diff.
9271047
**
9281048
** URL params:
@@ -1076,10 +1196,11 @@
10761196
const char * zName = P("name");
10771197
AjaxRoute routeName = {0,0,0,0};
10781198
const AjaxRoute * pRoute = 0;
10791199
const AjaxRoute routes[] = {
10801200
/* Keep these sorted by zName (for bsearch()) */
1201
+ {"attachments", wiki_ajax_route_attachments, 0, 0},
10811202
{"diff", wiki_ajax_route_diff, 1, 1},
10821203
{"fetch", wiki_ajax_route_fetch, 0, 0},
10831204
{"list", wiki_ajax_route_list, 0, 0},
10841205
{"preview", wiki_ajax_route_preview, 0, 1},
10851206
{"save", wiki_ajax_route_save, 1, 1}
@@ -1302,13 +1423,17 @@
13021423
13031424
/****** The obligatory "Misc" tab ******/
13041425
{
13051426
CX("<div id='wikiedit-tab-misc' "
13061427
"data-tab-parent='wikiedit-tabs' "
1307
- "data-tab-label='Help' "
1428
+ "data-tab-label='Misc.' "
13081429
"class='hidden'"
13091430
">");
1431
+ CX("<fieldset id='attachment-wrapper'>");
1432
+ CX("<legend>Attachments</legend>");
1433
+ CX("<div>No attachments for the current page.</div>");
1434
+ CX("</fieldset>");
13101435
CX("<h2>Wiki formatting rules</h2>");
13111436
CX("<ul>");
13121437
CX("<li><a href='%R/wiki_rules'>Fossil wiki format</a></li>");
13131438
CX("<li><a href='%R/md_rules'>Markdown format</a></li>");
13141439
CX("<li>Plain-text pages use no special formatting.</li>");
13151440
--- src/wiki.c
+++ src/wiki.c
@@ -759,10 +759,98 @@
759 }
760 }
761 ajax_route_error(403, "%s", zErr);
762 return 0;
763 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
764
765 /*
766 ** Loads the given wiki page, sets the response type to
767 ** application/json, and emits it as a JSON object. If zPageName is a
768 ** sandbox page then a "fake" object is emitted, as the wikiajax API
@@ -778,10 +866,11 @@
778 ** mimetype: "mimetype",
779 ** version: UUID string or null for a sandbox page,
780 ** parent: "parent uuid" or null if no parent,
781 ** isDeleted: true if the page has no content (is "deleted")
782 ** else not set (making it "falsy" in JS),
 
783 ** content: "page content" (only if includeContent is true)
784 ** }
785 **
786 ** If includeContent is false then the content member is elided.
787 */
@@ -828,10 +917,12 @@
828 CX(", \"isEmpty\": true");
829 }
830 if(includeContent){
831 CX(", \"content\": %!j", pWiki->zWiki);
832 }
 
 
833 CX("}");
834 fossil_free(zUuid);
835 manifest_destroy(pWiki);
836 return 2;
837 }
@@ -919,10 +1010,39 @@
919 return;
920 }
921 cgi_set_content_type("application/json");
922 wiki_ajax_emit_page_object(zPageName, 1);
923 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
924
925 /*
926 ** Ajax route handler for /wikiajax/diff.
927 **
928 ** URL params:
@@ -1076,10 +1196,11 @@
1076 const char * zName = P("name");
1077 AjaxRoute routeName = {0,0,0,0};
1078 const AjaxRoute * pRoute = 0;
1079 const AjaxRoute routes[] = {
1080 /* Keep these sorted by zName (for bsearch()) */
 
1081 {"diff", wiki_ajax_route_diff, 1, 1},
1082 {"fetch", wiki_ajax_route_fetch, 0, 0},
1083 {"list", wiki_ajax_route_list, 0, 0},
1084 {"preview", wiki_ajax_route_preview, 0, 1},
1085 {"save", wiki_ajax_route_save, 1, 1}
@@ -1302,13 +1423,17 @@
1302
1303 /****** The obligatory "Misc" tab ******/
1304 {
1305 CX("<div id='wikiedit-tab-misc' "
1306 "data-tab-parent='wikiedit-tabs' "
1307 "data-tab-label='Help' "
1308 "class='hidden'"
1309 ">");
 
 
 
 
1310 CX("<h2>Wiki formatting rules</h2>");
1311 CX("<ul>");
1312 CX("<li><a href='%R/wiki_rules'>Fossil wiki format</a></li>");
1313 CX("<li><a href='%R/md_rules'>Markdown format</a></li>");
1314 CX("<li>Plain-text pages use no special formatting.</li>");
1315
--- src/wiki.c
+++ src/wiki.c
@@ -759,10 +759,98 @@
759 }
760 }
761 ajax_route_error(403, "%s", zErr);
762 return 0;
763 }
764
765
766 /*
767 ** Emits an array of attachment info records for the given wiki page
768 ** artifact.
769 **
770 ** Output format:
771 **
772 ** [{
773 ** "uuid": attachment artifact hash,
774 ** "src": hash of the attachment blob,
775 ** "target": wiki page name or ticket/event ID,
776 ** "filename": filename of attachment,
777 ** "mtime": ISO-8601 timestamp UTC,
778 ** "isLatest": true this is the latest version of this file
779 ** else false,
780 ** }, ...once per attachment]
781 **
782 ** If there are no matching attachments then it will emit a JSON
783 ** null (if nullIfEmpty) or an empty JSON array.
784 **
785 ** If latestOnly is true then only the most recent entry for a given
786 ** attachment is emitted, else all versions are emitted in descending
787 ** mtime order.
788 */
789 static void wiki_ajax_emit_page_attachments(Manifest * pWiki,
790 int latestOnly,
791 int nullIfEmpty){
792 int i = 0;
793 Stmt q = empty_Stmt;
794 db_prepare(&q,
795 "SELECT datetime(mtime), src, target, filename, isLatest,"
796 " (SELECT uuid FROM blob WHERE rid=attachid) uuid"
797 " FROM attachment"
798 " WHERE target=%Q"
799 " AND (isLatest OR %d)"
800 " ORDER BY target, isLatest DESC, mtime DESC",
801 pWiki->zWikiTitle, !latestOnly
802 );
803 while(SQLITE_ROW == db_step(&q)){
804 const char * zTime = db_column_text(&q, 0);
805 const char * zSrc = db_column_text(&q, 1);
806 const char * zTarget = db_column_text(&q, 2);
807 const char * zName = db_column_text(&q, 3);
808 const int isLatest = db_column_int(&q, 4);
809 const char * zUuid = db_column_text(&q, 5);
810 if(!i++){
811 CX("[");
812 }else{
813 CX(",");
814 }
815 CX("{");
816 CX("\"uuid\": %!j, \"src\": %!j, \"target\": %!j, "
817 "\"filename\": %!j, \"mtime\": %!j, \"isLatest\": %s}",
818 zUuid, zSrc, zTarget,
819 zName, zTime, isLatest ? "true" : "false");
820 }
821 db_finalize(&q);
822 if(!i){
823 if(nullIfEmpty){
824 CX("null");
825 }else{
826 CX("[]");
827 }
828 }else{
829 CX("]");
830 }
831 }
832
833 /*
834 ** Proxy for wiki_ajax_emit_page_attachments2() which attempts to
835 ** load the given wiki page artifact and fails if it cannot.
836 ** Returns true if it loads the page, else false.
837 */
838 static int wiki_ajax_emit_page_attachments2(const char *zPageName,
839 int latestOnly,
840 int nullIfEmpty){
841 Manifest * pWiki = 0;
842 if( !wiki_fetch_by_name(zPageName, 0, 0, &pWiki) ){
843 ajax_route_error(404, "Wiki page could not be loaded: %s",
844 zPageName);
845 return 0;
846 }
847 wiki_ajax_emit_page_attachments(pWiki, latestOnly, nullIfEmpty);
848 manifest_destroy(pWiki);
849 return 1;
850 }
851
852
853 /*
854 ** Loads the given wiki page, sets the response type to
855 ** application/json, and emits it as a JSON object. If zPageName is a
856 ** sandbox page then a "fake" object is emitted, as the wikiajax API
@@ -778,10 +866,11 @@
866 ** mimetype: "mimetype",
867 ** version: UUID string or null for a sandbox page,
868 ** parent: "parent uuid" or null if no parent,
869 ** isDeleted: true if the page has no content (is "deleted")
870 ** else not set (making it "falsy" in JS),
871 ** attachments: see wiki_ajax_emit_page_attachments()
872 ** content: "page content" (only if includeContent is true)
873 ** }
874 **
875 ** If includeContent is false then the content member is elided.
876 */
@@ -828,10 +917,12 @@
917 CX(", \"isEmpty\": true");
918 }
919 if(includeContent){
920 CX(", \"content\": %!j", pWiki->zWiki);
921 }
922 CX(", \"attachments\": ");
923 wiki_ajax_emit_page_attachments(pWiki, 0, 1);
924 CX("}");
925 fossil_free(zUuid);
926 manifest_destroy(pWiki);
927 return 2;
928 }
@@ -919,10 +1010,39 @@
1010 return;
1011 }
1012 cgi_set_content_type("application/json");
1013 wiki_ajax_emit_page_object(zPageName, 1);
1014 }
1015
1016 /*
1017 ** Ajax route handler for /wikiajax/attachments.
1018 **
1019 ** URL params:
1020 **
1021 ** page = the wiki page name
1022 ** latestOnly = if set, only latest version of each attachment
1023 ** is emitted.
1024 **
1025 ** Responds with JSON: see wiki_ajax_emit_page_attachments()
1026 **
1027 ** If there are no attachments it emits an empty array instead of null
1028 ** so that the output can be used as a top-level JSON response.
1029 **
1030 ** On error, an object in the form documented by
1031 ** ajax_route_error(). On success, an object in the form documented
1032 ** for wiki_ajax_emit_page_object().
1033 */
1034 static void wiki_ajax_route_attachments(void){
1035 const char * zPageName = P("page");
1036 const int fLatestOnly = P("latestOnly")!=0;
1037 if( zPageName==0 || zPageName[0]==0 ){
1038 ajax_route_error(400,"Missing page name.");
1039 return;
1040 }
1041 cgi_set_content_type("application/json");
1042 wiki_ajax_emit_page_attachments2(zPageName, fLatestOnly, 0);
1043 }
1044
1045 /*
1046 ** Ajax route handler for /wikiajax/diff.
1047 **
1048 ** URL params:
@@ -1076,10 +1196,11 @@
1196 const char * zName = P("name");
1197 AjaxRoute routeName = {0,0,0,0};
1198 const AjaxRoute * pRoute = 0;
1199 const AjaxRoute routes[] = {
1200 /* Keep these sorted by zName (for bsearch()) */
1201 {"attachments", wiki_ajax_route_attachments, 0, 0},
1202 {"diff", wiki_ajax_route_diff, 1, 1},
1203 {"fetch", wiki_ajax_route_fetch, 0, 0},
1204 {"list", wiki_ajax_route_list, 0, 0},
1205 {"preview", wiki_ajax_route_preview, 0, 1},
1206 {"save", wiki_ajax_route_save, 1, 1}
@@ -1302,13 +1423,17 @@
1423
1424 /****** The obligatory "Misc" tab ******/
1425 {
1426 CX("<div id='wikiedit-tab-misc' "
1427 "data-tab-parent='wikiedit-tabs' "
1428 "data-tab-label='Misc.' "
1429 "class='hidden'"
1430 ">");
1431 CX("<fieldset id='attachment-wrapper'>");
1432 CX("<legend>Attachments</legend>");
1433 CX("<div>No attachments for the current page.</div>");
1434 CX("</fieldset>");
1435 CX("<h2>Wiki formatting rules</h2>");
1436 CX("<ul>");
1437 CX("<li><a href='%R/wiki_rules'>Fossil wiki format</a></li>");
1438 CX("<li><a href='%R/md_rules'>Markdown format</a></li>");
1439 CX("<li>Plain-text pages use no special formatting.</li>");
1440

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button