Fossil SCM

Implemented wiki page saving. Corrected order of the versions in the diff. Various cleanups.

stephan 2020-07-30 22:02 ajax-wiki-editor
Commit 63376a80fcdf3d739c74fa020f11dce800e58762d0d2880889a34c3b11bf5251
2 files changed +104 -40 +160 -70
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -15,11 +15,11 @@
1515
1616
{
1717
name: string,
1818
mimetype: mimetype string,
1919
type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
20
- version: UUID string or null for a sandbox page,
20
+ version: UUID string or null for a sandbox page or new page,
2121
parent: parent UUID string or null if no parent,
2222
content: string
2323
}
2424
2525
The internal docs and code frequently use the term "winfo", and such
@@ -282,17 +282,55 @@
282282
},
283283
/** Removes the given wiki page entry from the page selection
284284
list, if it's in the list. */
285285
removeEntry: function(name){
286286
const sel = this.e.select;
287
- const ndx = sel.selectedIndex;
287
+ var ndx = sel.selectedIndex;
288288
sel.value = name;
289289
if(sel.selectedIndex>-1){
290
+ if(ndx === sel.selectedIndex) ndx = -1;
290291
sel.options.remove(sel.selectedIndex);
291292
}
292293
sel.selectedIndex = ndx;
293294
},
295
+
296
+ /** Loads the page list and populates the selection list. */
297
+ loadList: function callee(){
298
+ if(!callee.sorticase){
299
+ callee.sorticase = function(l,r){
300
+ l = l.toLowerCase();
301
+ r = r.toLowerCase();
302
+ return l<=r ? -1 : 1;
303
+ };
304
+ const self = this;
305
+ callee.onload = function(list){
306
+ /* Jump through some hoops to integrate new/unsaved
307
+ pages into the list of existing pages... We use a map
308
+ as an intermediary in order to filter out any local-stash
309
+ dupes from server-side copies. */
310
+ const map = {}, ndx = $stash.getIndex(), sel = self.e.select;
311
+ D.clearElement(sel);
312
+ list.forEach((name)=>map[name] = true);
313
+ Object.keys(ndx).forEach(function(key){
314
+ const winfo = ndx[key];
315
+ if(!winfo.version/*new page*/) map[winfo.name] = true;
316
+ });
317
+ Object.keys(map)
318
+ .sort(callee.sorticase)
319
+ .forEach((name)=>D.option(sel, name));
320
+ D.enable(sel);
321
+ if(P.winfo) sel.value = P.winfo.name;
322
+ self.refreshStashMarks();
323
+ F.message("Loaded page list.");
324
+ };
325
+ }
326
+ F.fetch('wikiajax/list',{
327
+ responseType: 'json',
328
+ onload: callee.onload
329
+ });
330
+ return this;
331
+ },
294332
/**
295333
Installs a wiki page selection list into the given parent DOM
296334
element and loads the page list from the server.
297335
*/
298336
init: function(parentElem){
@@ -310,45 +348,13 @@
310348
);
311349
D.attr(sel, 'size', 10);
312350
D.option(D.disable(D.clearElement(sel)), "Loading...");
313351
const self = this;
314352
btn.addEventListener(
315
- 'click',
316
- function click(){
317
- if(!click.sorticase){
318
- click.sorticase = function(l,r){
319
- l = l.toLowerCase();
320
- r = r.toLowerCase();
321
- return l<=r ? -1 : 1;
322
- };
323
- }
324
- F.fetch('wikiajax/list',{
325
- responseType: 'json',
326
- onload: function(list){
327
- /* Jump through some hoops to integrate new/unsaved
328
- pages into the list of existing pages... We use a map
329
- as an intermediary in order to filter out any local-stash
330
- dupes from server-side copies. */
331
- const map = {}, ndx = $stash.getIndex();
332
- D.clearElement(sel);
333
- list.forEach((name)=>map[name] = true);
334
- Object.keys(ndx).forEach(function(key){
335
- const winfo = ndx[key];
336
- if(!winfo.version/*new page*/) map[winfo.name] = true;
337
- });
338
- Object.keys(map)
339
- .sort(click.sorticase)
340
- .forEach((name)=>D.option(sel, name));
341
- D.enable(sel);
342
- if(P.winfo) sel.value = P.winfo.name;
343
- self.refreshStashMarks();
344
- }
345
- });
346
- },
347
- false
353
+ 'click', ()=>this.loadList(), false
348354
);
349
- btn.click();
355
+ this.loadList();
350356
sel.addEventListener(
351357
'change',
352358
(e)=>P.loadPage(e.target.value),
353359
false
354360
);
@@ -410,10 +416,11 @@
410416
P.tabs = new fossil.TabManager('#wikiedit-tabs');
411417
P.e = { /* various DOM elements we work with... */
412418
taEditor: E('#wikiedit-content-editor'),
413419
// btnCommit: E("#wikiedit-btn-commit"),
414420
btnReload: E("#wikiedit-tab-content button.wikiedit-content-reload"),
421
+ btnSave: E("#wikiedit-tab-save button.wikiedit-save"),
415422
selectMimetype: E('select[name=mimetype]'),
416423
selectFontSizeWrap: E('#select-font-size'),
417424
// selectDiffWS: E('select[name=diff_ws]'),
418425
cbAutoPreview: E('#cb-preview-autoupdate > input[type=checkbox]'),
419426
previewTarget: E('#wikiedit-tab-preview-wrapper'),
@@ -420,11 +427,12 @@
420427
diffTarget: E('#wikiedit-tab-diff-wrapper'),
421428
tabs:{
422429
pageList: E('#wikiedit-tab-pages'),
423430
content: E('#wikiedit-tab-content'),
424431
preview: E('#wikiedit-tab-preview'),
425
- diff: E('#wikiedit-tab-diff')
432
+ diff: E('#wikiedit-tab-diff'),
433
+ save: E('#wikiedit-tab-save')
426434
//commit: E('#wikiedit-tab-commit')
427435
}
428436
};
429437
430438
P.tabs.e.container.insertBefore(
@@ -448,10 +456,18 @@
448456
the page size again. Weird. Maybe FF-specific. Note that
449457
this weirdness happens even though P.e.diffTarget's parent
450458
is hidden (and therefore P.e.diffTarget is also hidden).
451459
*/
452460
D.removeClass(P.e.diffTarget, 'hidden');
461
+ }else if(ev.detail===P.e.tabs.save){
462
+ const btn = P.e.btnSave;
463
+ if(!P.winfo || !P.getStashedWinfo(P.winfo)){
464
+ D.disable(btn).innerText =
465
+ "There are no changes to save";
466
+ }else{
467
+ D.enable(btn).innerText = "Save changes";
468
+ }
453469
}
454470
}
455471
);
456472
P.tabs.addEventListener(
457473
/* Set up auto-refresh of the preview tab... */
@@ -487,19 +503,21 @@
487503
const w = P.winfo;
488504
if(!w){
489505
F.error("No page loaded.");
490506
return;
491507
}
492
- if(!w.version/* new/unsaved page */ && P.wikiContent()){
508
+ if(!w.version/* new/unsaved page */
509
+ && w.type!=='sandbox'
510
+ && P.wikiContent()){
493511
F.error("This new/unsaved page has content.",
494512
"To really discard this page,",
495513
"first clear its content",
496514
"then use the Discard button.");
497515
return;
498516
}
499517
P.unstashContent()
500
- if(w.version){
518
+ if(w.version || w.type==='sandbox'){
501519
P.loadPage();
502520
}else{
503521
delete P.winfo;
504522
WikiList.removeEntry(w.name);
505523
P.updatePageTitle();
@@ -506,10 +524,23 @@
506524
F.message("Discarded new page ["+w.name+"].");
507525
}
508526
},
509527
ticks: 3
510528
});
529
+ F.confirmer(P.e.btnSave, {
530
+ confirmText: "Really save changes?",
531
+ onconfirm: function(e){
532
+ const w = P.winfo;
533
+ if(!w){
534
+ F.error("No page loaded.");
535
+ return;
536
+ }
537
+ P.save();
538
+ },
539
+ ticks: 3
540
+ });
541
+
511542
P.e.taEditor.addEventListener(
512543
'change', ()=>P.stashContentChange(), false
513544
);
514545
515546
P.selectMimetype(false, true);
@@ -567,11 +598,11 @@
567598
P.previewNeedsUpdate = true;
568599
P.e.selectMimetype.value = winfo.mimetype;
569600
P.tabs.switchToTab(P.e.tabs.content);
570601
P.wikiContent(winfo.content || '');
571602
WikiList.e.select.value = winfo.name;
572
- if(!winfo.version){
603
+ if(!winfo.version && winfo.type!=='sandbox'){
573604
F.error('You are editing a new, unsaved page:',winfo.name);
574605
}
575606
P.updatePageTitle();
576607
},
577608
false
@@ -842,10 +873,43 @@
842873
}
843874
});
844875
return this;
845876
};
846877
878
+ /**
879
+ Saves the current wiki page and re-populates the editor
880
+ with the saved state.
881
+ */
882
+ P.save = function callee(){
883
+ if(!affirmPageLoaded()) return this;
884
+ const self = this;
885
+ const content = this.wikiContent();
886
+ if(!callee.onload){
887
+ callee.onload = function(w){
888
+ const oldWinfo = self.winfo;
889
+ self.unstashContent(oldWinfo);
890
+ self.winfo = w;
891
+ self.updatePageTitle();
892
+ self.dispatchEvent('wiki-page-loaded', w);
893
+ F.message("Saved page: ["+w.name+"].");
894
+ }
895
+ }
896
+ const fd = new FormData(), w = P.winfo;
897
+ fd.append('page',w.name);
898
+ fd.append('mimetype', w.mimetype);
899
+ fd.append('isnew', w.version ? 0 : 1);
900
+ fd.append('content', P.wikiContent());
901
+ F.message(
902
+ "Saving page..."
903
+ ).fetch('wikiajax/save',{
904
+ payload: fd,
905
+ responseType: 'json',
906
+ onload: callee.onload
907
+ });
908
+ return this;
909
+ };
910
+
847911
/**
848912
Updates P.winfo for certain state and stashes P.winfo, with the
849913
current content fetched via P.wikiContent().
850914
851915
If passed truthy AND the stash already has stashed content for
852916
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -15,11 +15,11 @@
15
16 {
17 name: string,
18 mimetype: mimetype string,
19 type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
20 version: UUID string or null for a sandbox page,
21 parent: parent UUID string or null if no parent,
22 content: string
23 }
24
25 The internal docs and code frequently use the term "winfo", and such
@@ -282,17 +282,55 @@
282 },
283 /** Removes the given wiki page entry from the page selection
284 list, if it's in the list. */
285 removeEntry: function(name){
286 const sel = this.e.select;
287 const ndx = sel.selectedIndex;
288 sel.value = name;
289 if(sel.selectedIndex>-1){
 
290 sel.options.remove(sel.selectedIndex);
291 }
292 sel.selectedIndex = ndx;
293 },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294 /**
295 Installs a wiki page selection list into the given parent DOM
296 element and loads the page list from the server.
297 */
298 init: function(parentElem){
@@ -310,45 +348,13 @@
310 );
311 D.attr(sel, 'size', 10);
312 D.option(D.disable(D.clearElement(sel)), "Loading...");
313 const self = this;
314 btn.addEventListener(
315 'click',
316 function click(){
317 if(!click.sorticase){
318 click.sorticase = function(l,r){
319 l = l.toLowerCase();
320 r = r.toLowerCase();
321 return l<=r ? -1 : 1;
322 };
323 }
324 F.fetch('wikiajax/list',{
325 responseType: 'json',
326 onload: function(list){
327 /* Jump through some hoops to integrate new/unsaved
328 pages into the list of existing pages... We use a map
329 as an intermediary in order to filter out any local-stash
330 dupes from server-side copies. */
331 const map = {}, ndx = $stash.getIndex();
332 D.clearElement(sel);
333 list.forEach((name)=>map[name] = true);
334 Object.keys(ndx).forEach(function(key){
335 const winfo = ndx[key];
336 if(!winfo.version/*new page*/) map[winfo.name] = true;
337 });
338 Object.keys(map)
339 .sort(click.sorticase)
340 .forEach((name)=>D.option(sel, name));
341 D.enable(sel);
342 if(P.winfo) sel.value = P.winfo.name;
343 self.refreshStashMarks();
344 }
345 });
346 },
347 false
348 );
349 btn.click();
350 sel.addEventListener(
351 'change',
352 (e)=>P.loadPage(e.target.value),
353 false
354 );
@@ -410,10 +416,11 @@
410 P.tabs = new fossil.TabManager('#wikiedit-tabs');
411 P.e = { /* various DOM elements we work with... */
412 taEditor: E('#wikiedit-content-editor'),
413 // btnCommit: E("#wikiedit-btn-commit"),
414 btnReload: E("#wikiedit-tab-content button.wikiedit-content-reload"),
 
415 selectMimetype: E('select[name=mimetype]'),
416 selectFontSizeWrap: E('#select-font-size'),
417 // selectDiffWS: E('select[name=diff_ws]'),
418 cbAutoPreview: E('#cb-preview-autoupdate > input[type=checkbox]'),
419 previewTarget: E('#wikiedit-tab-preview-wrapper'),
@@ -420,11 +427,12 @@
420 diffTarget: E('#wikiedit-tab-diff-wrapper'),
421 tabs:{
422 pageList: E('#wikiedit-tab-pages'),
423 content: E('#wikiedit-tab-content'),
424 preview: E('#wikiedit-tab-preview'),
425 diff: E('#wikiedit-tab-diff')
 
426 //commit: E('#wikiedit-tab-commit')
427 }
428 };
429
430 P.tabs.e.container.insertBefore(
@@ -448,10 +456,18 @@
448 the page size again. Weird. Maybe FF-specific. Note that
449 this weirdness happens even though P.e.diffTarget's parent
450 is hidden (and therefore P.e.diffTarget is also hidden).
451 */
452 D.removeClass(P.e.diffTarget, 'hidden');
 
 
 
 
 
 
 
 
453 }
454 }
455 );
456 P.tabs.addEventListener(
457 /* Set up auto-refresh of the preview tab... */
@@ -487,19 +503,21 @@
487 const w = P.winfo;
488 if(!w){
489 F.error("No page loaded.");
490 return;
491 }
492 if(!w.version/* new/unsaved page */ && P.wikiContent()){
 
 
493 F.error("This new/unsaved page has content.",
494 "To really discard this page,",
495 "first clear its content",
496 "then use the Discard button.");
497 return;
498 }
499 P.unstashContent()
500 if(w.version){
501 P.loadPage();
502 }else{
503 delete P.winfo;
504 WikiList.removeEntry(w.name);
505 P.updatePageTitle();
@@ -506,10 +524,23 @@
506 F.message("Discarded new page ["+w.name+"].");
507 }
508 },
509 ticks: 3
510 });
 
 
 
 
 
 
 
 
 
 
 
 
 
