Fossil SCM
Permissions checks improvements. Added a couple TODOs, notably for how to integrate handling of new/as-yet-unsaved pages into the UI.
Commit
044e2b55a57cc68fb702fd5fd8239efb78de33a57b6730a6f4e077eb48aae122
Parent
5d61cec568febc3…
2 files changed
+3
-2
+121
-41
+3
-2
| --- src/fossil.page.wikiedit.js | ||
| +++ src/fossil.page.wikiedit.js | ||
| @@ -534,11 +534,11 @@ | ||
| 534 | 534 | For use when installing a custom editor widget. Pass it the |
| 535 | 535 | getter and setter callbacks to fetch resp. set the content of the |
| 536 | 536 | custom widget. They will be triggered via |
| 537 | 537 | P.wikiContent(). Returns this object. |
| 538 | 538 | */ |
| 539 | - P.setFileContentMethods = function(getter, setter){ | |
| 539 | + P.setContentMethods = function(getter, setter){ | |
| 540 | 540 | this.wikiContent.get = getter; |
| 541 | 541 | this.wikiContent.set = setter; |
| 542 | 542 | return this; |
| 543 | 543 | }; |
| 544 | 544 | |
| @@ -678,10 +678,11 @@ | ||
| 678 | 678 | callback(content); |
| 679 | 679 | return this; |
| 680 | 680 | } |
| 681 | 681 | const fd = new FormData(); |
| 682 | 682 | const mimetype = this.e.selectMimetype.value; |
| 683 | + fd.append('page', this.winfo.name); | |
| 683 | 684 | fd.append('mimetype',mimetype); |
| 684 | 685 | fd.append('content',content || ''); |
| 685 | 686 | F.message( |
| 686 | 687 | "Fetching preview..." |
| 687 | 688 | ).fetch('wikiajax/preview',{ |
| @@ -743,11 +744,11 @@ | ||
| 743 | 744 | ).fetch('wikiajax/diff',{ |
| 744 | 745 | payload: fd, |
| 745 | 746 | onload: function(c){ |
| 746 | 747 | target.innerHTML = [ |
| 747 | 748 | "<div>Diff <code>[", |
| 748 | - self.winfo.checkin, | |
| 749 | + self.winfo.name, | |
| 749 | 750 | "]</code> → Local Edits</div>", |
| 750 | 751 | c||'No changes.' |
| 751 | 752 | ].join(''); |
| 752 | 753 | if(sbs) P.tweakSbsDiffs2(); |
| 753 | 754 | F.message('Updated diff.'); |
| 754 | 755 |
| --- src/fossil.page.wikiedit.js | |
| +++ src/fossil.page.wikiedit.js | |
| @@ -534,11 +534,11 @@ | |
| 534 | For use when installing a custom editor widget. Pass it the |
| 535 | getter and setter callbacks to fetch resp. set the content of the |
| 536 | custom widget. They will be triggered via |
| 537 | P.wikiContent(). Returns this object. |
| 538 | */ |
| 539 | P.setFileContentMethods = function(getter, setter){ |
| 540 | this.wikiContent.get = getter; |
| 541 | this.wikiContent.set = setter; |
| 542 | return this; |
| 543 | }; |
| 544 | |
| @@ -678,10 +678,11 @@ | |
| 678 | callback(content); |
| 679 | return this; |
| 680 | } |
| 681 | const fd = new FormData(); |
| 682 | const mimetype = this.e.selectMimetype.value; |
| 683 | fd.append('mimetype',mimetype); |
| 684 | fd.append('content',content || ''); |
| 685 | F.message( |
| 686 | "Fetching preview..." |
| 687 | ).fetch('wikiajax/preview',{ |
| @@ -743,11 +744,11 @@ | |
| 743 | ).fetch('wikiajax/diff',{ |
| 744 | payload: fd, |
| 745 | onload: function(c){ |
| 746 | target.innerHTML = [ |
| 747 | "<div>Diff <code>[", |
| 748 | self.winfo.checkin, |
| 749 | "]</code> → Local Edits</div>", |
| 750 | c||'No changes.' |
| 751 | ].join(''); |
| 752 | if(sbs) P.tweakSbsDiffs2(); |
| 753 | F.message('Updated diff.'); |
| 754 |
| --- src/fossil.page.wikiedit.js | |
| +++ src/fossil.page.wikiedit.js | |
| @@ -534,11 +534,11 @@ | |
| 534 | For use when installing a custom editor widget. Pass it the |
| 535 | getter and setter callbacks to fetch resp. set the content of the |
| 536 | custom widget. They will be triggered via |
| 537 | P.wikiContent(). Returns this object. |
| 538 | */ |
| 539 | P.setContentMethods = function(getter, setter){ |
| 540 | this.wikiContent.get = getter; |
| 541 | this.wikiContent.set = setter; |
| 542 | return this; |
| 543 | }; |
| 544 | |
| @@ -678,10 +678,11 @@ | |
| 678 | callback(content); |
| 679 | return this; |
| 680 | } |
| 681 | const fd = new FormData(); |
| 682 | const mimetype = this.e.selectMimetype.value; |
| 683 | fd.append('page', this.winfo.name); |
| 684 | fd.append('mimetype',mimetype); |
| 685 | fd.append('content',content || ''); |
| 686 | F.message( |
| 687 | "Fetching preview..." |
| 688 | ).fetch('wikiajax/preview',{ |
| @@ -743,11 +744,11 @@ | |
| 744 | ).fetch('wikiajax/diff',{ |
| 745 | payload: fd, |
| 746 | onload: function(c){ |
| 747 | target.innerHTML = [ |
| 748 | "<div>Diff <code>[", |
| 749 | self.winfo.name, |
| 750 | "]</code> → Local Edits</div>", |
| 751 | c||'No changes.' |
| 752 | ].join(''); |
| 753 | if(sbs) P.tweakSbsDiffs2(); |
| 754 | F.message('Updated diff.'); |
| 755 |
+121
-41
| --- src/wiki.c | ||
| +++ src/wiki.c | ||
| @@ -680,10 +680,49 @@ | ||
| 680 | 680 | } |
| 681 | 681 | *ppWiki = pWiki; |
| 682 | 682 | } |
| 683 | 683 | return 1; |
| 684 | 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 | +} | |
| 685 | 724 | |
| 686 | 725 | /* |
| 687 | 726 | ** Ajax route handler for /wikiajax/fetch. |
| 688 | 727 | ** |
| 689 | 728 | ** URL params: |
| @@ -695,11 +734,11 @@ | ||
| 695 | 734 | ** |
| 696 | 735 | ** { name: "page name", |
| 697 | 736 | ** type: "normal" | "tag" | "checkin" | "branch" | "sandbox", |
| 698 | 737 | ** mimetype: "mime type", |
| 699 | 738 | ** version: UUID string or null for a sandbox page, |
| 700 | -** parent: "parent uuid" or null, | |
| 739 | +** parent: "parent uuid" or null if no parent, | |
| 701 | 740 | ** content: "page content" |
| 702 | 741 | ** } |
| 703 | 742 | */ |
| 704 | 743 | static void wiki_ajax_route_fetch(void){ |
| 705 | 744 | const char * zPageName = P("page"); |
| @@ -751,10 +790,13 @@ | ||
| 751 | 790 | ** |
| 752 | 791 | ** URL params: |
| 753 | 792 | ** |
| 754 | 793 | ** page = the wiki page name |
| 755 | 794 | ** content = the new/edited wiki page content |
| 795 | +** | |
| 796 | +** Requires that the user have write access solely to avoid some | |
| 797 | +** potential abuse cases. It does not actually write anything. | |
| 756 | 798 | */ |
| 757 | 799 | static void wiki_ajax_route_diff(void){ |
| 758 | 800 | const char * zPageName = P("page"); |
| 759 | 801 | Blob contentNew = empty_blob, contentOrig = empty_blob; |
| 760 | 802 | Manifest * pParent = 0; |
| @@ -761,10 +803,12 @@ | ||
| 761 | 803 | const char * zContent = P("content"); |
| 762 | 804 | u64 diffFlags = DIFF_HTML | DIFF_NOTTOOBIG | DIFF_STRIP_EOLCR; |
| 763 | 805 | |
| 764 | 806 | if( zPageName==0 || zPageName[0]==0 ){ |
| 765 | 807 | ajax_route_error(400,"Missing page name."); |
| 808 | + return; | |
| 809 | + }else if(!wiki_ajax_can_write(zPageName, 0)){ | |
| 766 | 810 | return; |
| 767 | 811 | } |
| 768 | 812 | switch(atoi(PD("sbs","0"))){ |
| 769 | 813 | case 0: diffFlags |= DIFF_LINENO; break; |
| 770 | 814 | default: diffFlags |= DIFF_SIDEBYSIDE; |
| @@ -792,26 +836,33 @@ | ||
| 792 | 836 | /* |
| 793 | 837 | ** Ajax route handler for /wikiajax/preview. |
| 794 | 838 | ** |
| 795 | 839 | ** URL params: |
| 796 | 840 | ** |
| 841 | +** page = wiki page name. This is only needed for authorization | |
| 842 | +** checking. | |
| 797 | 843 | ** mimetype = the wiki page mimetype (determines rendering style) |
| 798 | 844 | ** content = the wiki page content |
| 799 | 845 | */ |
| 800 | 846 | static void wiki_ajax_route_preview(void){ |
| 801 | - Blob content = empty_blob; | |
| 802 | - const char * zMimetype = PD("mimetype","text/x-fossil-wiki"); | |
| 847 | + const char * zPageName = P("page"); | |
| 803 | 848 | const char * zContent = P("content"); |
| 804 | 849 | |
| 805 | - if( zContent==0 ){ | |
| 850 | + if(!wiki_ajax_can_write(zPageName, 0)){ | |
| 851 | + return; | |
| 852 | + }else if( zContent==0 ){ | |
| 806 | 853 | ajax_route_error(400,"Missing content to preview."); |
| 807 | 854 | return; |
| 855 | + }else{ | |
| 856 | + Blob content = empty_blob; | |
| 857 | + const char * zMimetype = PD("mimetype","text/x-fossil-wiki"); | |
| 858 | + | |
| 859 | + blob_init(&content, zContent, -1); | |
| 860 | + cgi_set_content_type("text/html"); | |
| 861 | + wiki_render_by_mimetype(&content, zMimetype); | |
| 862 | + blob_reset(&content); | |
| 808 | 863 | } |
| 809 | - blob_init(&content, zContent, -1); | |
| 810 | - cgi_set_content_type("text/html"); | |
| 811 | - wiki_render_by_mimetype(&content, zMimetype); | |
| 812 | - blob_reset(&content); | |
| 813 | 864 | } |
| 814 | 865 | |
| 815 | 866 | /* |
| 816 | 867 | ** Ajax route handler for /wikiajax/list. |
| 817 | 868 | ** |
| @@ -855,16 +906,16 @@ | ||
| 855 | 906 | const AjaxRoute routes[] = { |
| 856 | 907 | /* Keep these sorted by zName (for bsearch()) */ |
| 857 | 908 | {"diff", wiki_ajax_route_diff, 1, 1}, |
| 858 | 909 | {"fetch", wiki_ajax_route_fetch, 0, 0}, |
| 859 | 910 | {"list", wiki_ajax_route_list, 0, 0}, |
| 860 | - {"preview", wiki_ajax_route_preview, 1, 1} | |
| 861 | - /* /preview access mode: whether or not wiki-write mode is | |
| 862 | - needed really depends on multiple factors. e.g. the sandbox | |
| 863 | - page does not normally require more than anonymous access. | |
| 864 | - TODO: set its write-mode to false and do the check manually | |
| 865 | - in that route's handler. | |
| 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. | |
| 866 | 917 | */ |
| 867 | 918 | }; |
| 868 | 919 | |
| 869 | 920 | if(zName==0 || zName[0]==0){ |
| 870 | 921 | ajax_route_error(400,"Missing required [route] 'name' parameter."); |
| @@ -892,39 +943,44 @@ | ||
| 892 | 943 | pRoute->xCallback(); |
| 893 | 944 | } |
| 894 | 945 | |
| 895 | 946 | /* |
| 896 | 947 | ** Main front-end for the Ajax-based wiki editor app. |
| 948 | +** | |
| 949 | +** Optional URL arguments: | |
| 950 | +** | |
| 951 | +** name = wiki page name. Note that Ajax-based "v2" APIs use "page" | |
| 952 | +** instead because "name" has a special meaning for fossil. | |
| 953 | +** | |
| 954 | +** mimetype=fossil-standard mimetype. This is typically only passed in | |
| 955 | +** via the new-page process. | |
| 897 | 956 | */ |
| 898 | 957 | static void wikiedit_page_v2(void){ |
| 899 | 958 | const char *zPageName; |
| 900 | - Blob endScript = empty_blob; /* end-of-page JS code */; | |
| 959 | + const char * zMimetype = P("mimetype"); | |
| 901 | 960 | int isSandbox; |
| 961 | + int found = 0; | |
| 902 | 962 | |
| 903 | 963 | login_check_credentials(); |
| 904 | 964 | zPageName = PD("name",""); |
| 905 | - /* TODO: not require a page name, and instead offer a list | |
| 906 | - of pages. */ | |
| 907 | - /*TODO: check name only for case of new page: | |
| 908 | - if( check_name(zPageName) ) return;*/ | |
| 965 | + if(zPageName && *zPageName){ | |
| 966 | + if( check_name(zPageName) ) return; | |
| 967 | + } | |
| 909 | 968 | isSandbox = is_sandbox(zPageName); |
| 910 | 969 | if( isSandbox ){ |
| 911 | 970 | if( !g.perm.WrWiki ){ |
| 912 | 971 | login_needed(g.anon.WrWiki); |
| 913 | 972 | return; |
| 914 | 973 | } |
| 974 | + found = 1; | |
| 915 | 975 | }else if( zPageName!=0 ){ |
| 916 | 976 | int rid = 0; |
| 917 | - int found = 0; | |
| 918 | 977 | if( !wiki_special_permission(zPageName) ){ |
| 919 | 978 | login_needed(0); |
| 920 | 979 | return; |
| 921 | 980 | } |
| 922 | 981 | found = wiki_fetch_by_name(zPageName, 0, &rid, 0); |
| 923 | - if( !found ){ | |
| 924 | - /* TODO: set up for a new page */ | |
| 925 | - } | |
| 926 | 982 | if( (rid && !g.perm.WrWiki) || (!rid && !g.perm.NewWiki) ){ |
| 927 | 983 | login_needed(rid ? g.anon.WrWiki : g.anon.NewWiki); |
| 928 | 984 | return; |
| 929 | 985 | } |
| 930 | 986 | } |
| @@ -1039,16 +1095,23 @@ | ||
| 1039 | 1095 | "Diffs will be shown here." |
| 1040 | 1096 | "</div>"); |
| 1041 | 1097 | CX("</div>"/*#wikiedit-tab-diff*/); |
| 1042 | 1098 | } |
| 1043 | 1099 | |
| 1044 | - if(zPageName && *zPageName){ | |
| 1045 | - /* Dynamically populate the editor... */ | |
| 1046 | - blob_appendf(&endScript, "fossil.onPageLoad(function(){"); | |
| 1047 | - blob_appendf(&endScript, "fossil.page.loadPage(%!j);", | |
| 1048 | - zPageName); | |
| 1049 | - blob_appendf(&endScript, "});\n"); | |
| 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>Figure out how to integrate new/unusaved files into " | |
| 1109 | + "the UI</li>"); | |
| 1110 | + /*CX("<li></li>");*/ | |
| 1111 | + CX("</ul>"); | |
| 1112 | + CX("</div>"); | |
| 1050 | 1113 | } |
| 1051 | 1114 | |
| 1052 | 1115 | style_emit_script_fossil_bootstrap(0); |
| 1053 | 1116 | append_diff_javascript(1); |
| 1054 | 1117 | style_emit_script_fetch(0); |
| @@ -1055,22 +1118,39 @@ | ||
| 1055 | 1118 | style_emit_script_tabs(0)/*also emits fossil.dom*/; |
| 1056 | 1119 | style_emit_script_confirmer(0); |
| 1057 | 1120 | style_emit_script_builtin(0, "fossil.storage.js"); |
| 1058 | 1121 | style_emit_script_builtin(0, "fossil.page.wikiedit.js"); |
| 1059 | 1122 | |
| 1060 | - if(blob_size(&endScript)>0){ | |
| 1061 | - style_emit_script_tag(0,0); | |
| 1062 | - CX("\n(function(){\n"); | |
| 1063 | - CX("try{\n%b}\n" | |
| 1064 | - "catch(e){" | |
| 1065 | - "fossil.error(e); console.error('Exception:',e);" | |
| 1066 | - "}\n", | |
| 1067 | - &endScript); | |
| 1068 | - CX("})();"); | |
| 1069 | - style_emit_script_tag(1,0); | |
| 1070 | - } | |
| 1071 | - blob_reset(&endScript); | |
| 1123 | + /* Dynamically populate the editor... */ | |
| 1124 | + style_emit_script_tag(0,0); | |
| 1125 | + CX("\nfossil.onPageLoad(function(){\n"); | |
| 1126 | + CX("try{\n"); | |
| 1127 | + if(zMimetype && *zMimetype){ | |
| 1128 | + CX("fossil.page.selectMimetype(%!j);\n", | |
| 1129 | + zMimetype); | |
| 1130 | + } | |
| 1131 | + if(found){ | |
| 1132 | + CX("fossil.page.loadPage(%!j);\n", zPageName); | |
| 1133 | + }else if(zPageName && *zPageName){ | |
| 1134 | + /* | |
| 1135 | + FIXME: we don't yet have a good way to integrate a | |
| 1136 | + not-yet-existing/new/unsaved page into the UI. | |
| 1137 | + */ | |
| 1138 | + CX("fossil.page.config.newPage = {" | |
| 1139 | + "\"name\": %!j, \"mimetype\": %!j" | |
| 1140 | + "};\n", | |
| 1141 | + zPageName, | |
| 1142 | + zMimetype ? zMimetype : "text/x-fossil-wiki"); | |
| 1143 | + CX("fossil.error('You are editing a new, " | |
| 1144 | + "unsaved page:',%!j);\n", | |
| 1145 | + zPageName); | |
| 1146 | + } | |
| 1147 | + CX("}catch(e){" | |
| 1148 | + "fossil.error(e); console.error('Exception:',e);" | |
| 1149 | + "}\n"); | |
| 1150 | + CX("});\n"/*fossil.onPageLoad()*/); | |
| 1151 | + style_emit_script_tag(1,0); | |
| 1072 | 1152 | style_footer(); |
| 1073 | 1153 | } |
| 1074 | 1154 | |
| 1075 | 1155 | /* |
| 1076 | 1156 | ** WEBPAGE: wikiedit |
| 1077 | 1157 |
| --- src/wiki.c | |
| +++ src/wiki.c | |
| @@ -680,10 +680,49 @@ | |
| 680 | } |
| 681 | *ppWiki = pWiki; |
| 682 | } |
| 683 | return 1; |
| 684 | } |
| 685 | |
| 686 | /* |
| 687 | ** Ajax route handler for /wikiajax/fetch. |
| 688 | ** |
| 689 | ** URL params: |
| @@ -695,11 +734,11 @@ | |
| 695 | ** |
| 696 | ** { name: "page name", |
| 697 | ** type: "normal" | "tag" | "checkin" | "branch" | "sandbox", |
| 698 | ** mimetype: "mime type", |
| 699 | ** version: UUID string or null for a sandbox page, |
| 700 | ** parent: "parent uuid" or null, |
| 701 | ** content: "page content" |
| 702 | ** } |
| 703 | */ |
| 704 | static void wiki_ajax_route_fetch(void){ |
| 705 | const char * zPageName = P("page"); |
| @@ -751,10 +790,13 @@ | |
| 751 | ** |
| 752 | ** URL params: |
| 753 | ** |
| 754 | ** page = the wiki page name |
| 755 | ** content = the new/edited wiki page content |
| 756 | */ |
| 757 | static void wiki_ajax_route_diff(void){ |
| 758 | const char * zPageName = P("page"); |
| 759 | Blob contentNew = empty_blob, contentOrig = empty_blob; |
| 760 | Manifest * pParent = 0; |
| @@ -761,10 +803,12 @@ | |
| 761 | const char * zContent = P("content"); |
| 762 | u64 diffFlags = DIFF_HTML | DIFF_NOTTOOBIG | DIFF_STRIP_EOLCR; |
| 763 | |
| 764 | if( zPageName==0 || zPageName[0]==0 ){ |
| 765 | ajax_route_error(400,"Missing page name."); |
| 766 | return; |
| 767 | } |
| 768 | switch(atoi(PD("sbs","0"))){ |
| 769 | case 0: diffFlags |= DIFF_LINENO; break; |
| 770 | default: diffFlags |= DIFF_SIDEBYSIDE; |
| @@ -792,26 +836,33 @@ | |
| 792 | /* |
| 793 | ** Ajax route handler for /wikiajax/preview. |
| 794 | ** |
| 795 | ** URL params: |
| 796 | ** |
| 797 | ** mimetype = the wiki page mimetype (determines rendering style) |
| 798 | ** content = the wiki page content |
| 799 | */ |
| 800 | static void wiki_ajax_route_preview(void){ |
| 801 | Blob content = empty_blob; |
| 802 | const char * zMimetype = PD("mimetype","text/x-fossil-wiki"); |
| 803 | const char * zContent = P("content"); |
| 804 | |
| 805 | if( zContent==0 ){ |
| 806 | ajax_route_error(400,"Missing content to preview."); |
| 807 | return; |
| 808 | } |
| 809 | blob_init(&content, zContent, -1); |
| 810 | cgi_set_content_type("text/html"); |
| 811 | wiki_render_by_mimetype(&content, zMimetype); |
| 812 | blob_reset(&content); |
| 813 | } |
| 814 | |
| 815 | /* |
| 816 | ** Ajax route handler for /wikiajax/list. |
| 817 | ** |
| @@ -855,16 +906,16 @@ | |
| 855 | const AjaxRoute routes[] = { |
| 856 | /* Keep these sorted by zName (for bsearch()) */ |
| 857 | {"diff", wiki_ajax_route_diff, 1, 1}, |
| 858 | {"fetch", wiki_ajax_route_fetch, 0, 0}, |
| 859 | {"list", wiki_ajax_route_list, 0, 0}, |
| 860 | {"preview", wiki_ajax_route_preview, 1, 1} |
| 861 | /* /preview access mode: whether or not wiki-write mode is |
| 862 | needed really depends on multiple factors. e.g. the sandbox |
| 863 | page does not normally require more than anonymous access. |
| 864 | TODO: set its write-mode to false and do the check manually |
| 865 | in that route's handler. |
| 866 | */ |
| 867 | }; |
| 868 | |
| 869 | if(zName==0 || zName[0]==0){ |
| 870 | ajax_route_error(400,"Missing required [route] 'name' parameter."); |
| @@ -892,39 +943,44 @@ | |
| 892 | pRoute->xCallback(); |
| 893 | } |
| 894 | |
| 895 | /* |
| 896 | ** Main front-end for the Ajax-based wiki editor app. |
| 897 | */ |
| 898 | static void wikiedit_page_v2(void){ |
| 899 | const char *zPageName; |
| 900 | Blob endScript = empty_blob; /* end-of-page JS code */; |
| 901 | int isSandbox; |
| 902 | |
| 903 | login_check_credentials(); |
| 904 | zPageName = PD("name",""); |
| 905 | /* TODO: not require a page name, and instead offer a list |
| 906 | of pages. */ |
| 907 | /*TODO: check name only for case of new page: |
| 908 | if( check_name(zPageName) ) return;*/ |
| 909 | isSandbox = is_sandbox(zPageName); |
| 910 | if( isSandbox ){ |
| 911 | if( !g.perm.WrWiki ){ |
| 912 | login_needed(g.anon.WrWiki); |
| 913 | return; |
| 914 | } |
| 915 | }else if( zPageName!=0 ){ |
| 916 | int rid = 0; |
| 917 | int found = 0; |
| 918 | if( !wiki_special_permission(zPageName) ){ |
| 919 | login_needed(0); |
| 920 | return; |
| 921 | } |
| 922 | found = wiki_fetch_by_name(zPageName, 0, &rid, 0); |
| 923 | if( !found ){ |
| 924 | /* TODO: set up for a new page */ |
| 925 | } |
| 926 | if( (rid && !g.perm.WrWiki) || (!rid && !g.perm.NewWiki) ){ |
| 927 | login_needed(rid ? g.anon.WrWiki : g.anon.NewWiki); |
| 928 | return; |
| 929 | } |
| 930 | } |
| @@ -1039,16 +1095,23 @@ | |
| 1039 | "Diffs will be shown here." |
| 1040 | "</div>"); |
| 1041 | CX("</div>"/*#wikiedit-tab-diff*/); |
| 1042 | } |
| 1043 | |
| 1044 | if(zPageName && *zPageName){ |
| 1045 | /* Dynamically populate the editor... */ |
| 1046 | blob_appendf(&endScript, "fossil.onPageLoad(function(){"); |
| 1047 | blob_appendf(&endScript, "fossil.page.loadPage(%!j);", |
| 1048 | zPageName); |
| 1049 | blob_appendf(&endScript, "});\n"); |
| 1050 | } |
| 1051 | |
| 1052 | style_emit_script_fossil_bootstrap(0); |
| 1053 | append_diff_javascript(1); |
| 1054 | style_emit_script_fetch(0); |
| @@ -1055,22 +1118,39 @@ | |
| 1055 | style_emit_script_tabs(0)/*also emits fossil.dom*/; |
| 1056 | style_emit_script_confirmer(0); |
| 1057 | style_emit_script_builtin(0, "fossil.storage.js"); |
| 1058 | style_emit_script_builtin(0, "fossil.page.wikiedit.js"); |
| 1059 | |
| 1060 | if(blob_size(&endScript)>0){ |
| 1061 | style_emit_script_tag(0,0); |
| 1062 | CX("\n(function(){\n"); |
| 1063 | CX("try{\n%b}\n" |
| 1064 | "catch(e){" |
| 1065 | "fossil.error(e); console.error('Exception:',e);" |
| 1066 | "}\n", |
| 1067 | &endScript); |
| 1068 | CX("})();"); |
| 1069 | style_emit_script_tag(1,0); |
| 1070 | } |
| 1071 | blob_reset(&endScript); |
| 1072 | style_footer(); |
| 1073 | } |
| 1074 | |
| 1075 | /* |
| 1076 | ** WEBPAGE: wikiedit |
| 1077 |
| --- src/wiki.c | |
| +++ src/wiki.c | |
| @@ -680,10 +680,49 @@ | |
| 680 | } |
| 681 | *ppWiki = pWiki; |
| 682 | } |
| 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: |
| @@ -695,11 +734,11 @@ | |
| 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"); |
| @@ -751,10 +790,13 @@ | |
| 790 | ** |
| 791 | ** URL params: |
| 792 | ** |
| 793 | ** page = the wiki page name |
| 794 | ** content = the new/edited wiki page content |
| 795 | ** |
| 796 | ** Requires that the user have write access solely to avoid some |
| 797 | ** potential abuse cases. It does not actually write anything. |
| 798 | */ |
| 799 | static void wiki_ajax_route_diff(void){ |
| 800 | const char * zPageName = P("page"); |
| 801 | Blob contentNew = empty_blob, contentOrig = empty_blob; |
| 802 | Manifest * pParent = 0; |
| @@ -761,10 +803,12 @@ | |
| 803 | const char * zContent = P("content"); |
| 804 | u64 diffFlags = DIFF_HTML | DIFF_NOTTOOBIG | DIFF_STRIP_EOLCR; |
| 805 | |
| 806 | if( zPageName==0 || zPageName[0]==0 ){ |
| 807 | ajax_route_error(400,"Missing page name."); |
| 808 | return; |
| 809 | }else if(!wiki_ajax_can_write(zPageName, 0)){ |
| 810 | return; |
| 811 | } |
| 812 | switch(atoi(PD("sbs","0"))){ |
| 813 | case 0: diffFlags |= DIFF_LINENO; break; |
| 814 | default: diffFlags |= DIFF_SIDEBYSIDE; |
| @@ -792,26 +836,33 @@ | |
| 836 | /* |
| 837 | ** Ajax route handler for /wikiajax/preview. |
| 838 | ** |
| 839 | ** URL params: |
| 840 | ** |
| 841 | ** page = wiki page name. This is only needed for authorization |
| 842 | ** checking. |
| 843 | ** mimetype = the wiki page mimetype (determines rendering style) |
| 844 | ** content = the wiki page content |
| 845 | */ |
| 846 | static void wiki_ajax_route_preview(void){ |
| 847 | const char * zPageName = P("page"); |
| 848 | const char * zContent = P("content"); |
| 849 | |
| 850 | if(!wiki_ajax_can_write(zPageName, 0)){ |
| 851 | return; |
| 852 | }else if( zContent==0 ){ |
| 853 | ajax_route_error(400,"Missing content to preview."); |
| 854 | return; |
| 855 | }else{ |
| 856 | Blob content = empty_blob; |
| 857 | const char * zMimetype = PD("mimetype","text/x-fossil-wiki"); |
| 858 | |
| 859 | blob_init(&content, zContent, -1); |
| 860 | cgi_set_content_type("text/html"); |
| 861 | wiki_render_by_mimetype(&content, zMimetype); |
| 862 | blob_reset(&content); |
| 863 | } |
| 864 | } |
| 865 | |
| 866 | /* |
| 867 | ** Ajax route handler for /wikiajax/list. |
| 868 | ** |
| @@ -855,16 +906,16 @@ | |
| 906 | const AjaxRoute routes[] = { |
| 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."); |
| @@ -892,39 +943,44 @@ | |
| 943 | pRoute->xCallback(); |
| 944 | } |
| 945 | |
| 946 | /* |
| 947 | ** Main front-end for the Ajax-based wiki editor app. |
| 948 | ** |
| 949 | ** Optional URL arguments: |
| 950 | ** |
| 951 | ** name = wiki page name. Note that Ajax-based "v2" APIs use "page" |
| 952 | ** instead because "name" has a special meaning for fossil. |
| 953 | ** |
| 954 | ** mimetype=fossil-standard mimetype. This is typically only passed in |
| 955 | ** via the new-page process. |
| 956 | */ |
| 957 | static void wikiedit_page_v2(void){ |
| 958 | const char *zPageName; |
| 959 | const char * zMimetype = P("mimetype"); |
| 960 | int isSandbox; |
| 961 | int found = 0; |
| 962 | |
| 963 | login_check_credentials(); |
| 964 | zPageName = PD("name",""); |
| 965 | if(zPageName && *zPageName){ |
| 966 | if( check_name(zPageName) ) return; |
| 967 | } |
| 968 | isSandbox = is_sandbox(zPageName); |
| 969 | if( isSandbox ){ |
| 970 | if( !g.perm.WrWiki ){ |
| 971 | login_needed(g.anon.WrWiki); |
| 972 | return; |
| 973 | } |
| 974 | found = 1; |
| 975 | }else if( zPageName!=0 ){ |
| 976 | int rid = 0; |
| 977 | if( !wiki_special_permission(zPageName) ){ |
| 978 | login_needed(0); |
| 979 | return; |
| 980 | } |
| 981 | found = wiki_fetch_by_name(zPageName, 0, &rid, 0); |
| 982 | if( (rid && !g.perm.WrWiki) || (!rid && !g.perm.NewWiki) ){ |
| 983 | login_needed(rid ? g.anon.WrWiki : g.anon.NewWiki); |
| 984 | return; |
| 985 | } |
| 986 | } |
| @@ -1039,16 +1095,23 @@ | |
| 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>Figure out how to integrate new/unusaved files into " |
| 1109 | "the UI</li>"); |
| 1110 | /*CX("<li></li>");*/ |
| 1111 | CX("</ul>"); |
| 1112 | CX("</div>"); |
| 1113 | } |
| 1114 | |
| 1115 | style_emit_script_fossil_bootstrap(0); |
| 1116 | append_diff_javascript(1); |
| 1117 | style_emit_script_fetch(0); |
| @@ -1055,22 +1118,39 @@ | |
| 1118 | style_emit_script_tabs(0)/*also emits fossil.dom*/; |
| 1119 | style_emit_script_confirmer(0); |
| 1120 | style_emit_script_builtin(0, "fossil.storage.js"); |
| 1121 | style_emit_script_builtin(0, "fossil.page.wikiedit.js"); |
| 1122 | |
| 1123 | /* Dynamically populate the editor... */ |
| 1124 | style_emit_script_tag(0,0); |
| 1125 | CX("\nfossil.onPageLoad(function(){\n"); |
| 1126 | CX("try{\n"); |
| 1127 | if(zMimetype && *zMimetype){ |
| 1128 | CX("fossil.page.selectMimetype(%!j);\n", |
| 1129 | zMimetype); |
| 1130 | } |
| 1131 | if(found){ |
| 1132 | CX("fossil.page.loadPage(%!j);\n", zPageName); |
| 1133 | }else if(zPageName && *zPageName){ |
| 1134 | /* |
| 1135 | FIXME: we don't yet have a good way to integrate a |
| 1136 | not-yet-existing/new/unsaved page into the UI. |
| 1137 | */ |
| 1138 | CX("fossil.page.config.newPage = {" |
| 1139 | "\"name\": %!j, \"mimetype\": %!j" |
| 1140 | "};\n", |
| 1141 | zPageName, |
| 1142 | zMimetype ? zMimetype : "text/x-fossil-wiki"); |
| 1143 | CX("fossil.error('You are editing a new, " |
| 1144 | "unsaved page:',%!j);\n", |
| 1145 | zPageName); |
| 1146 | } |
| 1147 | CX("}catch(e){" |
| 1148 | "fossil.error(e); console.error('Exception:',e);" |
| 1149 | "}\n"); |
| 1150 | CX("});\n"/*fossil.onPageLoad()*/); |
| 1151 | style_emit_script_tag(1,0); |
| 1152 | style_footer(); |
| 1153 | } |
| 1154 | |
| 1155 | /* |
| 1156 | ** WEBPAGE: wikiedit |
| 1157 |