Fossil SCM

/wikiedit: show the list of attachments for the current page and list URLs suitable for pasting them into the page, e.g. for use in IMG tags.

stephan 2021-07-18 12:47 trunk merge
Commit ce15e35e4729ffad3dc993a03fe8c9d340d11c613aabe26b2223aa8841412d95
--- 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
}
@@ -526,11 +527,11 @@
526527
else if(0===name.indexOf('branch/')) wtype = 'branch';
527528
else if(0===name.indexOf('tag/')) wtype = 'tag';
528529
/* ^^^ note that we're not validating that, e.g., checkin/XYZ
529530
has a full artifact ID after "checkin/". */
530531
const winfo = {
531
- name: name, type: wtype, mimetype: 'text/x-fossil-wiki',
532
+ name: name, type: wtype, mimetype: 'text/x-markdown',
532533
version: null, parent: null
533534
};
534535
this.cache.pageList.push(
535536
winfo/*keeps entry from getting lost from the list on save*/
536537
);
@@ -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,105 @@
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 this;
1143
+ }
1144
+ else if(!wi.version){
1145
+ D.append(f.eAttach,
1146
+ "Page ["+wi.name+"] cannot have ",
1147
+ "attachments until it is saved once.");
1148
+ return this;
1149
+ }
1150
+ const btnReload = D.button("Reload list");
1151
+ const self = this;
1152
+ btnReload.addEventListener('click', function(){
1153
+ const isStashed = $stash.hasStashedContent(wi);
1154
+ F.fetch('wikiajax/attachments',{
1155
+ responseType: 'json',
1156
+ urlParams: {page: wi.name},
1157
+ onload: function(r){
1158
+ wi.attachments = r;
1159
+ if(isStashed) self.stashContentChange(true);
1160
+ F.message("Reloaded attachment list for ["+wi.name+"].");
1161
+ self.updateAttachmentsView();
1162
+ }
1163
+ });
1164
+ });
1165
+ if(!wi.attachments || !wi.attachments.length){
1166
+ D.append(f.eAttach,
1167
+ btnReload,
1168
+ " No attachments found for page ["+wi.name+"]. ",
1169
+ D.a(F.repoUrl('attachadd',{
1170
+ page: wi.name,
1171
+ from: F.repoUrl('wikiedit',{name: wi.name})}),
1172
+ "Add attachments..." )
1173
+ );
1174
+ return this;
1175
+ }
1176
+ D.append(
1177
+ f.eAttach,
1178
+ D.append(D.p(),
1179
+ btnReload," ",
1180
+ D.a(F.repoUrl('attachlist',{page:wi.name}),
1181
+ "Attachments for page ["+wi.name+"]."),
1182
+ " ",
1183
+ D.a(F.repoUrl('attachadd',{
1184
+ page:wi.name,
1185
+ from: F.repoUrl('wikiedit',{name: wi.name})}),
1186
+ "Add attachments..." )
1187
+ )
1188
+ );
1189
+ wi.attachments.forEach(function(a){
1190
+ const wrap = D.div();
1191
+ D.append(f.eAttach, wrap);
1192
+ D.append(wrap,
1193
+ D.append(D.div(),
1194
+ "Attachment ",
1195
+ D.addClass(
1196
+ D.a(F.repoUrl('ainfo',{name:a.uuid}),
1197
+ F.hashDigits(a.uuid,true)),
1198
+ 'monospace'),
1199
+ " ",
1200
+ a.filename,
1201
+ (a.isLatest ? " (latest)" : "")
1202
+ )
1203
+ );
1204
+ //D.append(wrap,D.append(D.div(), "URLs:"));
1205
+ const ul = D.ul();
1206
+ D.append(wrap, ul);
1207
+ [ // List download URL variants for each attachment:
1208
+ [
1209
+ "attachdownload?page=",
1210
+ encodeURIComponent(wi.name),
1211
+ "&file=",
1212
+ encodeURIComponent(a.filename)
1213
+ ].join(''),
1214
+ "raw/"+a.src
1215
+ ].forEach(function(url){
1216
+ const imgUrl = D.append(D.addClass(D.span(), 'monospace'), url);
1217
+ const urlCopy = D.span();
1218
+ const li = D.li(ul);
1219
+ D.append(li, urlCopy, " ", imgUrl);
1220
+ F.copyButton(urlCopy, {copyFromElement: imgUrl});
1221
+ });
1222
+ });
1223
+ return this;
1224
+ };
11271225
11281226
/** Updates the in-tab title/edit status information */
11291227
P.updateEditStatus = function f(){
11301228
if(!f.eLinks){
11311229
f.eName = P.e.editStatus.querySelector('span.name');
@@ -1133,24 +1231,25 @@
11331231
}
11341232
const wi = this.winfo;
11351233
D.clearElement(f.eName, f.eLinks);
11361234
if(!wi){
11371235
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
- }
1236
+ this.updateAttachmentsView();
1237
+ return this;
1238
+ }
1239
+ D.append(f.eName,getEditMarker(wi, false),wi.name);
1240
+ this.updateAttachmentsView();
1241
+ if(!wi.version) return this;
1242
+ D.append(
1243
+ f.eLinks,
1244
+ D.a(F.repoUrl('wiki',{name:wi.name}),"viewer"),
1245
+ D.a(F.repoUrl('whistory',{name:wi.name}),'history'),
1246
+ D.a(F.repoUrl('attachlist',{page:wi.name}),"attachments"),
1247
+ D.a(F.repoUrl('attachadd',{page:wi.name,from: F.repoUrl('wikiedit',{name: wi.name})}), "attach"),
1248
+ D.a(F.repoUrl('wikiedit',{name:wi.name}),"editor permalink")
1249
+ );
1250
+ return this;
11521251
};
11531252
11541253
/**
11551254
Update the page title and header based on the state of
11561255
this.winfo. A no-op if this.winfo is not set. Returns this.
@@ -1313,11 +1412,12 @@
13131412
mimetype: stashWinfo.mimetype,
13141413
type: stashWinfo.type,
13151414
version: stashWinfo.version,
13161415
parent: stashWinfo.parent,
13171416
isEmpty: !!stashWinfo.isEmpty,
1318
- content: $stash.stashedContent(stashWinfo)
1417
+ content: $stash.stashedContent(stashWinfo),
1418
+ attachments: stashWinfo.attachments
13191419
});
13201420
this._isDirty = true/*b/c loading normally clears that flag*/;
13211421
return this;
13221422
}
13231423
F.message(
13241424
--- 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 }
@@ -526,11 +527,11 @@
526 else if(0===name.indexOf('branch/')) wtype = 'branch';
527 else if(0===name.indexOf('tag/')) wtype = 'tag';
528 /* ^^^ note that we're not validating that, e.g., checkin/XYZ
529 has a full artifact ID after "checkin/". */
530 const winfo = {
531 name: name, type: wtype, mimetype: 'text/x-fossil-wiki',
532 version: null, parent: null
533 };
534 this.cache.pageList.push(
535 winfo/*keeps entry from getting lost from the list on save*/
536 );
@@ -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,105 @@
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 +1231,25 @@
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 +1412,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 }
@@ -526,11 +527,11 @@
527 else if(0===name.indexOf('branch/')) wtype = 'branch';
528 else if(0===name.indexOf('tag/')) wtype = 'tag';
529 /* ^^^ note that we're not validating that, e.g., checkin/XYZ
530 has a full artifact ID after "checkin/". */
531 const winfo = {
532 name: name, type: wtype, mimetype: 'text/x-markdown',
533 version: null, parent: null
534 };
535 this.cache.pageList.push(
536 winfo/*keeps entry from getting lost from the list on save*/
537 );
@@ -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,105 @@
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 this;
1143 }
1144 else if(!wi.version){
1145 D.append(f.eAttach,
1146 "Page ["+wi.name+"] cannot have ",
1147 "attachments until it is saved once.");
1148 return this;
1149 }
1150 const btnReload = D.button("Reload list");
1151 const self = this;
1152 btnReload.addEventListener('click', function(){
1153 const isStashed = $stash.hasStashedContent(wi);
1154 F.fetch('wikiajax/attachments',{
1155 responseType: 'json',
1156 urlParams: {page: wi.name},
1157 onload: function(r){
1158 wi.attachments = r;
1159 if(isStashed) self.stashContentChange(true);
1160 F.message("Reloaded attachment list for ["+wi.name+"].");
1161 self.updateAttachmentsView();
1162 }
1163 });
1164 });
1165 if(!wi.attachments || !wi.attachments.length){
1166 D.append(f.eAttach,
1167 btnReload,
1168 " No attachments found for page ["+wi.name+"]. ",
1169 D.a(F.repoUrl('attachadd',{
1170 page: wi.name,
1171 from: F.repoUrl('wikiedit',{name: wi.name})}),
1172 "Add attachments..." )
1173 );
1174 return this;
1175 }
1176 D.append(
1177 f.eAttach,
1178 D.append(D.p(),
1179 btnReload," ",
1180 D.a(F.repoUrl('attachlist',{page:wi.name}),
1181 "Attachments for page ["+wi.name+"]."),
1182 " ",
1183 D.a(F.repoUrl('attachadd',{
1184 page:wi.name,
1185 from: F.repoUrl('wikiedit',{name: wi.name})}),
1186 "Add attachments..." )
1187 )
1188 );
1189 wi.attachments.forEach(function(a){
1190 const wrap = D.div();
1191 D.append(f.eAttach, wrap);
1192 D.append(wrap,
1193 D.append(D.div(),
1194 "Attachment ",
1195 D.addClass(
1196 D.a(F.repoUrl('ainfo',{name:a.uuid}),
1197 F.hashDigits(a.uuid,true)),
1198 'monospace'),
1199 " ",
1200 a.filename,
1201 (a.isLatest ? " (latest)" : "")
1202 )
1203 );
1204 //D.append(wrap,D.append(D.div(), "URLs:"));
1205 const ul = D.ul();
1206 D.append(wrap, ul);
1207 [ // List download URL variants for each attachment:
1208 [
1209 "attachdownload?page=",
1210 encodeURIComponent(wi.name),
1211 "&file=",
1212 encodeURIComponent(a.filename)
1213 ].join(''),
1214 "raw/"+a.src
1215 ].forEach(function(url){
1216 const imgUrl = D.append(D.addClass(D.span(), 'monospace'), url);
1217 const urlCopy = D.span();
1218 const li = D.li(ul);
1219 D.append(li, urlCopy, " ", imgUrl);
1220 F.copyButton(urlCopy, {copyFromElement: imgUrl});
1221 });
1222 });
1223 return this;
1224 };
1225
1226 /** Updates the in-tab title/edit status information */
1227 P.updateEditStatus = function f(){
1228 if(!f.eLinks){
1229 f.eName = P.e.editStatus.querySelector('span.name');
@@ -1133,24 +1231,25 @@
1231 }
1232 const wi = this.winfo;
1233 D.clearElement(f.eName, f.eLinks);
1234 if(!wi){
1235 D.append(f.eName, '(no page loaded)');
1236 this.updateAttachmentsView();
1237 return this;
1238 }
1239 D.append(f.eName,getEditMarker(wi, false),wi.name);
1240 this.updateAttachmentsView();
1241 if(!wi.version) return this;
1242 D.append(
1243 f.eLinks,
1244 D.a(F.repoUrl('wiki',{name:wi.name}),"viewer"),
1245 D.a(F.repoUrl('whistory',{name:wi.name}),'history'),
1246 D.a(F.repoUrl('attachlist',{page:wi.name}),"attachments"),
1247 D.a(F.repoUrl('attachadd',{page:wi.name,from: F.repoUrl('wikiedit',{name: wi.name})}), "attach"),
1248 D.a(F.repoUrl('wikiedit',{name:wi.name}),"editor permalink")
1249 );
1250 return this;
1251 };
1252
1253 /**
1254 Update the page title and header based on the state of
1255 this.winfo. A no-op if this.winfo is not set. Returns this.
@@ -1313,11 +1412,12 @@
1412 mimetype: stashWinfo.mimetype,
1413 type: stashWinfo.type,
1414 version: stashWinfo.version,
1415 parent: stashWinfo.parent,
1416 isEmpty: !!stashWinfo.isEmpty,
1417 content: $stash.stashedContent(stashWinfo),
1418 attachments: stashWinfo.attachments
1419 });
1420 this._isDirty = true/*b/c loading normally clears that flag*/;
1421 return this;
1422 }
1423 F.message(
1424
+127 -1
--- src/wiki.c
+++ src/wiki.c
@@ -759,10 +759,99 @@
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_attachments() which attempts to load
835
+** the given wiki page artifact. Returns true if it can load the given
836
+** page, else false. If it returns false then it queues up a 404 ajax
837
+** error response.
838
+*/
839
+static int wiki_ajax_emit_page_attachments2(const char *zPageName,
840
+ int latestOnly,
841
+ int nullIfEmpty){
842
+ Manifest * pWiki = 0;
843
+ if( !wiki_fetch_by_name(zPageName, 0, 0, &pWiki) ){
844
+ ajax_route_error(404, "Wiki page could not be loaded: %s",
845
+ zPageName);
846
+ return 0;
847
+ }
848
+ wiki_ajax_emit_page_attachments(pWiki, latestOnly, nullIfEmpty);
849
+ manifest_destroy(pWiki);
850
+ return 1;
851
+}
852
+
764853
765854
/*
766855
** Loads the given wiki page, sets the response type to
767856
** application/json, and emits it as a JSON object. If zPageName is a
768857
** sandbox page then a "fake" object is emitted, as the wikiajax API
@@ -778,10 +867,11 @@
778867
** mimetype: "mimetype",
779868
** version: UUID string or null for a sandbox page,
780869
** parent: "parent uuid" or null if no parent,
781870
** isDeleted: true if the page has no content (is "deleted")
782871
** else not set (making it "falsy" in JS),
872
+** attachments: see wiki_ajax_emit_page_attachments(),
783873
** content: "page content" (only if includeContent is true)
784874
** }
785875
**
786876
** If includeContent is false then the content member is elided.
787877
*/
@@ -828,10 +918,12 @@
828918
CX(", \"isEmpty\": true");
829919
}
830920
if(includeContent){
831921
CX(", \"content\": %!j", pWiki->zWiki);
832922
}
923
+ CX(", \"attachments\": ");
924
+ wiki_ajax_emit_page_attachments(pWiki, 0, 1);
833925
CX("}");
834926
fossil_free(zUuid);
835927
manifest_destroy(pWiki);
836928
return 2;
837929
}
@@ -919,10 +1011,39 @@
9191011
return;
9201012
}
9211013
cgi_set_content_type("application/json");
9221014
wiki_ajax_emit_page_object(zPageName, 1);
9231015
}
1016
+
1017
+/*
1018
+** Ajax route handler for /wikiajax/attachments.
1019
+**
1020
+** URL params:
1021
+**
1022
+** page = the wiki page name
1023
+** latestOnly = if set, only latest version of each attachment
1024
+** is emitted.
1025
+**
1026
+** Responds with JSON: see wiki_ajax_emit_page_attachments()
1027
+**
1028
+** If there are no attachments it emits an empty array instead of null
1029
+** so that the output can be used as a top-level JSON response.
1030
+**
1031
+** On error, an object in the form documented by
1032
+** ajax_route_error(). On success, an object in the form documented
1033
+** for wiki_ajax_emit_page_attachments().
1034
+*/
1035
+static void wiki_ajax_route_attachments(void){
1036
+ const char * zPageName = P("page");
1037
+ const int fLatestOnly = P("latestOnly")!=0;
1038
+ if( zPageName==0 || zPageName[0]==0 ){
1039
+ ajax_route_error(400,"Missing page name.");
1040
+ return;
1041
+ }
1042
+ cgi_set_content_type("application/json");
1043
+ wiki_ajax_emit_page_attachments2(zPageName, fLatestOnly, 0);
1044
+}
9241045
9251046
/*
9261047
** Ajax route handler for /wikiajax/diff.
9271048
**
9281049
** URL params:
@@ -1076,10 +1197,11 @@
10761197
const char * zName = P("name");
10771198
AjaxRoute routeName = {0,0,0,0};
10781199
const AjaxRoute * pRoute = 0;
10791200
const AjaxRoute routes[] = {
10801201
/* Keep these sorted by zName (for bsearch()) */
1202
+ {"attachments", wiki_ajax_route_attachments, 0, 0},
10811203
{"diff", wiki_ajax_route_diff, 1, 1},
10821204
{"fetch", wiki_ajax_route_fetch, 0, 0},
10831205
{"list", wiki_ajax_route_list, 0, 0},
10841206
{"preview", wiki_ajax_route_preview, 0, 1},
10851207
{"save", wiki_ajax_route_save, 1, 1}
@@ -1302,13 +1424,17 @@
13021424
13031425
/****** The obligatory "Misc" tab ******/
13041426
{
13051427
CX("<div id='wikiedit-tab-misc' "
13061428
"data-tab-parent='wikiedit-tabs' "
1307
- "data-tab-label='Help' "
1429
+ "data-tab-label='Misc.' "
13081430
"class='hidden'"
13091431
">");
1432
+ CX("<fieldset id='attachment-wrapper'>");
1433
+ CX("<legend>Attachments</legend>");
1434
+ CX("<div>No attachments for the current page.</div>");
1435
+ CX("</fieldset>");
13101436
CX("<h2>Wiki formatting rules</h2>");
13111437
CX("<ul>");
13121438
CX("<li><a href='%R/wiki_rules'>Fossil wiki format</a></li>");
13131439
CX("<li><a href='%R/md_rules'>Markdown format</a></li>");
13141440
CX("<li>Plain-text pages use no special formatting.</li>");
13151441
--- src/wiki.c
+++ src/wiki.c
@@ -759,10 +759,99 @@
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 +867,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 +918,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 +1011,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 +1197,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 +1424,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,99 @@
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_attachments() which attempts to load
835 ** the given wiki page artifact. Returns true if it can load the given
836 ** page, else false. If it returns false then it queues up a 404 ajax
837 ** error response.
838 */
839 static int wiki_ajax_emit_page_attachments2(const char *zPageName,
840 int latestOnly,
841 int nullIfEmpty){
842 Manifest * pWiki = 0;
843 if( !wiki_fetch_by_name(zPageName, 0, 0, &pWiki) ){
844 ajax_route_error(404, "Wiki page could not be loaded: %s",
845 zPageName);
846 return 0;
847 }
848 wiki_ajax_emit_page_attachments(pWiki, latestOnly, nullIfEmpty);
849 manifest_destroy(pWiki);
850 return 1;
851 }
852
853
854 /*
855 ** Loads the given wiki page, sets the response type to
856 ** application/json, and emits it as a JSON object. If zPageName is a
857 ** sandbox page then a "fake" object is emitted, as the wikiajax API
@@ -778,10 +867,11 @@
867 ** mimetype: "mimetype",
868 ** version: UUID string or null for a sandbox page,
869 ** parent: "parent uuid" or null if no parent,
870 ** isDeleted: true if the page has no content (is "deleted")
871 ** else not set (making it "falsy" in JS),
872 ** attachments: see wiki_ajax_emit_page_attachments(),
873 ** content: "page content" (only if includeContent is true)
874 ** }
875 **
876 ** If includeContent is false then the content member is elided.
877 */
@@ -828,10 +918,12 @@
918 CX(", \"isEmpty\": true");
919 }
920 if(includeContent){
921 CX(", \"content\": %!j", pWiki->zWiki);
922 }
923 CX(", \"attachments\": ");
924 wiki_ajax_emit_page_attachments(pWiki, 0, 1);
925 CX("}");
926 fossil_free(zUuid);
927 manifest_destroy(pWiki);
928 return 2;
929 }
@@ -919,10 +1011,39 @@
1011 return;
1012 }
1013 cgi_set_content_type("application/json");
1014 wiki_ajax_emit_page_object(zPageName, 1);
1015 }
1016
1017 /*
1018 ** Ajax route handler for /wikiajax/attachments.
1019 **
1020 ** URL params:
1021 **
1022 ** page = the wiki page name
1023 ** latestOnly = if set, only latest version of each attachment
1024 ** is emitted.
1025 **
1026 ** Responds with JSON: see wiki_ajax_emit_page_attachments()
1027 **
1028 ** If there are no attachments it emits an empty array instead of null
1029 ** so that the output can be used as a top-level JSON response.
1030 **
1031 ** On error, an object in the form documented by
1032 ** ajax_route_error(). On success, an object in the form documented
1033 ** for wiki_ajax_emit_page_attachments().
1034 */
1035 static void wiki_ajax_route_attachments(void){
1036 const char * zPageName = P("page");
1037 const int fLatestOnly = P("latestOnly")!=0;
1038 if( zPageName==0 || zPageName[0]==0 ){
1039 ajax_route_error(400,"Missing page name.");
1040 return;
1041 }
1042 cgi_set_content_type("application/json");
1043 wiki_ajax_emit_page_attachments2(zPageName, fLatestOnly, 0);
1044 }
1045
1046 /*
1047 ** Ajax route handler for /wikiajax/diff.
1048 **
1049 ** URL params:
@@ -1076,10 +1197,11 @@
1197 const char * zName = P("name");
1198 AjaxRoute routeName = {0,0,0,0};
1199 const AjaxRoute * pRoute = 0;
1200 const AjaxRoute routes[] = {
1201 /* Keep these sorted by zName (for bsearch()) */
1202 {"attachments", wiki_ajax_route_attachments, 0, 0},
1203 {"diff", wiki_ajax_route_diff, 1, 1},
1204 {"fetch", wiki_ajax_route_fetch, 0, 0},
1205 {"list", wiki_ajax_route_list, 0, 0},
1206 {"preview", wiki_ajax_route_preview, 0, 1},
1207 {"save", wiki_ajax_route_save, 1, 1}
@@ -1302,13 +1424,17 @@
1424
1425 /****** The obligatory "Misc" tab ******/
1426 {
1427 CX("<div id='wikiedit-tab-misc' "
1428 "data-tab-parent='wikiedit-tabs' "
1429 "data-tab-label='Misc.' "
1430 "class='hidden'"
1431 ">");
1432 CX("<fieldset id='attachment-wrapper'>");
1433 CX("<legend>Attachments</legend>");
1434 CX("<div>No attachments for the current page.</div>");
1435 CX("</fieldset>");
1436 CX("<h2>Wiki formatting rules</h2>");
1437 CX("<ul>");
1438 CX("<li><a href='%R/wiki_rules'>Fossil wiki format</a></li>");
1439 CX("<li><a href='%R/md_rules'>Markdown format</a></li>");
1440 CX("<li>Plain-text pages use no special formatting.</li>");
1441
+127 -1
--- src/wiki.c
+++ src/wiki.c
@@ -759,10 +759,99 @@
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_attachments() which attempts to load
835
+** the given wiki page artifact. Returns true if it can load the given
836
+** page, else false. If it returns false then it queues up a 404 ajax
837
+** error response.
838
+*/
839
+static int wiki_ajax_emit_page_attachments2(const char *zPageName,
840
+ int latestOnly,
841
+ int nullIfEmpty){
842
+ Manifest * pWiki = 0;
843
+ if( !wiki_fetch_by_name(zPageName, 0, 0, &pWiki) ){
844
+ ajax_route_error(404, "Wiki page could not be loaded: %s",
845
+ zPageName);
846
+ return 0;
847
+ }
848
+ wiki_ajax_emit_page_attachments(pWiki, latestOnly, nullIfEmpty);
849
+ manifest_destroy(pWiki);
850
+ return 1;
851
+}
852
+
764853
765854
/*
766855
** Loads the given wiki page, sets the response type to
767856
** application/json, and emits it as a JSON object. If zPageName is a
768857
** sandbox page then a "fake" object is emitted, as the wikiajax API
@@ -778,10 +867,11 @@
778867
** mimetype: "mimetype",
779868
** version: UUID string or null for a sandbox page,
780869
** parent: "parent uuid" or null if no parent,
781870
** isDeleted: true if the page has no content (is "deleted")
782871
** else not set (making it "falsy" in JS),
872
+** attachments: see wiki_ajax_emit_page_attachments(),
783873
** content: "page content" (only if includeContent is true)
784874
** }
785875
**
786876
** If includeContent is false then the content member is elided.
787877
*/
@@ -828,10 +918,12 @@
828918
CX(", \"isEmpty\": true");
829919
}
830920
if(includeContent){
831921
CX(", \"content\": %!j", pWiki->zWiki);
832922
}
923
+ CX(", \"attachments\": ");
924
+ wiki_ajax_emit_page_attachments(pWiki, 0, 1);
833925
CX("}");
834926
fossil_free(zUuid);
835927
manifest_destroy(pWiki);
836928
return 2;
837929
}
@@ -919,10 +1011,39 @@
9191011
return;
9201012
}
9211013
cgi_set_content_type("application/json");
9221014
wiki_ajax_emit_page_object(zPageName, 1);
9231015
}
1016
+
1017
+/*
1018
+** Ajax route handler for /wikiajax/attachments.
1019
+**
1020
+** URL params:
1021
+**
1022
+** page = the wiki page name
1023
+** latestOnly = if set, only latest version of each attachment
1024
+** is emitted.
1025
+**
1026
+** Responds with JSON: see wiki_ajax_emit_page_attachments()
1027
+**
1028
+** If there are no attachments it emits an empty array instead of null
1029
+** so that the output can be used as a top-level JSON response.
1030
+**
1031
+** On error, an object in the form documented by
1032
+** ajax_route_error(). On success, an object in the form documented
1033
+** for wiki_ajax_emit_page_attachments().
1034
+*/
1035
+static void wiki_ajax_route_attachments(void){
1036
+ const char * zPageName = P("page");
1037
+ const int fLatestOnly = P("latestOnly")!=0;
1038
+ if( zPageName==0 || zPageName[0]==0 ){
1039
+ ajax_route_error(400,"Missing page name.");
1040
+ return;
1041
+ }
1042
+ cgi_set_content_type("application/json");
1043
+ wiki_ajax_emit_page_attachments2(zPageName, fLatestOnly, 0);
1044
+}
9241045
9251046
/*
9261047
** Ajax route handler for /wikiajax/diff.
9271048
**
9281049
** URL params:
@@ -1076,10 +1197,11 @@
10761197
const char * zName = P("name");
10771198
AjaxRoute routeName = {0,0,0,0};
10781199
const AjaxRoute * pRoute = 0;
10791200
const AjaxRoute routes[] = {
10801201
/* Keep these sorted by zName (for bsearch()) */
1202
+ {"attachments", wiki_ajax_route_attachments, 0, 0},
10811203
{"diff", wiki_ajax_route_diff, 1, 1},
10821204
{"fetch", wiki_ajax_route_fetch, 0, 0},
10831205
{"list", wiki_ajax_route_list, 0, 0},
10841206
{"preview", wiki_ajax_route_preview, 0, 1},
10851207
{"save", wiki_ajax_route_save, 1, 1}
@@ -1302,13 +1424,17 @@
13021424
13031425
/****** The obligatory "Misc" tab ******/
13041426
{
13051427
CX("<div id='wikiedit-tab-misc' "
13061428
"data-tab-parent='wikiedit-tabs' "
1307
- "data-tab-label='Help' "
1429
+ "data-tab-label='Misc.' "
13081430
"class='hidden'"
13091431
">");
1432
+ CX("<fieldset id='attachment-wrapper'>");
1433
+ CX("<legend>Attachments</legend>");
1434
+ CX("<div>No attachments for the current page.</div>");
1435
+ CX("</fieldset>");
13101436
CX("<h2>Wiki formatting rules</h2>");
13111437
CX("<ul>");
13121438
CX("<li><a href='%R/wiki_rules'>Fossil wiki format</a></li>");
13131439
CX("<li><a href='%R/md_rules'>Markdown format</a></li>");
13141440
CX("<li>Plain-text pages use no special formatting.</li>");
13151441
--- src/wiki.c
+++ src/wiki.c
@@ -759,10 +759,99 @@
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 +867,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 +918,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 +1011,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 +1197,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 +1424,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,99 @@
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_attachments() which attempts to load
835 ** the given wiki page artifact. Returns true if it can load the given
836 ** page, else false. If it returns false then it queues up a 404 ajax
837 ** error response.
838 */
839 static int wiki_ajax_emit_page_attachments2(const char *zPageName,
840 int latestOnly,
841 int nullIfEmpty){
842 Manifest * pWiki = 0;
843 if( !wiki_fetch_by_name(zPageName, 0, 0, &pWiki) ){
844 ajax_route_error(404, "Wiki page could not be loaded: %s",
845 zPageName);
846 return 0;
847 }
848 wiki_ajax_emit_page_attachments(pWiki, latestOnly, nullIfEmpty);
849 manifest_destroy(pWiki);
850 return 1;
851 }
852
853
854 /*
855 ** Loads the given wiki page, sets the response type to
856 ** application/json, and emits it as a JSON object. If zPageName is a
857 ** sandbox page then a "fake" object is emitted, as the wikiajax API
@@ -778,10 +867,11 @@
867 ** mimetype: "mimetype",
868 ** version: UUID string or null for a sandbox page,
869 ** parent: "parent uuid" or null if no parent,
870 ** isDeleted: true if the page has no content (is "deleted")
871 ** else not set (making it "falsy" in JS),
872 ** attachments: see wiki_ajax_emit_page_attachments(),
873 ** content: "page content" (only if includeContent is true)
874 ** }
875 **
876 ** If includeContent is false then the content member is elided.
877 */
@@ -828,10 +918,12 @@
918 CX(", \"isEmpty\": true");
919 }
920 if(includeContent){
921 CX(", \"content\": %!j", pWiki->zWiki);
922 }
923 CX(", \"attachments\": ");
924 wiki_ajax_emit_page_attachments(pWiki, 0, 1);
925 CX("}");
926 fossil_free(zUuid);
927 manifest_destroy(pWiki);
928 return 2;
929 }
@@ -919,10 +1011,39 @@
1011 return;
1012 }
1013 cgi_set_content_type("application/json");
1014 wiki_ajax_emit_page_object(zPageName, 1);
1015 }
1016
1017 /*
1018 ** Ajax route handler for /wikiajax/attachments.
1019 **
1020 ** URL params:
1021 **
1022 ** page = the wiki page name
1023 ** latestOnly = if set, only latest version of each attachment
1024 ** is emitted.
1025 **
1026 ** Responds with JSON: see wiki_ajax_emit_page_attachments()
1027 **
1028 ** If there are no attachments it emits an empty array instead of null
1029 ** so that the output can be used as a top-level JSON response.
1030 **
1031 ** On error, an object in the form documented by
1032 ** ajax_route_error(). On success, an object in the form documented
1033 ** for wiki_ajax_emit_page_attachments().
1034 */
1035 static void wiki_ajax_route_attachments(void){
1036 const char * zPageName = P("page");
1037 const int fLatestOnly = P("latestOnly")!=0;
1038 if( zPageName==0 || zPageName[0]==0 ){
1039 ajax_route_error(400,"Missing page name.");
1040 return;
1041 }
1042 cgi_set_content_type("application/json");
1043 wiki_ajax_emit_page_attachments2(zPageName, fLatestOnly, 0);
1044 }
1045
1046 /*
1047 ** Ajax route handler for /wikiajax/diff.
1048 **
1049 ** URL params:
@@ -1076,10 +1197,11 @@
1197 const char * zName = P("name");
1198 AjaxRoute routeName = {0,0,0,0};
1199 const AjaxRoute * pRoute = 0;
1200 const AjaxRoute routes[] = {
1201 /* Keep these sorted by zName (for bsearch()) */
1202 {"attachments", wiki_ajax_route_attachments, 0, 0},
1203 {"diff", wiki_ajax_route_diff, 1, 1},
1204 {"fetch", wiki_ajax_route_fetch, 0, 0},
1205 {"list", wiki_ajax_route_list, 0, 0},
1206 {"preview", wiki_ajax_route_preview, 0, 1},
1207 {"save", wiki_ajax_route_save, 1, 1}
@@ -1302,13 +1424,17 @@
1424
1425 /****** The obligatory "Misc" tab ******/
1426 {
1427 CX("<div id='wikiedit-tab-misc' "
1428 "data-tab-parent='wikiedit-tabs' "
1429 "data-tab-label='Misc.' "
1430 "class='hidden'"
1431 ">");
1432 CX("<fieldset id='attachment-wrapper'>");
1433 CX("<legend>Attachments</legend>");
1434 CX("<div>No attachments for the current page.</div>");
1435 CX("</fieldset>");
1436 CX("<h2>Wiki formatting rules</h2>");
1437 CX("<ul>");
1438 CX("<li><a href='%R/wiki_rules'>Fossil wiki format</a></li>");
1439 CX("<li><a href='%R/md_rules'>Markdown format</a></li>");
1440 CX("<li>Plain-text pages use no special formatting.</li>");
1441

Keyboard Shortcuts

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