511 P.e.taEditor.addEventListener(
512 'change', ()=>P.stashContentChange(), false
513 );
514
515 P.selectMimetype(false, true);
@@ -567,11 +598,11 @@
567 P.previewNeedsUpdate = true;
568 P.e.selectMimetype.value = winfo.mimetype;
569 P.tabs.switchToTab(P.e.tabs.content);
570 P.wikiContent(winfo.content || '');
571 WikiList.e.select.value = winfo.name;
572 if(!winfo.version){
573 F.error('You are editing a new, unsaved page:',winfo.name);
574 }
575 P.updatePageTitle();
576 },
577 false
@@ -842,10 +873,43 @@
842 }
843 });
844 return this;
845 };
846
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
847 /**
848 Updates P.winfo for certain state and stashes P.winfo, with the
849 current content fetched via P.wikiContent().
850
851 If passed truthy AND the stash already has stashed content for
852
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -15,11 +15,11 @@
15
16 {
17 name: string,
18 mimetype: mimetype string,
19 type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
20 version: UUID string or null for a sandbox page or new page,
21 parent: parent UUID string or null if no parent,
22 content: string
23 }
24
25 The internal docs and code frequently use the term "winfo", and such
@@ -282,17 +282,55 @@
282 },
283 /** Removes the given wiki page entry from the page selection
284 list, if it's in the list. */
285 removeEntry: function(name){
286 const sel = this.e.select;
287 var ndx = sel.selectedIndex;
288 sel.value = name;
289 if(sel.selectedIndex>-1){
290 if(ndx === sel.selectedIndex) ndx = -1;
291 sel.options.remove(sel.selectedIndex);
292 }
293 sel.selectedIndex = ndx;
294 },
295
296 /** Loads the page list and populates the selection list. */
297 loadList: function callee(){
298 if(!callee.sorticase){
299 callee.sorticase = function(l,r){
300 l = l.toLowerCase();
301 r = r.toLowerCase();
302 return l<=r ? -1 : 1;
303 };
304 const self = this;
305 callee.onload = function(list){
306 /* Jump through some hoops to integrate new/unsaved
307 pages into the list of existing pages... We use a map
308 as an intermediary in order to filter out any local-stash
309 dupes from server-side copies. */
310 const map = {}, ndx = $stash.getIndex(), sel = self.e.select;
311 D.clearElement(sel);
312 list.forEach((name)=>map[name] = true);
313 Object.keys(ndx).forEach(function(key){
314 const winfo = ndx[key];
315 if(!winfo.version/*new page*/) map[winfo.name] = true;
316 });
317 Object.keys(map)
318 .sort(callee.sorticase)
319 .forEach((name)=>D.option(sel, name));
320 D.enable(sel);
321 if(P.winfo) sel.value = P.winfo.name;
322 self.refreshStashMarks();
323 F.message("Loaded page list.");
324 };
325 }
326 F.fetch('wikiajax/list',{
327 responseType: 'json',
328 onload: callee.onload
329 });
330 return this;
331 },
332 /**
333 Installs a wiki page selection list into the given parent DOM
334 element and loads the page list from the server.
335 */
336 init: function(parentElem){
@@ -310,45 +348,13 @@
348 );
349 D.attr(sel, 'size', 10);
350 D.option(D.disable(D.clearElement(sel)), "Loading...");
351 const self = this;
352 btn.addEventListener(
353 'click', ()=>this.loadList(), false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354 );
355 this.loadList();
356 sel.addEventListener(
357 'change',
358 (e)=>P.loadPage(e.target.value),
359 false
360 );
@@ -410,10 +416,11 @@
416 P.tabs = new fossil.TabManager('#wikiedit-tabs');
417 P.e = { /* various DOM elements we work with... */
418 taEditor: E('#wikiedit-content-editor'),
419 // btnCommit: E("#wikiedit-btn-commit"),
420 btnReload: E("#wikiedit-tab-content button.wikiedit-content-reload"),
421 btnSave: E("#wikiedit-tab-save button.wikiedit-save"),
422 selectMimetype: E('select[name=mimetype]'),
423 selectFontSizeWrap: E('#select-font-size'),
424 // selectDiffWS: E('select[name=diff_ws]'),
425 cbAutoPreview: E('#cb-preview-autoupdate > input[type=checkbox]'),
426 previewTarget: E('#wikiedit-tab-preview-wrapper'),
@@ -420,11 +427,12 @@
427 diffTarget: E('#wikiedit-tab-diff-wrapper'),
428 tabs:{
429 pageList: E('#wikiedit-tab-pages'),
430 content: E('#wikiedit-tab-content'),
431 preview: E('#wikiedit-tab-preview'),
432 diff: E('#wikiedit-tab-diff'),
433 save: E('#wikiedit-tab-save')
434 //commit: E('#wikiedit-tab-commit')
435 }
436 };
437
438 P.tabs.e.container.insertBefore(
@@ -448,10 +456,18 @@
456 the page size again. Weird. Maybe FF-specific. Note that
457 this weirdness happens even though P.e.diffTarget's parent
458 is hidden (and therefore P.e.diffTarget is also hidden).
459 */
460 D.removeClass(P.e.diffTarget, 'hidden');
461 }else if(ev.detail===P.e.tabs.save){
462 const btn = P.e.btnSave;
463 if(!P.winfo || !P.getStashedWinfo(P.winfo)){
464 D.disable(btn).innerText =
465 "There are no changes to save";
466 }else{
467 D.enable(btn).innerText = "Save changes";
468 }
469 }
470 }
471 );
472 P.tabs.addEventListener(
473 /* Set up auto-refresh of the preview tab... */
@@ -487,19 +503,21 @@
503 const w = P.winfo;
504 if(!w){
505 F.error("No page loaded.");
506 return;
507 }
508 if(!w.version/* new/unsaved page */
509 && w.type!=='sandbox'
510 && P.wikiContent()){
511 F.error("This new/unsaved page has content.",
512 "To really discard this page,",
513 "first clear its content",
514 "then use the Discard button.");
515 return;
516 }
517 P.unstashContent()
518 if(w.version || w.type==='sandbox'){
519 P.loadPage();
520 }else{
521 delete P.winfo;
522 WikiList.removeEntry(w.name);
523 P.updatePageTitle();
@@ -506,10 +524,23 @@
524 F.message("Discarded new page ["+w.name+"].");
525 }
526 },
527 ticks: 3
528 });
529 F.confirmer(P.e.btnSave, {
530 confirmText: "Really save changes?",
531 onconfirm: function(e){
532 const w = P.winfo;
533 if(!w){
534 F.error("No page loaded.");
535 return;
536 }
537 P.save();
538 },
539 ticks: 3
540 });
541
542 P.e.taEditor.addEventListener(
543 'change', ()=>P.stashContentChange(), false
544 );
545
546 P.selectMimetype(false, true);
@@ -567,11 +598,11 @@
598 P.previewNeedsUpdate = true;
599 P.e.selectMimetype.value = winfo.mimetype;
600 P.tabs.switchToTab(P.e.tabs.content);
601 P.wikiContent(winfo.content || '');
602 WikiList.e.select.value = winfo.name;
603 if(!winfo.version && winfo.type!=='sandbox'){
604 F.error('You are editing a new, unsaved page:',winfo.name);
605 }
606 P.updatePageTitle();
607 },
608 false
@@ -842,10 +873,43 @@
873 }
874 });
875 return this;
876 };
877
878 /**
879 Saves the current wiki page and re-populates the editor
880 with the saved state.
881 */
882 P.save = function callee(){
883 if(!affirmPageLoaded()) return this;
884 const self = this;
885 const content = this.wikiContent();
886 if(!callee.onload){
887 callee.onload = function(w){
888 const oldWinfo = self.winfo;
889 self.unstashContent(oldWinfo);
890 self.winfo = w;
891 self.updatePageTitle();
892 self.dispatchEvent('wiki-page-loaded', w);
893 F.message("Saved page: ["+w.name+"].");
894 }
895 }
896 const fd = new FormData(), w = P.winfo;
897 fd.append('page',w.name);
898 fd.append('mimetype', w.mimetype);
899 fd.append('isnew', w.version ? 0 : 1);
900 fd.append('content', P.wikiContent());
901 F.message(
902 "Saving page..."
903 ).fetch('wikiajax/save',{
904 payload: fd,
905 responseType: 'json',
906 onload: callee.onload
907 });
908 return this;
909 };
910
911 /**
912 Updates P.winfo for certain state and stashes P.winfo, with the
913 current content fetched via P.wikiContent().
914
915 If passed truthy AND the stash already has stashed content for
916
+160 -70
--- src/wiki.c
+++ src/wiki.c
@@ -646,13 +646,13 @@
646646
** If pRid is not NULL then if a result is found *pRid is set to its
647647
** RID. If ppWiki is not NULL then if found *ppWiki is set to the
648648
** loaded wiki object, which the caller is responsible for passing to
649649
** manifest_destroy().
650650
*/
651
-int wiki_fetch_by_name( const char *zPageName,
652
- unsigned int versionsBack,
653
- int * pRid, Manifest **ppWiki ){
651
+static int wiki_fetch_by_name( const char *zPageName,
652
+ unsigned int versionsBack,
653
+ int * pRid, Manifest **ppWiki ){
654654
Manifest *pWiki = 0;
655655
char *zTag = mprintf("wiki-%s", zPageName);
656656
Stmt q = empty_Stmt;
657657
int rid = 0;
658658
@@ -683,90 +683,91 @@
683683
return 1;
684684
}
685685
686686
/*
687687
** Determines whether the wiki page with the given name can be edited
688
-** by the current user. If not, an AJAX error is queued and false is
689
-** returned, else true is returned. A NULL, empty, or malformed name
690
-** is considered non-writable.
688
+** or created by the current user. If not, an AJAX error is queued and
689
+** false is returned, else true is returned. A NULL, empty, or
690
+** malformed name is considered non-writable, regardless of the user.
691691
**
692692
** If pRid is not NULL then this function writes the page's rid to
693
-** *pRid (whether or not access is granted).
693
+** *pRid (whether or not access is granted). On error or if the page
694
+** does not yet exist, *pRid will be set to 0.
694695
**
695696
** Note that the sandbox is a special case: it is a pseudo-page with
696
-** no rid and this API does not allow anyone to actually save the
697
-** sandbox page, but it is reported as writable here (with rid 0).
697
+** no rid and the /wikiajax API does not allow anyone to actually save
698
+** a sandbox page, but it is reported as writable here (with rid 0).
698699
*/
699700
static int wiki_ajax_can_write(const char *zPageName, int * pRid){
700
- int rid;
701
- const char * zMsg = 0;
701
+ int rid = 0;
702
+ const char * zErr = 0;
702703
703704
if(pRid) *pRid = 0;
704705
if(!zPageName || !*zPageName
705706
|| !wiki_name_is_wellformed((unsigned const char *)zPageName)){
706
- return 0;
707
- }
708
- if(is_sandbox(zPageName)) return 1;
709
- wiki_fetch_by_name(zPageName, 0, &rid, 0);
710
- if(pRid) *pRid = rid;
711
- if(!wiki_special_permission(zPageName)) return 0;
712
- if( (rid && g.perm.WrWiki) || (!rid && g.perm.NewWiki) ){
713
- return 3;
714
- }else if(rid && !g.perm.WrWiki){
715
- zMsg = "Requires wiki-write permissions.";
716
- }else if(!rid && !g.perm.NewWiki){
717
- zMsg = "Requires new-wiki permissions.";
707
+ zErr = "Invalid page name.";
708
+ }else if(is_sandbox(zPageName)){
709
+ return 1;
718710
}else{
719
- assert(!"Can't happen?");
711
+ wiki_fetch_by_name(zPageName, 0, &rid, 0);
712
+ if(pRid) *pRid = rid;
713
+ if(!wiki_special_permission(zPageName)){
714
+ zErr = "Editing this page requires non-wiki write permissions.";
715
+ }else if( (rid && g.perm.WrWiki) || (!rid && g.perm.NewWiki) ){
716
+ return 3;
717
+ }else if(rid && !g.perm.WrWiki){
718
+ zErr = "Requires wiki-write permissions.";
719
+ }else if(!rid && !g.perm.NewWiki){
720
+ zErr = "Requires new-wiki permissions.";
721
+ }else{
722
+ zErr = "Cannot happen! Please report this as a bug.";
723
+ }
720724
}
721
- ajax_route_error(403, "%s", zMsg);
725
+ ajax_route_error(403, "%s", zErr);
722726
return 0;
723727
}
724728
725729
/*
726
-** Ajax route handler for /wikiajax/fetch.
727
-**
728
-** URL params:
729
-**
730
-** page = the wiki page name
731
-**
732
-** Responds with JSON. On error, an object in the form documented by
733
-** ajax_route_error(). On success, an object in this form:
730
+** Loads the given wiki page, sets the response type to
731
+** application/json, and emits it as a JSON object. If zPageName is a
732
+** sandbox page then a "fake" object is emitted, as the wikiajax API
733
+** does not permit saving the sandbox.
734
+**
735
+** Returns true on success, false on error, and on error it
736
+** queues up a JSON-format error response.
737
+**
738
+** Output JSON format:
734739
**
735740
** { name: "page name",
736741
** type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
737742
** mimetype: "mime type",
738743
** version: UUID string or null for a sandbox page,
739744
** parent: "parent uuid" or null if no parent,
740745
** content: "page content"
741746
** }
742747
*/
743
-static void wiki_ajax_route_fetch(void){
744
- const char * zPageName = P("page");
745
- int isSandbox;
746
-
747
- if( zPageName==0 || zPageName[0]==0 ){
748
- ajax_route_error(400,"Missing page name.");
749
- return;
750
- }
748
+static int wiki_ajax_emit_page_object(const char *zPageName){
749
+ Manifest * pWiki = 0;
750
+ char * zUuid;
751
+
751752
cgi_set_content_type("application/json");
752
- isSandbox = is_sandbox(zPageName);
753
- if( isSandbox ){
753
+ if( is_sandbox(zPageName) ){
754754
char * zMimetype =
755755
db_get("sandbox-mimetype","text/x-fossil-wiki");
756
+ char * zBody = db_get("sandbox","");
756757
CX("{\"name\": %!j, \"type\": \"sandbox\", "
757758
"\"mimetype\": %!j, \"version\": null, \"parent\": null, "
758
- "\"content\": \"\"}",
759
- zPageName, zMimetype);
759
+ "\"content\": %!j}",
760
+ zPageName, zMimetype, zBody);
760761
fossil_free(zMimetype);
762
+ fossil_free(zBody);
763
+ return 1;
764
+ }else if( !wiki_fetch_by_name(zPageName, 0, 0, &pWiki) ){
765
+ ajax_route_error(404, "Wiki page could not be loaded: %s",
766
+ zPageName);
767
+ return 0;
761768
}else{
762
- Manifest * pWiki = 0;
763
- char * zUuid;
764
- if( !wiki_fetch_by_name(zPageName, 0, 0, &pWiki) ){
765
- ajax_route_error(404, "Wiki page not found.");
766
- return;
767
- }
768769
zUuid = rid_to_uuid(pWiki->rid);
769770
CX("{\"name\": %!j, \"type\": %!j, "
770771
"\"version\": %!j, "
771772
"\"mimetype\": %!j, ",
772773
pWiki->zWikiTitle,
@@ -780,11 +781,92 @@
780781
CX("null, ");
781782
}
782783
CX("\"content\": %!j}", pWiki->zWiki);
783784
fossil_free(zUuid);
784785
manifest_destroy(pWiki);
786
+ return 1;
787
+ }
788
+}
789
+
790
+/*
791
+** Ajax route handler for /wikiajax/save.
792
+**
793
+** URL params:
794
+**
795
+** page = the wiki page name.
796
+** mimetype = content mime type.
797
+** content = page content. Fossil considers an empty page to
798
+** be "deleted".
799
+** isnew = 1 if the page is to be newly-created, else 0 or
800
+** not send.
801
+**
802
+** Responds with JSON. On error, an object in the form documented by
803
+** ajax_route_error(). On success, an object in the form documented
804
+** for wiki_ajax_emit_page_object().
805
+**
806
+** The wikiajax API disallows saving of a sandbox pseudo-page, and
807
+** will respond with an error if asked to save one.
808
+*/
809
+static void wiki_ajax_route_save(void){
810
+ const char *zPageName = P("page");
811
+ const char *zMimetype = P("mimetype");
812
+ const char *zContent = P("content");
813
+ const int isNew = atoi(PD("isnew","0"))==1;
814
+ Blob content = empty_blob;
815
+ int parentRid = 0;
816
+ int rollback = 0;
817
+
818
+ if(!wiki_ajax_can_write(zPageName, &parentRid)){
819
+ return;
820
+ }else if(is_sandbox(zPageName)){
821
+ ajax_route_error(403,"Saving a sandbox page is prohibited.");
822
+ return;
823
+ }
824
+
825
+ /* These isNew checks are just me being pedantic. The hope is
826
+ to avoid accidental addition of new pages which differ only
827
+ by the case of their name. We could just as easily derive
828
+ isNew based on whether or not the page already exists. */
829
+ if(isNew){
830
+ if(parentRid>0){
831
+ ajax_route_error(403,"Requested a new page, "
832
+ "but it already exists with RID %d: %s",
833
+ parentRid, zPageName);
834
+ return;
835
+ }
836
+ }else if(parentRid==0){
837
+ ajax_route_error(403,"Creating new page [%s] requires passing "
838
+ "isnew=1.", zPageName);
839
+ return;
840
+ }
841
+
842
+ blob_init(&content, zContent ? zContent : "", -1);
843
+ db_begin_transaction();
844
+ wiki_cmd_commit(zPageName, parentRid, &content, zMimetype, 0);
845
+ rollback = wiki_ajax_emit_page_object(zPageName) ? 0 : 1;
846
+ db_end_transaction(rollback);
847
+}
848
+
849
+/*
850
+** Ajax route handler for /wikiajax/fetch.
851
+**
852
+** URL params:
853
+**
854
+** page = the wiki page name
855
+**
856
+** Responds with JSON. On error, an object in the form documented by
857
+** ajax_route_error(). On success, an object in the form documented
858
+** for wiki_ajax_emit_page_object().
859
+*/
860
+static void wiki_ajax_route_fetch(void){
861
+ const char * zPageName = P("page");
862
+
863
+ if( zPageName==0 || zPageName[0]==0 ){
864
+ ajax_route_error(400,"Missing page name.");
865
+ return;
785866
}
867
+ wiki_ajax_emit_page_object(zPageName);
786868
}
787869
788870
/*
789871
** Ajax route handler for /wikiajax/diff.
790872
**
@@ -824,16 +906,15 @@
824906
}else{
825907
blob_init(&contentOrig, "", 0);
826908
}
827909
blob_init(&contentNew, zContent ? zContent : "", -1);
828910
cgi_set_content_type("text/html");
829
- ajax_render_diff(&contentNew, &contentOrig, diffFlags);
911
+ ajax_render_diff(&contentOrig, &contentNew, diffFlags);
830912
blob_reset(&contentNew);
831913
blob_reset(&contentOrig);
832914
manifest_destroy(pParent);
833915
}
834
-
835916
836917
/*
837918
** Ajax route handler for /wikiajax/preview.
838919
**
839920
** URL params:
@@ -866,11 +947,12 @@
866947
/*
867948
** Ajax route handler for /wikiajax/list.
868949
**
869950
** Responds with JSON. On error, an object in the form documented by
870951
** ajax_route_error(). On success, an array of strings (page names)
871
-** sorted case-insensitively.
952
+** sorted case-insensitively. The result list contains an entry
953
+** named "sandbox" which represents the sandbox pseudo-page.
872954
*/
873955
static void wiki_ajax_route_list(void){
874956
Stmt q = empty_Stmt;
875957
int n = 0;
876958
@@ -907,16 +989,17 @@
907989
/* Keep these sorted by zName (for bsearch()) */
908990
{"diff", wiki_ajax_route_diff, 1, 1},
909991
{"fetch", wiki_ajax_route_fetch, 0, 0},
910992
{"list", wiki_ajax_route_list, 0, 0},
911993
{"preview", wiki_ajax_route_preview, 0, 1}
912
- /* /preview access mode: whether or not wiki-write mode is needed
994
+ /* preview access mode: whether or not wiki-write mode is needed
913995
really depends on multiple factors. e.g. the sandbox page does
914
- not normally require more than anonymous access. We set its
996
+ not normally require more than anonymous access. We set its
915997
write-mode to false and do those checks manually in that route's
916998
handler.
917
- */
999
+ */,
1000
+ {"save", wiki_ajax_route_save, 1, 1}
9181001
};
9191002
9201003
if(zName==0 || zName[0]==0){
9211004
ajax_route_error(400,"Missing required [route] 'name' parameter.");
9221005
return;
@@ -1011,11 +1094,11 @@
10111094
"data-tab-parent='wikiedit-tabs' "
10121095
"data-tab-label='Page Editor'"
10131096
">");
10141097
CX("<div class='flex-container flex-row child-gap-small'>");
10151098
mimetype_option_menu(0);
1016
- CX("<button class='wikiedit-content-reload confirmer' "
1099
+ CX("<button class='wikiedit-content-reload' "
10171100
"title='Reload the file from the server, discarding "
10181101
"any local edits. To help avoid accidental loss of "
10191102
"edits, it requires confirmation (a second click) within "
10201103
"a few seconds or it will not reload.'"
10211104
">Discard &amp; Reload</button>");
@@ -1095,19 +1178,21 @@
10951178
"Diffs will be shown here."
10961179
"</div>");
10971180
CX("</div>"/*#wikiedit-tab-diff*/);
10981181
}
10991182
1100
- /****** TODOs (remove before merging to trunk) ******/
11011183
{
1102
- CX("<div id='wikiedit-tab-todos' "
1184
+ CX("<div id='wikiedit-tab-save' "
11031185
"data-tab-parent='wikiedit-tabs' "
1104
- "data-tab-label='TODOs'"
1186
+ "data-tab-label='Save &amp; Help'"
11051187
">");
1106
- CX("TODOs, in no particular order:<ul>");
1107
- CX("<li>Saving, obviously.</li>");
1108
- /*CX("<li></li>");*/
1188
+ CX("<button class='wikiedit-save'>Save</button>");
1189
+ CX("<hr>");
1190
+ CX("The wiki formatting rules can be found at:");
1191
+ CX("<ul>");
1192
+ CX("<li><a href='%R/wiki_rules'>Fossil wiki format</a></li>");
1193
+ CX("<li><a href='%R/md_rules'>Markdown format</a></li>");
11091194
CX("</ul>");
11101195
CX("</div>");
11111196
}
11121197
11131198
style_emit_script_fossil_bootstrap(0);
@@ -1119,27 +1204,32 @@
11191204
style_emit_script_builtin(0, "fossil.page.wikiedit.js");
11201205
11211206
/* Dynamically populate the editor... */
11221207
style_emit_script_tag(0,0);
11231208
CX("\nfossil.onPageLoad(function(){\n");
1124
- CX("try{\n");
1209
+ CX("const P = fossil.page;\n"
1210
+ "try{\n");
11251211
if(found){
1126
- CX("fossil.page.loadPage(%!j);\n", zPageName);
1212
+ CX("P.loadPage(%!j);\n", zPageName);
11271213
}else if(zPageName && *zPageName){
11281214
/* For a new page, stick a dummy entry in the JS-side stash
1129
- and simulate an on-load reaction to update the editor
1130
- with that stashed state. */
1215
+ and "load" it from there. */
11311216
CX("const winfo = {"
11321217
"\"name\": %!j, \"mimetype\": %!j, "
11331218
"\"type\": %!j, "
11341219
"\"parent\": null, \"version\": null"
11351220
"};\n",
11361221
zPageName,
11371222
zMimetype ? zMimetype : "text/x-fossil-wiki",
11381223
wiki_page_type_name(zPageName));
1139
- CX("fossil.page.$stash.updateWinfo(winfo,'');\n");
1140
- CX("fossil.page.dispatchEvent('wiki-page-loaded',winfo);\n");
1224
+ /* If the JS-side stash already has this page, load that
1225
+ copy from the stash, otherwise inject a new stash entry
1226
+ for it and load *that* one... */
1227
+ CX("if(!P.$stash.getWinfo(winfo)){"
1228
+ "P.$stash.updateWinfo(winfo,'');"
1229
+ "}\n");
1230
+ CX("P.loadPage(%!j);\n", zPageName);
11411231
}
11421232
CX("}catch(e){"
11431233
"fossil.error(e); console.error('Exception:',e);"
11441234
"}\n");
11451235
CX("});\n"/*fossil.onPageLoad()*/);
11461236
--- src/wiki.c
+++ src/wiki.c
@@ -646,13 +646,13 @@
646 ** If pRid is not NULL then if a result is found *pRid is set to its
647 ** RID. If ppWiki is not NULL then if found *ppWiki is set to the
648 ** loaded wiki object, which the caller is responsible for passing to
649 ** manifest_destroy().
650 */
651 int wiki_fetch_by_name( const char *zPageName,
652 unsigned int versionsBack,
653 int * pRid, Manifest **ppWiki ){
654 Manifest *pWiki = 0;
655 char *zTag = mprintf("wiki-%s", zPageName);
656 Stmt q = empty_Stmt;
657 int rid = 0;
658
@@ -683,90 +683,91 @@
683 return 1;
684 }
685
686 /*
687 ** Determines whether the wiki page with the given name can be edited
688 ** by the current user. If not, an AJAX error is queued and false is
689 ** returned, else true is returned. A NULL, empty, or malformed name
690 ** is considered non-writable.
691 **
692 ** If pRid is not NULL then this function writes the page's rid to
693 ** *pRid (whether or not access is granted).
 
694 **
695 ** Note that the sandbox is a special case: it is a pseudo-page with
696 ** no rid and this API does not allow anyone to actually save the
697 ** sandbox page, but it is reported as writable here (with rid 0).
698 */
699 static int wiki_ajax_can_write(const char *zPageName, int * pRid){
700 int rid;
701 const char * zMsg = 0;
702
703 if(pRid) *pRid = 0;
704 if(!zPageName || !*zPageName
705 || !wiki_name_is_wellformed((unsigned const char *)zPageName)){
706 return 0;
707 }
708 if(is_sandbox(zPageName)) return 1;
709 wiki_fetch_by_name(zPageName, 0, &rid, 0);
710 if(pRid) *pRid = rid;
711 if(!wiki_special_permission(zPageName)) return 0;
712 if( (rid && g.perm.WrWiki) || (!rid && g.perm.NewWiki) ){
713 return 3;
714 }else if(rid && !g.perm.WrWiki){
715 zMsg = "Requires wiki-write permissions.";
716 }else if(!rid && !g.perm.NewWiki){
717 zMsg = "Requires new-wiki permissions.";
718 }else{
719 assert(!"Can't happen?");
 
 
 
 
 
 
 
 
 
 
 
 
720 }
721 ajax_route_error(403, "%s", zMsg);
722 return 0;
723 }
724
725 /*
726 ** Ajax route handler for /wikiajax/fetch.
727 **
728 ** URL params:
729 **
730 ** page = the wiki page name
731 **
732 ** Responds with JSON. On error, an object in the form documented by
733 ** ajax_route_error(). On success, an object in this form:
 
734 **
735 ** { name: "page name",
736 ** type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
737 ** mimetype: "mime type",
738 ** version: UUID string or null for a sandbox page,
739 ** parent: "parent uuid" or null if no parent,
740 ** content: "page content"
741 ** }
742 */
743 static void wiki_ajax_route_fetch(void){
744 const char * zPageName = P("page");
745 int isSandbox;
746
747 if( zPageName==0 || zPageName[0]==0 ){
748 ajax_route_error(400,"Missing page name.");
749 return;
750 }
751 cgi_set_content_type("application/json");
752 isSandbox = is_sandbox(zPageName);
753 if( isSandbox ){
754 char * zMimetype =
755 db_get("sandbox-mimetype","text/x-fossil-wiki");
 
756 CX("{\"name\": %!j, \"type\": \"sandbox\", "
757 "\"mimetype\": %!j, \"version\": null, \"parent\": null, "
758 "\"content\": \"\"}",
759 zPageName, zMimetype);
760 fossil_free(zMimetype);
 
 
 
 
 
 
761 }else{
762 Manifest * pWiki = 0;
763 char * zUuid;
764 if( !wiki_fetch_by_name(zPageName, 0, 0, &pWiki) ){
765 ajax_route_error(404, "Wiki page not found.");
766 return;
767 }
768 zUuid = rid_to_uuid(pWiki->rid);
769 CX("{\"name\": %!j, \"type\": %!j, "
770 "\"version\": %!j, "
771 "\"mimetype\": %!j, ",
772 pWiki->zWikiTitle,
@@ -780,11 +781,92 @@
780 CX("null, ");
781 }
782 CX("\"content\": %!j}", pWiki->zWiki);
783 fossil_free(zUuid);
784 manifest_destroy(pWiki);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
785 }
 
786 }
787
788 /*
789 ** Ajax route handler for /wikiajax/diff.
790 **
@@ -824,16 +906,15 @@
824 }else{
825 blob_init(&contentOrig, "", 0);
826 }
827 blob_init(&contentNew, zContent ? zContent : "", -1);
828 cgi_set_content_type("text/html");
829 ajax_render_diff(&contentNew, &contentOrig, diffFlags);
830 blob_reset(&contentNew);
831 blob_reset(&contentOrig);
832 manifest_destroy(pParent);
833 }
834
835
836 /*
837 ** Ajax route handler for /wikiajax/preview.
838 **
839 ** URL params:
@@ -866,11 +947,12 @@
866 /*
867 ** Ajax route handler for /wikiajax/list.
868 **
869 ** Responds with JSON. On error, an object in the form documented by
870 ** ajax_route_error(). On success, an array of strings (page names)
871 ** sorted case-insensitively.
 
872 */
873 static void wiki_ajax_route_list(void){
874 Stmt q = empty_Stmt;
875 int n = 0;
876
@@ -907,16 +989,17 @@
907 /* Keep these sorted by zName (for bsearch()) */
908 {"diff", wiki_ajax_route_diff, 1, 1},
909 {"fetch", wiki_ajax_route_fetch, 0, 0},
910 {"list", wiki_ajax_route_list, 0, 0},
911 {"preview", wiki_ajax_route_preview, 0, 1}
912 /* /preview access mode: whether or not wiki-write mode is needed
913 really depends on multiple factors. e.g. the sandbox page does
914 not normally require more than anonymous access. We set its
915 write-mode to false and do those checks manually in that route's
916 handler.
917 */
 
918 };
919
920 if(zName==0 || zName[0]==0){
921 ajax_route_error(400,"Missing required [route] 'name' parameter.");
922 return;
@@ -1011,11 +1094,11 @@
1011 "data-tab-parent='wikiedit-tabs' "
1012 "data-tab-label='Page Editor'"
1013 ">");
1014 CX("<div class='flex-container flex-row child-gap-small'>");
1015 mimetype_option_menu(0);
1016 CX("<button class='wikiedit-content-reload confirmer' "
1017 "title='Reload the file from the server, discarding "
1018 "any local edits. To help avoid accidental loss of "
1019 "edits, it requires confirmation (a second click) within "
1020 "a few seconds or it will not reload.'"
1021 ">Discard &amp; Reload</button>");
@@ -1095,19 +1178,21 @@
1095 "Diffs will be shown here."
1096 "</div>");
1097 CX("</div>"/*#wikiedit-tab-diff*/);
1098 }
1099
1100 /****** TODOs (remove before merging to trunk) ******/
1101 {
1102 CX("<div id='wikiedit-tab-todos' "
1103 "data-tab-parent='wikiedit-tabs' "
1104 "data-tab-label='TODOs'"
1105 ">");
1106 CX("TODOs, in no particular order:<ul>");
1107 CX("<li>Saving, obviously.</li>");
1108 /*CX("<li></li>");*/
 
 
 
1109 CX("</ul>");
1110 CX("</div>");
1111 }
1112
1113 style_emit_script_fossil_bootstrap(0);
@@ -1119,27 +1204,32 @@
1119 style_emit_script_builtin(0, "fossil.page.wikiedit.js");
1120
1121 /* Dynamically populate the editor... */
1122 style_emit_script_tag(0,0);
1123 CX("\nfossil.onPageLoad(function(){\n");
1124 CX("try{\n");
 
