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.
Commit
ce15e35e4729ffad3dc993a03fe8c9d340d11c613aabe26b2223aa8841412d95
Parent
bee7418f07591f5…
5 files changed
+4
+2
-2
+117
-17
+127
-1
+127
-1
+4
| --- src/default.css | ||
| +++ src/default.css | ||
| @@ -1795,10 +1795,14 @@ | ||
| 1795 | 1795 | display: none; |
| 1796 | 1796 | } |
| 1797 | 1797 | body.branch .submenu > a.timeline-link.selected { |
| 1798 | 1798 | display: inline; |
| 1799 | 1799 | } |
| 1800 | + | |
| 1801 | +.monospace { | |
| 1802 | + font-family: monospace; | |
| 1803 | +} | |
| 1800 | 1804 | |
| 1801 | 1805 | /* Objects in the "desktoponly" class are invisible on mobile */ |
| 1802 | 1806 | @media screen and (max-width: 600px) { |
| 1803 | 1807 | .desktoponly { |
| 1804 | 1808 | display: none; |
| 1805 | 1809 |
| --- 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 |
+2
-2
| --- src/fossil.copybutton.js | ||
| +++ src/fossil.copybutton.js | ||
| @@ -21,11 +21,11 @@ | ||
| 21 | 21 | |
| 22 | 22 | One of copyFromElement or copyFromId must be provided, but copyFromId |
| 23 | 23 | may optionally be provided via e.dataset.copyFromId. |
| 24 | 24 | |
| 25 | 25 | .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 | |
| 27 | 27 | clipboard. The default is to extract it from the copy-from |
| 28 | 28 | element, using its [value] member, if it has one, else its |
| 29 | 29 | [innerText]. A client-provided callback may use any data source |
| 30 | 30 | it likes, so long as it's synchronous. If this function returns a |
| 31 | 31 | falsy value then the clipboard is not modified. This function is |
| @@ -81,11 +81,11 @@ | ||
| 81 | 81 | }); |
| 82 | 82 | */ |
| 83 | 83 | F.copyButton = function f(e, opt){ |
| 84 | 84 | if('string'===typeof e){ |
| 85 | 85 | e = document.querySelector(e); |
| 86 | - } | |
| 86 | + } | |
| 87 | 87 | opt = F.mergeLastWins(f.defaultOptions, opt); |
| 88 | 88 | if(opt.cssClass){ |
| 89 | 89 | D.addClass(e, opt.cssClass); |
| 90 | 90 | } |
| 91 | 91 | var srcId, srcElem; |
| 92 | 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. 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 |
+117
-17
| --- src/fossil.page.wikiedit.js | ||
| +++ src/fossil.page.wikiedit.js | ||
| @@ -182,10 +182,11 @@ | ||
| 182 | 182 | record.type = winfo.type; |
| 183 | 183 | record.parent = winfo.parent; |
| 184 | 184 | record.version = winfo.version; |
| 185 | 185 | record.stashTime = new Date().getTime(); |
| 186 | 186 | record.isEmpty = !!winfo.isEmpty; |
| 187 | + record.attachments = winfo.attachments; | |
| 187 | 188 | this.storeIndex(); |
| 188 | 189 | if(arguments.length>1){ |
| 189 | 190 | if(content) delete record.isEmpty; |
| 190 | 191 | F.storage.set(this.contentKey(key), content); |
| 191 | 192 | } |
| @@ -526,11 +527,11 @@ | ||
| 526 | 527 | else if(0===name.indexOf('branch/')) wtype = 'branch'; |
| 527 | 528 | else if(0===name.indexOf('tag/')) wtype = 'tag'; |
| 528 | 529 | /* ^^^ note that we're not validating that, e.g., checkin/XYZ |
| 529 | 530 | has a full artifact ID after "checkin/". */ |
| 530 | 531 | const winfo = { |
| 531 | - name: name, type: wtype, mimetype: 'text/x-fossil-wiki', | |
| 532 | + name: name, type: wtype, mimetype: 'text/x-markdown', | |
| 532 | 533 | version: null, parent: null |
| 533 | 534 | }; |
| 534 | 535 | this.cache.pageList.push( |
| 535 | 536 | winfo/*keeps entry from getting lost from the list on save*/ |
| 536 | 537 | ); |
| @@ -818,11 +819,12 @@ | ||
| 818 | 819 | if(!ajaxState.toDisable){ |
| 819 | 820 | ajaxState.toDisable = document.querySelectorAll( |
| 820 | 821 | ['button:not([disabled])', |
| 821 | 822 | 'input:not([disabled])', |
| 822 | 823 | 'select:not([disabled])', |
| 823 | - 'textarea:not([disabled])' | |
| 824 | + 'textarea:not([disabled])', | |
| 825 | + 'fieldset:not([disabled])' | |
| 824 | 826 | ].join(',') |
| 825 | 827 | ); |
| 826 | 828 | } |
| 827 | 829 | if(1===++ajaxState.count){ |
| 828 | 830 | D.addClass(document.body, 'waiting'); |
| @@ -853,10 +855,11 @@ | ||
| 853 | 855 | cbAutoPreview: E('#cb-preview-autorefresh'), |
| 854 | 856 | previewTarget: E('#wikiedit-tab-preview-wrapper'), |
| 855 | 857 | diffTarget: E('#wikiedit-tab-diff-wrapper'), |
| 856 | 858 | editStatus: E('#wikiedit-edit-status'), |
| 857 | 859 | tabContainer: E('#wikiedit-tabs'), |
| 860 | + attachmentContainer: E("#attachment-wrapper"), | |
| 858 | 861 | tabs:{ |
| 859 | 862 | pageList: E('#wikiedit-tab-pages'), |
| 860 | 863 | content: E('#wikiedit-tab-content'), |
| 861 | 864 | preview: E('#wikiedit-tab-preview'), |
| 862 | 865 | diff: E('#wikiedit-tab-diff'), |
| @@ -1122,10 +1125,105 @@ | ||
| 1122 | 1125 | */ |
| 1123 | 1126 | const affirmPageLoaded = function(quiet){ |
| 1124 | 1127 | if(!P.winfo && !quiet) F.error("No wiki page is loaded."); |
| 1125 | 1128 | return !!P.winfo; |
| 1126 | 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 | + }; | |
| 1127 | 1225 | |
| 1128 | 1226 | /** Updates the in-tab title/edit status information */ |
| 1129 | 1227 | P.updateEditStatus = function f(){ |
| 1130 | 1228 | if(!f.eLinks){ |
| 1131 | 1229 | f.eName = P.e.editStatus.querySelector('span.name'); |
| @@ -1133,24 +1231,25 @@ | ||
| 1133 | 1231 | } |
| 1134 | 1232 | const wi = this.winfo; |
| 1135 | 1233 | D.clearElement(f.eName, f.eLinks); |
| 1136 | 1234 | if(!wi){ |
| 1137 | 1235 | 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; | |
| 1152 | 1251 | }; |
| 1153 | 1252 | |
| 1154 | 1253 | /** |
| 1155 | 1254 | Update the page title and header based on the state of |
| 1156 | 1255 | this.winfo. A no-op if this.winfo is not set. Returns this. |
| @@ -1313,11 +1412,12 @@ | ||
| 1313 | 1412 | mimetype: stashWinfo.mimetype, |
| 1314 | 1413 | type: stashWinfo.type, |
| 1315 | 1414 | version: stashWinfo.version, |
| 1316 | 1415 | parent: stashWinfo.parent, |
| 1317 | 1416 | isEmpty: !!stashWinfo.isEmpty, |
| 1318 | - content: $stash.stashedContent(stashWinfo) | |
| 1417 | + content: $stash.stashedContent(stashWinfo), | |
| 1418 | + attachments: stashWinfo.attachments | |
| 1319 | 1419 | }); |
| 1320 | 1420 | this._isDirty = true/*b/c loading normally clears that flag*/; |
| 1321 | 1421 | return this; |
| 1322 | 1422 | } |
| 1323 | 1423 | F.message( |
| 1324 | 1424 |
| --- 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 @@ | ||
| 759 | 759 | } |
| 760 | 760 | } |
| 761 | 761 | ajax_route_error(403, "%s", zErr); |
| 762 | 762 | return 0; |
| 763 | 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 | + | |
| 764 | 853 | |
| 765 | 854 | /* |
| 766 | 855 | ** Loads the given wiki page, sets the response type to |
| 767 | 856 | ** application/json, and emits it as a JSON object. If zPageName is a |
| 768 | 857 | ** sandbox page then a "fake" object is emitted, as the wikiajax API |
| @@ -778,10 +867,11 @@ | ||
| 778 | 867 | ** mimetype: "mimetype", |
| 779 | 868 | ** version: UUID string or null for a sandbox page, |
| 780 | 869 | ** parent: "parent uuid" or null if no parent, |
| 781 | 870 | ** isDeleted: true if the page has no content (is "deleted") |
| 782 | 871 | ** else not set (making it "falsy" in JS), |
| 872 | +** attachments: see wiki_ajax_emit_page_attachments(), | |
| 783 | 873 | ** content: "page content" (only if includeContent is true) |
| 784 | 874 | ** } |
| 785 | 875 | ** |
| 786 | 876 | ** If includeContent is false then the content member is elided. |
| 787 | 877 | */ |
| @@ -828,10 +918,12 @@ | ||
| 828 | 918 | CX(", \"isEmpty\": true"); |
| 829 | 919 | } |
| 830 | 920 | if(includeContent){ |
| 831 | 921 | CX(", \"content\": %!j", pWiki->zWiki); |
| 832 | 922 | } |
| 923 | + CX(", \"attachments\": "); | |
| 924 | + wiki_ajax_emit_page_attachments(pWiki, 0, 1); | |
| 833 | 925 | CX("}"); |
| 834 | 926 | fossil_free(zUuid); |
| 835 | 927 | manifest_destroy(pWiki); |
| 836 | 928 | return 2; |
| 837 | 929 | } |
| @@ -919,10 +1011,39 @@ | ||
| 919 | 1011 | return; |
| 920 | 1012 | } |
| 921 | 1013 | cgi_set_content_type("application/json"); |
| 922 | 1014 | wiki_ajax_emit_page_object(zPageName, 1); |
| 923 | 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 | +} | |
| 924 | 1045 | |
| 925 | 1046 | /* |
| 926 | 1047 | ** Ajax route handler for /wikiajax/diff. |
| 927 | 1048 | ** |
| 928 | 1049 | ** URL params: |
| @@ -1076,10 +1197,11 @@ | ||
| 1076 | 1197 | const char * zName = P("name"); |
| 1077 | 1198 | AjaxRoute routeName = {0,0,0,0}; |
| 1078 | 1199 | const AjaxRoute * pRoute = 0; |
| 1079 | 1200 | const AjaxRoute routes[] = { |
| 1080 | 1201 | /* Keep these sorted by zName (for bsearch()) */ |
| 1202 | + {"attachments", wiki_ajax_route_attachments, 0, 0}, | |
| 1081 | 1203 | {"diff", wiki_ajax_route_diff, 1, 1}, |
| 1082 | 1204 | {"fetch", wiki_ajax_route_fetch, 0, 0}, |
| 1083 | 1205 | {"list", wiki_ajax_route_list, 0, 0}, |
| 1084 | 1206 | {"preview", wiki_ajax_route_preview, 0, 1}, |
| 1085 | 1207 | {"save", wiki_ajax_route_save, 1, 1} |
| @@ -1302,13 +1424,17 @@ | ||
| 1302 | 1424 | |
| 1303 | 1425 | /****** The obligatory "Misc" tab ******/ |
| 1304 | 1426 | { |
| 1305 | 1427 | CX("<div id='wikiedit-tab-misc' " |
| 1306 | 1428 | "data-tab-parent='wikiedit-tabs' " |
| 1307 | - "data-tab-label='Help' " | |
| 1429 | + "data-tab-label='Misc.' " | |
| 1308 | 1430 | "class='hidden'" |
| 1309 | 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>"); | |
| 1310 | 1436 | CX("<h2>Wiki formatting rules</h2>"); |
| 1311 | 1437 | CX("<ul>"); |
| 1312 | 1438 | CX("<li><a href='%R/wiki_rules'>Fossil wiki format</a></li>"); |
| 1313 | 1439 | CX("<li><a href='%R/md_rules'>Markdown format</a></li>"); |
| 1314 | 1440 | CX("<li>Plain-text pages use no special formatting.</li>"); |
| 1315 | 1441 |
| --- 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 @@ | ||
| 759 | 759 | } |
| 760 | 760 | } |
| 761 | 761 | ajax_route_error(403, "%s", zErr); |
| 762 | 762 | return 0; |
| 763 | 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 | + | |
| 764 | 853 | |
| 765 | 854 | /* |
| 766 | 855 | ** Loads the given wiki page, sets the response type to |
| 767 | 856 | ** application/json, and emits it as a JSON object. If zPageName is a |
| 768 | 857 | ** sandbox page then a "fake" object is emitted, as the wikiajax API |
| @@ -778,10 +867,11 @@ | ||
| 778 | 867 | ** mimetype: "mimetype", |
| 779 | 868 | ** version: UUID string or null for a sandbox page, |
| 780 | 869 | ** parent: "parent uuid" or null if no parent, |
| 781 | 870 | ** isDeleted: true if the page has no content (is "deleted") |
| 782 | 871 | ** else not set (making it "falsy" in JS), |
| 872 | +** attachments: see wiki_ajax_emit_page_attachments(), | |
| 783 | 873 | ** content: "page content" (only if includeContent is true) |
| 784 | 874 | ** } |
| 785 | 875 | ** |
| 786 | 876 | ** If includeContent is false then the content member is elided. |
| 787 | 877 | */ |
| @@ -828,10 +918,12 @@ | ||
| 828 | 918 | CX(", \"isEmpty\": true"); |
| 829 | 919 | } |
| 830 | 920 | if(includeContent){ |
| 831 | 921 | CX(", \"content\": %!j", pWiki->zWiki); |
| 832 | 922 | } |
| 923 | + CX(", \"attachments\": "); | |
| 924 | + wiki_ajax_emit_page_attachments(pWiki, 0, 1); | |
| 833 | 925 | CX("}"); |
| 834 | 926 | fossil_free(zUuid); |
| 835 | 927 | manifest_destroy(pWiki); |
| 836 | 928 | return 2; |
| 837 | 929 | } |
| @@ -919,10 +1011,39 @@ | ||
| 919 | 1011 | return; |
| 920 | 1012 | } |
| 921 | 1013 | cgi_set_content_type("application/json"); |
| 922 | 1014 | wiki_ajax_emit_page_object(zPageName, 1); |
| 923 | 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 | +} | |
| 924 | 1045 | |
| 925 | 1046 | /* |
| 926 | 1047 | ** Ajax route handler for /wikiajax/diff. |
| 927 | 1048 | ** |
| 928 | 1049 | ** URL params: |
| @@ -1076,10 +1197,11 @@ | ||
| 1076 | 1197 | const char * zName = P("name"); |
| 1077 | 1198 | AjaxRoute routeName = {0,0,0,0}; |
| 1078 | 1199 | const AjaxRoute * pRoute = 0; |
| 1079 | 1200 | const AjaxRoute routes[] = { |
| 1080 | 1201 | /* Keep these sorted by zName (for bsearch()) */ |
| 1202 | + {"attachments", wiki_ajax_route_attachments, 0, 0}, | |
| 1081 | 1203 | {"diff", wiki_ajax_route_diff, 1, 1}, |
| 1082 | 1204 | {"fetch", wiki_ajax_route_fetch, 0, 0}, |
| 1083 | 1205 | {"list", wiki_ajax_route_list, 0, 0}, |
| 1084 | 1206 | {"preview", wiki_ajax_route_preview, 0, 1}, |
| 1085 | 1207 | {"save", wiki_ajax_route_save, 1, 1} |
| @@ -1302,13 +1424,17 @@ | ||
| 1302 | 1424 | |
| 1303 | 1425 | /****** The obligatory "Misc" tab ******/ |
| 1304 | 1426 | { |
| 1305 | 1427 | CX("<div id='wikiedit-tab-misc' " |
| 1306 | 1428 | "data-tab-parent='wikiedit-tabs' " |
| 1307 | - "data-tab-label='Help' " | |
| 1429 | + "data-tab-label='Misc.' " | |
| 1308 | 1430 | "class='hidden'" |
| 1309 | 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>"); | |
| 1310 | 1436 | CX("<h2>Wiki formatting rules</h2>"); |
| 1311 | 1437 | CX("<ul>"); |
| 1312 | 1438 | CX("<li><a href='%R/wiki_rules'>Fossil wiki format</a></li>"); |
| 1313 | 1439 | CX("<li><a href='%R/md_rules'>Markdown format</a></li>"); |
| 1314 | 1440 | CX("<li>Plain-text pages use no special formatting.</li>"); |
| 1315 | 1441 |
| --- 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 |