1125 if(found){
1126 CX("fossil.page.loadPage(%!j);\n", zPageName);
1127 }else if(zPageName && *zPageName){
1128 /* For a new page, stick a dummy entry in the JS-side stash
1129 and simulate an on-load reaction to update the editor
1130 with that stashed state. */
1131 CX("const winfo = {"
1132 "\"name\": %!j, \"mimetype\": %!j, "
1133 "\"type\": %!j, "
1134 "\"parent\": null, \"version\": null"
1135 "};\n",
1136 zPageName,
1137 zMimetype ? zMimetype : "text/x-fossil-wiki",
1138 wiki_page_type_name(zPageName));
1139 CX("fossil.page.$stash.updateWinfo(winfo,'');\n");
1140 CX("fossil.page.dispatchEvent('wiki-page-loaded',winfo);\n");
 
 
 
 
 
1141 }
1142 CX("}catch(e){"
1143 "fossil.error(e); console.error('Exception:',e);"
1144 "}\n");
1145 CX("});\n"/*fossil.onPageLoad()*/);
1146
--- src/wiki.c
+++ src/wiki.c
@@ -646,13 +646,13 @@
646 ** If pRid is not NULL then if a result is found *pRid is set to its
647 ** RID. If ppWiki is not NULL then if found *ppWiki is set to the
648 ** loaded wiki object, which the caller is responsible for passing to
649 ** manifest_destroy().
650 */
651 static int wiki_fetch_by_name( const char *zPageName,
652 unsigned int versionsBack,
653 int * pRid, Manifest **ppWiki ){
654 Manifest *pWiki = 0;
655 char *zTag = mprintf("wiki-%s", zPageName);
656 Stmt q = empty_Stmt;
657 int rid = 0;
658
@@ -683,90 +683,91 @@
683 return 1;
684 }
685
686 /*
687 ** Determines whether the wiki page with the given name can be edited
688 ** or created by the current user. If not, an AJAX error is queued and
689 ** false is returned, else true is returned. A NULL, empty, or
690 ** malformed name is considered non-writable, regardless of the user.
691 **
692 ** If pRid is not NULL then this function writes the page's rid to
693 ** *pRid (whether or not access is granted). On error or if the page
694 ** does not yet exist, *pRid will be set to 0.
695 **
696 ** Note that the sandbox is a special case: it is a pseudo-page with
697 ** no rid and the /wikiajax API does not allow anyone to actually save
698 ** a sandbox page, but it is reported as writable here (with rid 0).
699 */
700 static int wiki_ajax_can_write(const char *zPageName, int * pRid){
701 int rid = 0;
702 const char * zErr = 0;
703
704 if(pRid) *pRid = 0;
705 if(!zPageName || !*zPageName
706 || !wiki_name_is_wellformed((unsigned const char *)zPageName)){
707 zErr = "Invalid page name.";
708 }else if(is_sandbox(zPageName)){
709 return 1;
 
 
 
 
 
 
 
 
 
710 }else{
711 wiki_fetch_by_name(zPageName, 0, &rid, 0);
712 if(pRid) *pRid = rid;
713 if(!wiki_special_permission(zPageName)){
714 zErr = "Editing this page requires non-wiki write permissions.";
715 }else if( (rid && g.perm.WrWiki) || (!rid && g.perm.NewWiki) ){
716 return 3;
717 }else if(rid && !g.perm.WrWiki){
718 zErr = "Requires wiki-write permissions.";
719 }else if(!rid && !g.perm.NewWiki){
720 zErr = "Requires new-wiki permissions.";
721 }else{
722 zErr = "Cannot happen! Please report this as a bug.";
723 }
724 }
725 ajax_route_error(403, "%s", zErr);
726 return 0;
727 }
728
729 /*
730 ** Loads the given wiki page, sets the response type to
731 ** application/json, and emits it as a JSON object. If zPageName is a
732 ** sandbox page then a "fake" object is emitted, as the wikiajax API
733 ** does not permit saving the sandbox.
734 **
735 ** Returns true on success, false on error, and on error it
736 ** queues up a JSON-format error response.
737 **
738 ** Output JSON format:
739 **
740 ** { name: "page name",
741 ** type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
742 ** mimetype: "mime type",
743 ** version: UUID string or null for a sandbox page,
744 ** parent: "parent uuid" or null if no parent,
745 ** content: "page content"
746 ** }
747 */
748 static int wiki_ajax_emit_page_object(const char *zPageName){
749 Manifest * pWiki = 0;
750 char * zUuid;
751
 
 
 
 
752 cgi_set_content_type("application/json");
753 if( is_sandbox(zPageName) ){
 
754 char * zMimetype =
755 db_get("sandbox-mimetype","text/x-fossil-wiki");
756 char * zBody = db_get("sandbox","");
757 CX("{\"name\": %!j, \"type\": \"sandbox\", "
758 "\"mimetype\": %!j, \"version\": null, \"parent\": null, "
759 "\"content\": %!j}",
760 zPageName, zMimetype, zBody);
761 fossil_free(zMimetype);
762 fossil_free(zBody);
763 return 1;
764 }else if( !wiki_fetch_by_name(zPageName, 0, 0, &pWiki) ){
765 ajax_route_error(404, "Wiki page could not be loaded: %s",
766 zPageName);
767 return 0;
768 }else{
 
 
 
 
 
 
769 zUuid = rid_to_uuid(pWiki->rid);
770 CX("{\"name\": %!j, \"type\": %!j, "
771 "\"version\": %!j, "
772 "\"mimetype\": %!j, ",
773 pWiki->zWikiTitle,
@@ -780,11 +781,92 @@
781 CX("null, ");
782 }
783 CX("\"content\": %!j}", pWiki->zWiki);
784 fossil_free(zUuid);
785 manifest_destroy(pWiki);
786 return 1;
787 }
788 }
789
790 /*
791 ** Ajax route handler for /wikiajax/save.
792 **
793 ** URL params:
794 **
795 ** page = the wiki page name.
796 ** mimetype = content mime type.
797 ** content = page content. Fossil considers an empty page to
798 ** be "deleted".
799 ** isnew = 1 if the page is to be newly-created, else 0 or
800 ** not send.
801 **
802 ** Responds with JSON. On error, an object in the form documented by
803 ** ajax_route_error(). On success, an object in the form documented
804 ** for wiki_ajax_emit_page_object().
805 **
806 ** The wikiajax API disallows saving of a sandbox pseudo-page, and
807 ** will respond with an error if asked to save one.
808 */
809 static void wiki_ajax_route_save(void){
810 const char *zPageName = P("page");
811 const char *zMimetype = P("mimetype");
812 const char *zContent = P("content");
813 const int isNew = atoi(PD("isnew","0"))==1;
814 Blob content = empty_blob;
815 int parentRid = 0;
816 int rollback = 0;
817
818 if(!wiki_ajax_can_write(zPageName, &parentRid)){
819 return;
820 }else if(is_sandbox(zPageName)){
821 ajax_route_error(403,"Saving a sandbox page is prohibited.");
822 return;
823 }
824
825 /* These isNew checks are just me being pedantic. The hope is
826 to avoid accidental addition of new pages which differ only
827 by the case of their name. We could just as easily derive
828 isNew based on whether or not the page already exists. */
829 if(isNew){
830 if(parentRid>0){
831 ajax_route_error(403,"Requested a new page, "
832 "but it already exists with RID %d: %s",
833 parentRid, zPageName);
834 return;
835 }
836 }else if(parentRid==0){
837 ajax_route_error(403,"Creating new page [%s] requires passing "
838 "isnew=1.", zPageName);
839 return;
840 }
841
842 blob_init(&content, zContent ? zContent : "", -1);
843 db_begin_transaction();
844 wiki_cmd_commit(zPageName, parentRid, &content, zMimetype, 0);
845 rollback = wiki_ajax_emit_page_object(zPageName) ? 0 : 1;
846 db_end_transaction(rollback);
847 }
848
849 /*
850 ** Ajax route handler for /wikiajax/fetch.
851 **
852 ** URL params:
853 **
854 ** page = the wiki page name
855 **
856 ** Responds with JSON. On error, an object in the form documented by
857 ** ajax_route_error(). On success, an object in the form documented
858 ** for wiki_ajax_emit_page_object().
859 */
860 static void wiki_ajax_route_fetch(void){
861 const char * zPageName = P("page");
862
863 if( zPageName==0 || zPageName[0]==0 ){
864 ajax_route_error(400,"Missing page name.");
865 return;
866 }
867 wiki_ajax_emit_page_object(zPageName);
868 }
869
870 /*
871 ** Ajax route handler for /wikiajax/diff.
872 **
@@ -824,16 +906,15 @@
906 }else{
907 blob_init(&contentOrig, "", 0);
908 }
909 blob_init(&contentNew, zContent ? zContent : "", -1);
910 cgi_set_content_type("text/html");
911 ajax_render_diff(&contentOrig, &contentNew, diffFlags);
912 blob_reset(&contentNew);
913 blob_reset(&contentOrig);
914 manifest_destroy(pParent);
915 }
 
916
917 /*
918 ** Ajax route handler for /wikiajax/preview.
919 **
920 ** URL params:
@@ -866,11 +947,12 @@
947 /*
948 ** Ajax route handler for /wikiajax/list.
949 **
950 ** Responds with JSON. On error, an object in the form documented by
951 ** ajax_route_error(). On success, an array of strings (page names)
952 ** sorted case-insensitively. The result list contains an entry
953 ** named "sandbox" which represents the sandbox pseudo-page.
954 */
955 static void wiki_ajax_route_list(void){
956 Stmt q = empty_Stmt;
957 int n = 0;
958
@@ -907,16 +989,17 @@
989 /* Keep these sorted by zName (for bsearch()) */
990 {"diff", wiki_ajax_route_diff, 1, 1},
991 {"fetch", wiki_ajax_route_fetch, 0, 0},
992 {"list", wiki_ajax_route_list, 0, 0},
993 {"preview", wiki_ajax_route_preview, 0, 1}
994 /* preview access mode: whether or not wiki-write mode is needed
995 really depends on multiple factors. e.g. the sandbox page does
996 not normally require more than anonymous access. We set its
997 write-mode to false and do those checks manually in that route's
998 handler.
999 */,
1000 {"save", wiki_ajax_route_save, 1, 1}
1001 };
1002
1003 if(zName==0 || zName[0]==0){
1004 ajax_route_error(400,"Missing required [route] 'name' parameter.");
1005 return;
@@ -1011,11 +1094,11 @@
1094 "data-tab-parent='wikiedit-tabs' "
1095 "data-tab-label='Page Editor'"
1096 ">");
1097 CX("<div class='flex-container flex-row child-gap-small'>");
1098 mimetype_option_menu(0);
1099 CX("<button class='wikiedit-content-reload' "
1100 "title='Reload the file from the server, discarding "
1101 "any local edits. To help avoid accidental loss of "
1102 "edits, it requires confirmation (a second click) within "
1103 "a few seconds or it will not reload.'"
1104 ">Discard &amp; Reload</button>");
@@ -1095,19 +1178,21 @@
1178 "Diffs will be shown here."
1179 "</div>");
1180 CX("</div>"/*#wikiedit-tab-diff*/);
1181 }
1182
 
1183 {
1184 CX("<div id='wikiedit-tab-save' "
1185 "data-tab-parent='wikiedit-tabs' "
1186 "data-tab-label='Save &amp; Help'"
1187 ">");
1188 CX("<button class='wikiedit-save'>Save</button>");
1189 CX("<hr>");
1190 CX("The wiki formatting rules can be found at:");
1191 CX("<ul>");
1192 CX("<li><a href='%R/wiki_rules'>Fossil wiki format</a></li>");
1193 CX("<li><a href='%R/md_rules'>Markdown format</a></li>");
1194 CX("</ul>");
1195 CX("</div>");
1196 }
1197
1198 style_emit_script_fossil_bootstrap(0);
@@ -1119,27 +1204,32 @@
1204 style_emit_script_builtin(0, "fossil.page.wikiedit.js");
1205
1206 /* Dynamically populate the editor... */
1207 style_emit_script_tag(0,0);
1208 CX("\nfossil.onPageLoad(function(){\n");
1209 CX("const P = fossil.page;\n"
1210 "try{\n");
1211 if(found){
1212 CX("P.loadPage(%!j);\n", zPageName);
1213 }else if(zPageName && *zPageName){
1214 /* For a new page, stick a dummy entry in the JS-side stash
1215 and "load" it from there. */
 
1216 CX("const winfo = {"
1217 "\"name\": %!j, \"mimetype\": %!j, "
1218 "\"type\": %!j, "
1219 "\"parent\": null, \"version\": null"
1220 "};\n",
1221 zPageName,
1222 zMimetype ? zMimetype : "text/x-fossil-wiki",
1223 wiki_page_type_name(zPageName));
1224 /* If the JS-side stash already has this page, load that
1225 copy from the stash, otherwise inject a new stash entry
1226 for it and load *that* one... */
1227 CX("if(!P.$stash.getWinfo(winfo)){"
1228 "P.$stash.updateWinfo(winfo,'');"
1229 "}\n");
1230 CX("P.loadPage(%!j);\n", zPageName);
1231 }
1232 CX("}catch(e){"
1233 "fossil.error(e); console.error('Exception:',e);"
1234 "}\n");
1235 CX("});\n"/*fossil.onPageLoad()*/);
1236

Keyboard Shortcuts

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