├── templates ├── html_standard_intro ├── fail_plaintext ├── redirect ├── fail ├── index ├── not_yet_a_page ├── html_footer ├── auth_error ├── action_on_user_complete ├── delete_confirmation ├── view ├── action_on_user_confirmation ├── html_startcontent ├── pushed_to_main ├── search ├── made_pull_request ├── added_to_pull_request ├── edit_existing_pull_request ├── html_header └── edit ├── .gitignore ├── static_files ├── link.svg ├── ghwikipp.css └── pandoc.css ├── LICENSE.txt ├── pandoc-filter.lua ├── config.php.sample ├── pandoc-filter-offline.lua ├── make_offline_archive.php ├── README.md └── index.php /templates/html_standard_intro: -------------------------------------------------------------------------------- 1 | @include html_header@ 2 | @include html_startcontent@ 3 | -------------------------------------------------------------------------------- /templates/fail_plaintext: -------------------------------------------------------------------------------- 1 | 2 | @title@ 3 | 4 | @httpresponse@ 5 | 6 | @message@ 7 | 8 | -------------------------------------------------------------------------------- /templates/redirect: -------------------------------------------------------------------------------- 1 | @include html_standard_intro@ 2 | 3 | @message@ 4 | 5 | @include html_footer@ 6 | 7 | 8 | -------------------------------------------------------------------------------- /templates/fail: -------------------------------------------------------------------------------- 1 | @include html_standard_intro@ 2 | 3 |

@httpresponse@

4 | 5 |

@message@

6 | 7 | @include html_footer@ 8 | 9 | -------------------------------------------------------------------------------- /templates/index: -------------------------------------------------------------------------------- 1 | @include html_standard_intro@ 2 | 3 |

Index of all pages

4 | 5 |

6 | 7 | @include html_footer@ 8 | 9 | -------------------------------------------------------------------------------- /templates/not_yet_a_page: -------------------------------------------------------------------------------- 1 | @include html_standard_intro@ 2 | 3 |

@page@

4 | 5 |

No such page '@page@' yet.

6 |

Click here to create it!

7 | 8 | @include html_footer@ 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cooked 2 | raw 3 | trusted 4 | admins 5 | blocked 6 | config.php 7 | git_repo_lock 8 | tmp_commit_message.txt 9 | id_ed25519 10 | id_ed25519.pub 11 | offline 12 | static_files/logo.png 13 | static_files/favicon.ico 14 | static_files/offline 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /templates/html_footer: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

All wiki content is licensed under @wikilicense@.
5 | Wiki powered by ghwikipp.

6 |
7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /templates/auth_error: -------------------------------------------------------------------------------- 1 | @include html_standard_intro@ 2 | 3 |

@httpresponse@

4 | 5 |

We can't go on unless you authorize on GitHub first, sorry.

6 |

GitHub error: @error@

7 |

GitHub reason: @reason@

8 | 9 |

Click here to go home.

10 | 11 | @include html_footer@ 12 | 13 | 14 | -------------------------------------------------------------------------------- /static_files/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/action_on_user_complete: -------------------------------------------------------------------------------- 1 | @include html_standard_intro@ 2 | 3 |

@username@, @@user@ , has been @action@.

4 | 5 |

If this was in error, you can reverse it here.

6 | 7 |

Or just go home instead.

8 | 9 | @include html_footer@ 10 | 11 | -------------------------------------------------------------------------------- /templates/delete_confirmation: -------------------------------------------------------------------------------- 1 | @include html_standard_intro@ 2 | 3 |

Delete @page@?

4 |

Are you sure you want to delete this page?

5 |

6 | 7 | 8 | [ Cancel ] 9 |
10 |

11 | 12 | @include html_footer@ 13 | 14 | -------------------------------------------------------------------------------- /templates/view: -------------------------------------------------------------------------------- 1 | @include html_header@ 2 | 3 | @include html_startcontent@ 4 | 5 | @preamble@ 6 | @cooked@ 7 | 8 |
9 |
10 | [ 11 | edit 12 | | 13 | delete 14 | | 15 | history 16 | | 17 | feedback 18 | | 19 | raw 20 | ] 21 |
22 | 23 | @include html_footer@ 24 | 25 | -------------------------------------------------------------------------------- /templates/action_on_user_confirmation: -------------------------------------------------------------------------------- 1 | @include html_standard_intro@ 2 | 3 |

@username@, @@user@ , is currently @currently@.

4 | 5 |

Would you like to @action@ this user? @explanation@

6 | 7 |

8 | 9 |

10 | 11 |

Or just go home instead.

12 | 13 | @include html_footer@ 14 | 15 | -------------------------------------------------------------------------------- /templates/html_startcontent: -------------------------------------------------------------------------------- 1 | 2 |
3 |
@wikiname@
4 |
5 | [ 6 | front page 7 | | 8 | index 9 | | 10 | search 11 | | 12 | recent changes 13 | | 14 | git repo 15 | | 16 | offline html 17 | ] 18 |
19 |
20 | -------------------------------------------------------------------------------- /templates/pushed_to_main: -------------------------------------------------------------------------------- 1 | @include html_header@ 2 | 3 | @include html_startcontent@ 4 | 5 |

Thank you for your edit!

6 | 7 |

You are a trusted contributor, so your changes 8 | went right into the wiki 9 | without the need for peer review. Thank you for 10 | editing responsibly!

11 | 12 |

Below is the current state of the page, with your edits.

13 | 14 |

Edit again?

15 | 16 |
17 | 18 | @cooked@ 19 | 20 | @include html_footer@ 21 | 22 | 23 | -------------------------------------------------------------------------------- /templates/search: -------------------------------------------------------------------------------- 1 | @include html_standard_intro@ 2 | 3 |

Search

4 |

5 |

6 | 7 | 8 |
9 |

10 | 11 |
12 |
13 |

14 | [ 15 | permalink 16 | | 17 | try on duckduckgo 18 | | 19 | try on google 20 | ] 21 |

22 |

25 |
26 | 27 | @include html_footer@ 28 | 29 | -------------------------------------------------------------------------------- /templates/made_pull_request: -------------------------------------------------------------------------------- 1 | @include html_header@ 2 | 3 | @include html_startcontent@ 4 | 5 |

@page@

6 | 7 |

Thank you for your edit!

8 | 9 |

Your changes have gone into a pull request, where they will be examined by 10 | a human and then approved or rejected.

11 | 12 |

You can check on that process at the pull request page.

13 | 14 |

After a few sucessful pull requests, the admins will probably mark your account 15 | as "trusted," and then your changes will go onto the wiki automatically, without an 16 | approval process!

17 | 18 |

Thanks!

19 | 20 |

Back to the (changes-not-yet-approved) page.

21 | 22 |

Below is what your change would look like, if approved.

23 | 24 |
25 | 26 | @cooked@ 27 | 28 | @include html_footer@ 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2021-2023 Ryan C. Gordon 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software 13 | in a product, an acknowledgment in the product documentation would be 14 | appreciated but is not required. 15 | 2. Altered source versions must be plainly marked as such, and must not be 16 | misrepresented as being the original software. 17 | 3. This notice may not be removed or altered from any source distribution. 18 | 19 | -------------------------------------------------------------------------------- /templates/added_to_pull_request: -------------------------------------------------------------------------------- 1 | @include html_header@ 2 | 3 | @include html_startcontent@ 4 | 5 |

@page@

6 | 7 |

Thank you for your edit!

8 | 9 |

Your changes have been added to your existing pull request, where they will be 10 | examined by a human and then approved or rejected.

11 | 12 |

You can check on your accumulated changes, and view the Pull Request, on GitHub.

13 | 14 |

After a few sucessful pull requests, the admins will probably mark your account 15 | as "trusted," and then your changes will go onto the wiki automatically, without an 16 | approval process!

17 | 18 |

Thanks!

19 | 20 |

Back to the (changes-not-yet-approved) page.

21 | 22 |

Below is what your change would look like, if approved.

23 | 24 |
25 | 26 | @cooked@ 27 | 28 | @include html_footer@ 29 | 30 | -------------------------------------------------------------------------------- /templates/edit_existing_pull_request: -------------------------------------------------------------------------------- 1 | @include html_standard_intro@ 2 | 3 |

4 |

5 | Logged in as @github_name@ 6 | Editing @page@ 7 |

8 |

9 | 10 |

You seem to have a Pull Request for this page already.

11 | 12 |

Any further edits will be added to that same PR until it is closed.

13 | 14 |

Here are changes already made.

15 | 16 |

Would you like to start editing from your last changes, or the current contents of the page?

17 |

18 | 19 | 20 | [ Cancel ] 21 |
22 |

23 | 24 | @include html_footer@ 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /templates/html_header: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | @title@ 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /pandoc-filter.lua: -------------------------------------------------------------------------------- 1 | function Link (link) 2 | local absolute_path = link.target:sub(1,1) == '/' 3 | local external_url = not absolute_path and link.target:find("%a+://") == 1 4 | local isInternalSection = link.target:sub(1,1) == '#' 5 | if isInternalSection then 6 | link.target = link.target:lower() 7 | end 8 | 9 | -- drop any Markdown or MediaWiki file extensions that might have snuck in. 10 | if not external_url then 11 | link.target = string.gsub(link.target, '%.md$', '') 12 | link.target = string.gsub(link.target, '%.mediawiki$', '') 13 | end 14 | 15 | -- !!! FIXME: this doesn't work with subdirs, figure out why this was like this at all. 16 | -- If it's not an absolute path, not an external URL, and not a section link, make it absolute. 17 | --if not absolute_path and not external_url and not isInternalSection then 18 | -- link.target = '/' .. link.target 19 | --end 20 | return link 21 | end 22 | 23 | function Header(header) 24 | if header.level >= 2 then 25 | local returnHeader = header 26 | returnHeader.attributes['class'] = 'anchorText' 27 | local svg = pandoc.Image('', '/static_files/link.svg') 28 | svg.attributes['class'] = 'anchorImage' 29 | svg.attributes['width'] = '16' 30 | svg.attributes['height'] = '16' 31 | 32 | local link = pandoc.Link('', '#' .. returnHeader.identifier) 33 | local linkContents = pandoc.List(link.content) 34 | linkContents:insert(#linkContents + 1, svg) 35 | 36 | local content = pandoc.List(returnHeader.content) 37 | content:insert(#content + 1, link) 38 | 39 | returnHeader.content = content 40 | return returnHeader 41 | end 42 | return header 43 | end 44 | -------------------------------------------------------------------------------- /static_files/ghwikipp.css: -------------------------------------------------------------------------------- 1 | :root { 2 | color-scheme: dark light; /* both supported */ 3 | } 4 | 5 | body { 6 | background-color: white; 7 | padding: 2vw; 8 | color: #333; 9 | max-width: 1200px; 10 | margin: 0 auto; 11 | font-size: 16px; 12 | line-height: 1.5; 13 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", 14 | Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; 15 | overflow-wrap: break-word; 16 | } 17 | 18 | a { 19 | color: #0969da; 20 | /* text-decoration: none; */ 21 | } 22 | 23 | a:visited { 24 | color: #064998; 25 | } 26 | 27 | h1 { 28 | border-bottom: 2px solid #efefef; 29 | } 30 | 31 | h2 { 32 | border-bottom: 1px solid #efefef; 33 | } 34 | 35 | p { 36 | max-width: 85ch; 37 | } 38 | 39 | li { 40 | max-width: 85ch; 41 | } 42 | 43 | div.sourceCode { 44 | background-color: #f6f8fa; 45 | max-width: 100%; 46 | padding: 16px; 47 | } 48 | 49 | code { 50 | background-color: #f6f8fa; 51 | padding: 0px; 52 | font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, 53 | "Liberation Mono", monospace; 54 | } 55 | 56 | table { 57 | border: 1px solid #808080; 58 | border-collapse: collapse; 59 | } 60 | 61 | td { 62 | border: 1px solid #808080; 63 | padding: 5px; 64 | } 65 | 66 | tr:nth-child(even) { 67 | background-color: #f6f8fa; 68 | } 69 | 70 | .wikitopbanner { 71 | display: flex; 72 | justify-content: space-between; 73 | align-items: center; 74 | background-color: #efefef; 75 | padding: 10px; 76 | margin-bottom: 10px; 77 | width: auto; 78 | } 79 | 80 | .wikibottombanner { 81 | background-color: #efefef; 82 | padding: 10px; 83 | margin-top: 10px; 84 | width: auto; 85 | } 86 | 87 | .alertBox { 88 | background-color: #f8d7da; 89 | border: 1px solid #f5c6cb; 90 | max-width: 60%; 91 | padding: 10; 92 | margin: auto; 93 | } 94 | 95 | .anchorImage { 96 | visibility: hidden; 97 | padding-left: 0.2em; 98 | color: #fff; 99 | } 100 | 101 | .anchorText:hover .anchorImage { 102 | visibility: visible; 103 | } 104 | 105 | hr { 106 | display: block; 107 | height: 1px; 108 | border: 0; 109 | border-top: 1px solid #efefef; 110 | margin: 1em 0; 111 | padding: 0; 112 | } 113 | 114 | /* Text and background color for dark mode */ 115 | @media (prefers-color-scheme: dark) { 116 | body { 117 | color: #e6edf3; 118 | background-color: #0d1117; 119 | } 120 | 121 | h1 { 122 | border-color: rgba(48, 54, 61, 0.7); 123 | } 124 | 125 | h2 { 126 | border-color: rgba(48, 54, 61, 0.7); 127 | } 128 | 129 | hr { 130 | border-color: rgba(48, 54, 61, 0.7); 131 | } 132 | 133 | div.sourceCode { 134 | background-color: #161b22; 135 | } 136 | 137 | code { 138 | background-color: #161b22; 139 | } 140 | 141 | a { 142 | color: #4493f8; 143 | } 144 | 145 | a:visited { 146 | color: #2f66ad; 147 | } 148 | 149 | table { 150 | border-color: rgba(48, 54, 61, 0.7); 151 | } 152 | 153 | td { 154 | border-color: rgba(48, 54, 61, 0.7); 155 | } 156 | 157 | tr:nth-child(even) { 158 | background-color: #161b22; 159 | } 160 | 161 | .wikitopbanner { 162 | background-color: #263040; 163 | } 164 | 165 | .wikibottombanner { 166 | background-color: #263040; 167 | } 168 | 169 | .anchorText:hover .anchorImage { 170 | filter: invert(100%); 171 | } 172 | } 173 | 174 | @media print { 175 | body { 176 | font-size: 12px; 177 | } 178 | 179 | table { 180 | font-size: inherit; 181 | } 182 | 183 | a:visited { 184 | color: #0969da; 185 | } 186 | 187 | .wikitopbanner, 188 | .anchorText, 189 | .wikibottombanner { 190 | display: none; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /static_files/pandoc.css: -------------------------------------------------------------------------------- 1 | code{white-space: pre-wrap;} 2 | span.smallcaps{font-variant: small-caps;} 3 | span.underline{text-decoration: underline;} 4 | div.column{display: inline-block; vertical-align: top; width: 50%;} 5 | a.sourceLine { display: inline-block; line-height: 1.25; } 6 | a.sourceLine { pointer-events: none; color: inherit; text-decoration: inherit; } 7 | a.sourceLine:empty { height: 1.2em; } 8 | .sourceCode { overflow: visible; } 9 | code.sourceCode { white-space: pre; position: relative; } 10 | div.sourceCode { margin: 1em 0; } 11 | pre.sourceCode { margin: 0; } 12 | @media screen { 13 | div.sourceCode { overflow: auto; } 14 | } 15 | @media print { 16 | code.sourceCode { white-space: pre-wrap; } 17 | a.sourceLine { text-indent: -1em; padding-left: 1em; } 18 | } 19 | pre.numberSource a.sourceLine 20 | { position: relative; left: -4em; } 21 | pre.numberSource a.sourceLine::before 22 | { content: attr(title); 23 | position: relative; left: -1em; text-align: right; vertical-align: baseline; 24 | border: none; pointer-events: all; display: inline-block; 25 | -webkit-touch-callout: none; -webkit-user-select: none; 26 | -khtml-user-select: none; -moz-user-select: none; 27 | -ms-user-select: none; user-select: none; 28 | padding: 0 4px; width: 4em; 29 | color: #aaaaaa; 30 | } 31 | pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa; padding-left: 4px; } 32 | div.sourceCode 33 | { } 34 | @media screen { 35 | a.sourceLine::before { text-decoration: underline; } 36 | } 37 | 38 | code span.al { color: #ff0000; font-weight: bold; } /* Alert */ 39 | code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */ 40 | code span.at { color: #7d9029; } /* Attribute */ 41 | code span.bn { color: #40a070; } /* BaseN */ 42 | code span.bu { } /* BuiltIn */ 43 | code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */ 44 | code span.ch { color: #4070a0; } /* Char */ 45 | code span.cn { color: #880000; } /* Constant */ 46 | code span.co { color: #60a0b0; font-style: italic; } /* Comment */ 47 | code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */ 48 | code span.do { color: #ba2121; font-style: italic; } /* Documentation */ 49 | code span.dt { color: #902000; } /* DataType */ 50 | code span.dv { color: #40a070; } /* DecVal */ 51 | code span.er { color: #ff0000; font-weight: bold; } /* Error */ 52 | code span.ex { } /* Extension */ 53 | code span.fl { color: #40a070; } /* Float */ 54 | code span.fu { color: #06287e; } /* Function */ 55 | code span.im { } /* Import */ 56 | code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */ 57 | code span.kw { color: #007020; font-weight: bold; } /* Keyword */ 58 | code span.op { color: #666666; } /* Operator */ 59 | code span.ot { color: #007020; } /* Other */ 60 | code span.pp { color: #bc7a00; } /* Preprocessor */ 61 | code span.sc { color: #4070a0; } /* SpecialChar */ 62 | code span.ss { color: #bb6688; } /* SpecialString */ 63 | code span.st { color: #4070a0; } /* String */ 64 | code span.va { color: #19177c; } /* Variable */ 65 | code span.vs { color: #4070a0; } /* VerbatimString */ 66 | code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */ 67 | 68 | @media (prefers-color-scheme: dark) { 69 | code span.al { color: #95da4c; font-weight: bold; } /* Alert */ 70 | code span.an { color: #3f8058; } /* Annotation */ 71 | code span.at { color: #2980b9; } /* Attribute */ 72 | code span.bn { color: #f67400; } /* BaseN */ 73 | code span.bu { color: #7f8c8d; } /* BuiltIn */ 74 | code span.ch { color: #3daee9; } /* Char */ 75 | code span.co { color: #7a7c7d; } /* Comment */ 76 | code span.cv { color: #7f8c8d; } /* CommentVar */ 77 | code span.cn { color: #27aeae; font-weight: bold; } /* Constant */ 78 | code span.cf { color: #fdbc4b; font-weight: bold; } /* ControlFlow */ 79 | code span.dt { color: #2980b9; } /* DataType */ 80 | code span.dv { color: #f67400; } /* DecVal */ 81 | code span.do { color: #a43340; } /* Documentation */ 82 | code span.er { color: #da4453; } /* Error */ 83 | code span.ex { color: #0099ff; font-weight: bold; } /* Extension */ 84 | code span.fl { color: #f67400; } /* Float */ 85 | code span.fu { color: #8e44ad; } /* Function */ 86 | code span.im { color: #27ae60; } /* Import */ 87 | code span.in { color: #c45b00; } /* Information */ 88 | code span.kw { color: #cfcfc2; font-weight: bold; } /* Keyword */ 89 | code span.op { color: #cfcfc2; } /* Operator */ 90 | code span.ot { color: #27ae60; } /* Other */ 91 | code span.pp { color: #27ae60; } /* Preprocessor */ 92 | code span.sc { color: #3daee9; } /* SpecialChar */ 93 | code span.ss { color: #da4453; } /* SpecialString */ 94 | code span.st { color: #f44f4f; } /* String */ 95 | code span.va { color: #27aeae; } /* Variable */ 96 | code span.vs { color: #da4453; } /* VerbatimString */ 97 | code span.wa { color: #da4453; } /* Warning */ 98 | } -------------------------------------------------------------------------------- /config.php.sample: -------------------------------------------------------------------------------- 1 | 102 | -------------------------------------------------------------------------------- /pandoc-filter-offline.lua: -------------------------------------------------------------------------------- 1 | local base_url = os.getenv("GHWIKIPP_BASE_URL") 2 | local input_path = os.getenv("GHWIKIPP_INPUT_PATH") 3 | local rawdir = os.getenv("GHWIKIPP_RAW_DIR") 4 | 5 | -- !!! FIXME: this is super-gross and unix-specific. 6 | function is_dir(path) 7 | local p = io.popen("[ -d '" .. path .. "' ] && echo 'yes' || echo 'no'") 8 | local result = p:read('*l') 9 | p:close() 10 | --print("is_dir('" .. path .. "') => " .. result); 11 | return result == 'yes' 12 | end 13 | 14 | local function split_path(str, sep) 15 | local result = {} 16 | if str ~= nil then 17 | for part in string.gmatch(str, '([^' .. sep .. ']+)') do 18 | table.insert(result, part) 19 | end 20 | end 21 | local fname = result[#result] 22 | if fname == nil then 23 | fname = '' 24 | end 25 | table.remove(result) -- drop the filename from the array 26 | local nofname = '' 27 | local sep = '' 28 | for i, v in ipairs(result) do 29 | nofname = nofname .. sep .. v 30 | sep = '/' 31 | end 32 | return result, fname, nofname 33 | end 34 | 35 | local input_path_array, input_path_filename, input_path_nofilename = split_path(input_path, '/') 36 | 37 | function Link (link) 38 | local url = link.target 39 | local pos, endpos 40 | 41 | -- if there's an anchor (the '#blahblahblah' at the end of a URL), 42 | -- split it out to a separate var and remove it from the url. 43 | local anchor = '' 44 | pos, endpos = string.find(url, '#.*$') 45 | if pos ~= nil then 46 | anchor = string.sub(url, pos) 47 | url = string.sub(url, 1, pos - 1) 48 | end 49 | 50 | -- http into https on base urls. 51 | local https_url = string.gsub(url, '^http:', 'https:', 1); 52 | 53 | -- if this a full URL to somewhere else in the wiki, chop off the base so we can use it as a path on the filesystem. 54 | if https_url == base_url then 55 | url = '/FrontPage' 56 | else 57 | pos, endpos = string.find(https_url, base_url, 1, false) 58 | if pos == 1 then 59 | url = string.sub(https_url, endpos + 1) 60 | end 61 | end 62 | 63 | if url == '/' then 64 | url = '/FrontPage' 65 | end 66 | 67 | -- Most links are relative in the wiki, but occasionally we'll do "/TopLevelDirectory/Whatever" when we have to 68 | -- access a parent directory, because we can't do ".." paths on the wiki, but these don't fly 69 | -- when looking at static HTML files on the user's local disk. 70 | -- If this is an absolute path into the wiki instead of relative, convert it to a relative page. 71 | -- Note that full URLs (https://wiki.example.com/) will land in here, too, as we intentionally chop off the base_url to leave the initial '/'. 72 | if string.sub(url, 1, 1) == '/' then -- if first char is '/', it's an absolute path. 73 | local split_path_array, split_path_filename = split_path(url, '/') 74 | local dotdot = false 75 | local sep = '' 76 | local final = '' 77 | for i, v in ipairs(input_path_array) do 78 | if not dotdot then 79 | if split_path_array[1] == v then 80 | table.remove(split_path_array, 1) 81 | else 82 | dotdot = true 83 | end 84 | end 85 | if dotdot then 86 | final = final .. sep .. '..' 87 | sep = '/' 88 | end 89 | end 90 | 91 | for i, v in ipairs(split_path_array) do 92 | final = final .. sep .. v 93 | sep = '/' 94 | end 95 | 96 | url = final .. sep .. split_path_filename 97 | end 98 | 99 | if string.find(url, '^.*://') ~= 1 then -- if this is an external URL, don't mangle it. 100 | -- Is this a link to a directory or a file? 101 | local rawpath = rawdir .. '/' .. input_path_nofilename .. '/' .. url 102 | if (url ~= '') and is_dir(rawpath) then 103 | --print("rawpath: '" .. rawpath .. "'") 104 | -- it's a directory! Either link directly to a FrontPage.html, if one will exist, or just dump the user in the directory itself if not. 105 | local f = io.open(rawpath .. '/FrontPage.md', 'r') 106 | if not f then f = io.open(rawpath .. '/FrontPage.mediawiki', 'r') end 107 | if f then 108 | f:close() 109 | url = url .. '/FrontPage.html'; 110 | end 111 | else 112 | -- drop any Markdown or MediaWiki file extensions that might have snuck in. 113 | url = string.gsub(url, '%.md$', '') 114 | url = string.gsub(url, '%.mediawiki$', '') 115 | 116 | -- Add HTML file extension. 117 | if url ~= '' then -- could be '' if there's nothing but an anchor on the current page. 118 | url = url .. '.html' 119 | end 120 | end 121 | end 122 | 123 | -- Add anchor back in 124 | url = url .. anchor 125 | 126 | --print(link.target .. ' -> ' .. url) 127 | 128 | link.target = url 129 | return link 130 | end 131 | 132 | -------------------------------------------------------------------------------- /make_offline_archive.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | 'gfm', 13 | 'mediawiki' => 'mediawiki' 14 | ]; 15 | 16 | 17 | function get_max_children() 18 | { 19 | $default_kids = 4; 20 | 21 | // try to cook on all available CPU cores. 22 | // !!! FIXME: this is Linux-specific. macOS, BSD, Windows want to do other things. Send a PR. :) 23 | $shell_output = shell_exec("nproc"); 24 | return is_numeric($shell_output) ? ((int) $shell_output) : $default_kids; 25 | } 26 | 27 | $max_children = get_max_children(); 28 | $num_children = 0; 29 | 30 | //print("max_children = $max_children\n"); 31 | 32 | function cook_tree_for_offline_html($srcdir, $dstdir, $input_dir) 33 | { 34 | global $supported_formats, $max_children, $num_children, $base_url, $raw_data; 35 | 36 | //print("cooktree: '$srcdir' ('$input_dir') -> '$dstdir'\n"); 37 | $dirp = opendir($srcdir); 38 | if ($dirp === false) { 39 | return; 40 | } 41 | 42 | while (($dent = readdir($dirp)) !== false) { 43 | if (substr($dent, 0, 1) == '.') { continue; } // skip ".", "..", and metadata. 44 | //print("cookdent: '$dent'\n"); 45 | $src = "$srcdir/$dent"; 46 | $dst = "$dstdir/$dent"; 47 | $input_path = ($input_dir == NULL) ? $dent : "$input_dir/$dent"; 48 | if (is_dir($src)) { 49 | mkdir($dst); 50 | cook_tree_for_offline_html($src, $dst, $input_path); 51 | } else { 52 | $ext = strrchr($dent, '.'); 53 | if ($ext !== false) { 54 | $ext = substr($ext, 1); 55 | //print("cookext: '$ext'\n"); 56 | if (isset($supported_formats[$ext])) { 57 | $from_format = $supported_formats[$ext]; 58 | $page = preg_replace('/\..*?$/', '', $dst); 59 | $page = preg_replace('/^.*\//', '', $page); 60 | $dst = preg_replace('/\..*?$/', '.html', $dst); 61 | 62 | $env = array( 63 | 'GHWIKIPP_INPUT_PATH' => $input_path, 64 | 'GHWIKIPP_BASE_URL' => $base_url, 65 | 'GHWIKIPP_RAW_DIR' => $raw_data 66 | ); 67 | 68 | // split this across CPU cores. 69 | while ($num_children >= $max_children) { 70 | pcntl_waitpid(-1, $status); // wait for any child to end. 71 | $num_children--; 72 | } 73 | // launch another! 74 | $pid = pcntl_fork(); 75 | if ($pid == -1) { 76 | print("FAILED TO FORK!\n"); 77 | } else if ($pid == 0) { // child process. 78 | pcntl_exec('/usr/bin/pandoc', [ '--metadata', "pagetitle=$page", '--embed-resources', '--standalone', '-f', $from_format, '-t', 'html', '--css=static_files/ghwikipp.css', '--css=static_files/pandoc.css', '--lua-filter=./pandoc-filter-offline.lua', '-o', $dst, $src ], $env); 79 | exit(1); 80 | } else { // parent process. 81 | $num_children++; 82 | //print("$page\n"); 83 | } 84 | } 85 | } 86 | } 87 | } 88 | closedir($dirp); 89 | } 90 | 91 | 92 | // Mainline! 93 | 94 | date_default_timezone_set('UTC'); 95 | $outputname = $wiki_offline_basename; 96 | 97 | @mkdir('static_files/offline'); // just in case. 98 | 99 | $tmpdir = $offline_data; 100 | $esctmpdir = escapeshellarg($tmpdir); 101 | system("rm -rf $esctmpdir"); 102 | if (!mkdir($tmpdir)) { 103 | print("Failed to mkdir '$tmpdir'\n"); 104 | exit(1); 105 | } 106 | 107 | $tmpoutdir = "$tmpdir/$outputname"; 108 | $esctmpoutdir = escapeshellarg($tmpoutdir); 109 | if (!mkdir($tmpoutdir)) { 110 | print("Failed to mkdir '$tmpoutdir'\n"); 111 | exit(1); 112 | } 113 | 114 | $tmprawdir = "$tmpdir/raw"; 115 | $escrawdir = escapeshellarg($raw_data); 116 | if (!mkdir($tmprawdir)) { 117 | print("Failed to mkdir '$tmprawdir'\n"); 118 | exit(1); 119 | } 120 | 121 | // copy all the raw data elsewhere, so we don't hold the lock longer than 122 | // necessary. We should probably use rsync to speed this up more. :) 123 | $git_repo_lock_fp = fopen($repo_lock_fname, 'c+'); 124 | if ($git_repo_lock_fp === false) { 125 | print("Failed to obtain Git repo lock. Please try again later.\n"); 126 | exit(1); 127 | } else if (flock($git_repo_lock_fp, LOCK_EX) === false) { 128 | print("Exclusive flock of Git repo lock failed. Please try again later."); // uh...? 129 | exit(1); 130 | } 131 | 132 | $esctmprawdir = escapeshellarg($tmprawdir); 133 | system("cp -R $escrawdir/* $esctmprawdir/"); 134 | 135 | flock($git_repo_lock_fp, LOCK_UN); 136 | fclose($git_repo_lock_fp); 137 | unset($git_repo_lock_fp); 138 | 139 | // okay, now we can operate on this data. 140 | cook_tree_for_offline_html($tmprawdir, $tmpoutdir, NULL); 141 | while ($num_children > 0) { 142 | pcntl_waitpid(-1, $status); // wait for any child to end. 143 | $num_children--; 144 | } 145 | 146 | if (file_exists("$tmpoutdir/FrontPage.html")) { 147 | copy("$tmpoutdir/FrontPage.html", "$tmpoutdir/index.html"); // make a copy, not a rename, in case something references FrontPage explicitly. 148 | } 149 | 150 | $zipfname = "$outputname.zip"; 151 | $esczipfname = escapeshellarg($zipfname); 152 | $escoutputname = escapeshellarg($outputname); 153 | system("cd $esctmpdir && zip -9rq $esczipfname $escoutputname && mv $esczipfname ../static_files/offline/"); 154 | 155 | -------------------------------------------------------------------------------- /templates/edit: -------------------------------------------------------------------------------- 1 | @include html_header@ 2 | 3 | 4 | 5 | 56 | 57 | 170 | 171 | @include html_startcontent@ 172 | 173 |

174 |

175 | Logged in as @github_name@ 176 | Editing @page@ 177 |

178 |

179 | 180 |

181 |

182 | 183 | 184 |
185 | 186 |
187 |
@cooked@
188 |
189 | 190 |
191 |
192 |
193 |
194 | 195 | 199 | 200 | [ Cancel ] 201 |
202 |
203 | 204 |

205 | 206 | @include html_footer@ 207 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ghwikipp 3 | 4 | (That's "GitHub Wiki++" because programmers shouldn't name things.) 5 | 6 | This is a little glue to make GitHub wikis nicer (I hope). 7 | 8 | The idea is that: 9 | - We want to use a git repo for our wiki revision history. 10 | - We mostly want to edit in a web UI, but want the power to edit 11 | text files directly, run scripts over them, etc. 12 | - We want to allow contributions from the outside world with low 13 | overhead, but we also want to avoid spammers. 14 | 15 | So this thing runs on your own server, but talks to GitHub through 16 | API calls and repo pushes. 17 | 18 | Anyone can read the wiki anonymously. Editing bounces you to GitHub 19 | to log in. If your GitHub account is trusted, edits to the wiki will 20 | be pushed directly to main and show up immediately on the website. 21 | If you aren't a trusted user, your edits become pull requests that 22 | we can examine, comment on, and eventually merge. 23 | 24 | If we like your pull requests, we can mark you trusted. If we don't 25 | like _you_, we can block you and the wiki won't let you submit any 26 | more edits. 27 | 28 | All wiki pages are either Markdown or MediaWiki format. An offline 29 | archive of the wiki pages, as HTML, is generated once per day and 30 | available from the website, and the raw markdown pages are just a 31 | simple git clone. 32 | 33 | Various pieces of UI are handed off to GitHub (See history of 34 | changes to a page? You get sent to the log on GitHub. Feedback on 35 | a page without a specific edit? They land in GitHub Issues, etc), 36 | to keep this small and use the existing tools for greater effect. 37 | 38 | 39 | # To install: 40 | 41 | This thing has a lot of moving parts, and setting them up is daunting. 42 | Do your best, ask for help if you need it, correct me if I explained 43 | something incorrectly. 44 | 45 | * You'll need a server that can run PHP code (mod_php on Apache or 46 | whatever) that allows you to access the filesystem and run shell 47 | commands on. Git, pandoc, command line PHP, and some other minor Unix 48 | command line tools have to be installed, and accessible to the PHP 49 | code, here. You will absolutely need to be able to serve webpages over 50 | SSL with a valid SSL certificate, as GitHub will require this to 51 | talk to your server. 52 | 53 | * We set this up as a new virtual host on Apache that serves wiki 54 | content from the root directory of the host. This probably needs some 55 | bugs fixed to work out of a subdirectory of an existing virtual host. 56 | Our config looks like this... (notably: the first Alias makes one 57 | directory serve static files, and _every other url_ goes through 58 | the PHP script. And, of course, `php_flag engine on` is crucial). 59 | 60 | ``` 61 | ServerAdmin webmaster@libsdl.org 62 | DocumentRoot "/webspace/wiki.libsdl.org" 63 | ServerName wiki.libsdl.org 64 | 65 | ErrorLog /var/log/apache2/error_log_wiki_libsdl_org.log 66 | CustomLog /var/log/apache2/access_log_wiki_libsdl_org.log combined 67 | 68 | Alias /static_files "/webspace/wiki.libsdl.org/static_files" 69 | Alias / "/webspace/wiki.libsdl.org/index.php/" 70 | 71 | 72 | Options +Indexes +FollowSymLinks 73 | AllowOverride Limit FileInfo Indexes 74 | Require all granted 75 | php_flag engine on 76 | 77 | ``` 78 | 79 | * Make a new user on GitHub. This is your wikibot. We called ours 80 | "SDLWikiBot". Give it a legit unused email address you can verify 81 | from (we'll call it `$WIKIBOT_EMAIL` in shell commands, later). 82 | 83 | * Set this new user up in a reasonable way. Go through all the settings. 84 | 85 | 86 | * Clone the ghwikipp repo to the proper directory on your webserver 87 | (this would be `/webspace/wiki.libsdl.org` in the config above, we'll 88 | call it `$MY_VHOST_ROOT_DIR` here. Set an environment variable.) 89 | 90 | ``` 91 | git clone git@github.com:libsdl-org/ghwikipp.git $MY_VHOST_ROOT_DIR 92 | ``` 93 | 94 | * And clone _your wiki_ under that, in a directory called `raw`: 95 | 96 | ``` 97 | git clone git@github.com:$MY_GITHUB_USER/$MY_WIKI_REPO.git $MY_VHOST_ROOT_DIR/raw 98 | ``` 99 | 100 | * Set up a ssh key for pushing changes to the wiki (hit enter for all questions) ... 101 | 102 | ``` 103 | cd $MY_VHOST_ROOT_DIR 104 | ssh-keygen -t ed25519 -C $WIKIBOT_EMAIL -f id_ed25519 105 | ``` 106 | 107 | * Print your new ssh public key to the terminal: 108 | 109 | ``` 110 | cat id_ed25519.pub 111 | ``` 112 | 113 | * You should see a line that starts with `ssh-ed25519` and ends with your 114 | wikibot email address. 115 | 116 | * Go here logged in as the wikibot: https://github.com/settings/keys 117 | 118 | * Click the "New SSH key" button. On the next page, give it whatever you 119 | want for a title, and paste that `ssh-ed25519` line from the terminal 120 | into the "key" textarea, and click "Add SSH key" ... now your wikibot can 121 | push changes to GitHub. 122 | 123 | * Go here logged in as the wikibot: https://github.com/settings/tokens 124 | 125 | * Click "Generate new token (classic)". Make the note "Wiki API access" or 126 | whatever, click "repo" in the scopes section so it fills in a bunch of 127 | checkboxes under it. Scroll down and click the "generate token" button. It 128 | will show you a long string of characters. _Copy this somewhere_ as it won't 129 | be shown to you again. 130 | 131 | * Log out as wikibot, you're done with this for the moment. 132 | 133 | * Go back to _your normal_ GitHub account, and visit the "settings" page 134 | on the new wiki project. Click on "Collaborators" on the left side of the 135 | page and invite the wikibot to this project with write access. Then, logged 136 | in as the wikibot, accept the invitation (which might require you to click a 137 | link in an email sent to the wikibot's address). Now the bot can push changes 138 | to this repo. 139 | 140 | * From your GitHub organization's settings page (or if you don't have an 141 | organization, your personal settings page), click on "Developer settings" 142 | and then "OAuth Apps". Click "Register an application". Put the name in 143 | as whatever (this will be shown to end users, so "MyProject's Wiki" is 144 | probably good). "Homepage URL" should be your virtual host's URL. 145 | "Application description" should be something like "This application 146 | verifies your GitHub username before you are allowed to make changes to 147 | MyProject's wiki." Set the "Callback URL" to the virtual host's URL, and 148 | click "Register application." 149 | 150 | * On the next page, click "Generate client secret" and _copy the string 151 | it gives you_ ... you can't get it again later. Also copy down the 152 | "Client ID" string. 153 | 154 | * On your wiki's project page, click "settings", then "webhooks", then 155 | "add webhook". For this, set the "Payload URL" to "github/webhook" on 156 | your virtual host's address (so, for wiki.libsdl.org, this would be 157 | set to `https://wiki.libsdl.org/github/webhook`). "Content-Type" should 158 | be set to "application/json" ... You can write anything in the "Secret" 159 | field as long as it's secret. A long string of numbers and letters, 160 | doesn't matter, just copy it somewhere because your server needs to know 161 | it too. Select "just the push event" for which events you want sent, and 162 | click "Add webhook". 163 | 164 | * Back in your virtual host's directory, copy the sample config... 165 | 166 | ``` 167 | cp config.php.sample config.php 168 | ``` 169 | 170 | * ...and then edit config.php to fit your needs. All those strings you 171 | were supposed to copy, above? They go in this file. 172 | 173 | * Copy your own logo.png and favicon.ico to the "static_files" directory. 174 | 175 | * Set up a cronjob to build the offline archive once a day: 176 | 177 | ``` 178 | sudo ln -s $MY_VHOST_ROOT_DIR/make_offline_archive.php /etc/cron.daily/build_offline_wiki_archive 179 | ``` 180 | 181 | * Make sure everything is fully readable/writable by the webserver user: 182 | 183 | ``` 184 | chown -R www-data:www-data $MY_VHOST_ROOT_DIR 185 | ``` 186 | 187 | (if you don't have root access, you might be able to set up a cronjob 188 | for your specific user. Nothing about this _needs_ root privileges. 189 | Read the manual. :) ) 190 | 191 | * Okay, now we're getting somewhere! Go to your vhost in a web browser 192 | and see if it chokes. It will probably complain that FrontPage is 193 | missing. Click "edit" ... you should be tossed over to github to verify 194 | you're a real person. Permit authorization and you'll be back to edit 195 | the wiki page. (unless you revoke this authorization in GitHub's settings 196 | or log out, you won't have to see GitHub again for reauthorization). 197 | 198 | * Just write "hello world" and save it. If you didn't set yourself up as 199 | an admin, you should end up with a pull request on the wiki github repo, 200 | otherwise you'll get a commit pushed to main (master, whatever) for your 201 | new change. This will fire GitHub's webhook, which will cause your server 202 | to recook all the existing wiki data on this first run. If you have a 203 | big wiki, give it a few minutes. Otherwise, you should be good to go. 204 | Mark a few people as admins, so they can mark people as trusted to make 205 | direct pushes instead of pull requests, as appropriate. 206 | 207 | * Set up a cronjob to run build_categories.php, if you want the wikibot 208 | to build category pages and push them. Once an hour or once a day is probably 209 | fine. SDL currently has a small script that runs when the wiki's git 210 | repository gets new pushes that runs build_categories.php every time, for 211 | faster updates. 212 | 213 | * Install codesearch: 214 | 215 | ``` 216 | sudo apt-get install codesearch 217 | ``` 218 | 219 | 220 | 221 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 'gfm', 7 | 'mediawiki' => 'mediawiki' 8 | ]; 9 | 10 | $github_url = "https://github.com/$github_repo_owner/$github_repo"; 11 | $document = NULL; // !!! FIXME: remove this global. 12 | $operation = NULL; 13 | 14 | putenv("GIT_SSH_COMMAND=ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i $ssh_key_fname"); 15 | putenv("GIT_COMMITTER_NAME=$git_committer_name"); 16 | putenv("GIT_COMMITTER_EMAIL=$git_committer_email"); 17 | putenv("CSEARCHINDEX=$cooked_data/.csearchindex"); 18 | 19 | 20 | $git_repo_lock_fp = false; 21 | $git_repo_lock_count = 0; 22 | function obtain_git_repo_lock() 23 | { 24 | global $repo_lock_fname, $git_repo_lock_fp, $git_repo_lock_count; 25 | 26 | //$starttime = microtime(true); 27 | 28 | if ( ($git_repo_lock_count < 0) || 29 | (($git_repo_lock_fp === false) && ($git_repo_lock_count != 0)) || 30 | (($git_repo_lock_fp !== false) && ($git_repo_lock_count == 0)) ) { 31 | fail503('Git repo lock confusion! This is a bug. Please try again later.'); 32 | } 33 | 34 | if ($git_repo_lock_count == 0) { 35 | $git_repo_lock_fp = fopen($repo_lock_fname, 'c+'); 36 | if ($git_repo_lock_fp === false) { 37 | fail503("Failed to obtain Git repo lock. Please try again later."); 38 | } else if (flock($git_repo_lock_fp, LOCK_EX) === false) { 39 | fail503("Exclusive flock of Git repo lock failed. Please try again later."); // uh...? 40 | } 41 | } 42 | 43 | $git_repo_lock_count++; 44 | 45 | //$totaltime = (int) (microtime(true) - $starttime); 46 | //print("\n\n\n\n\nTime to obtain git repo lock: $totaltime seconds.\n\n\n\n"); 47 | } 48 | 49 | function release_git_repo_lock() 50 | { 51 | global $git_repo_lock_fp, $git_repo_lock_count; 52 | if (($git_repo_lock_fp === false) || ($git_repo_lock_count <= 0)) { 53 | fail503('Git repo unlocked too many times! This is a bug. Please try again later.'); 54 | } 55 | 56 | --$git_repo_lock_count; 57 | if ($git_repo_lock_count == 0) { 58 | flock($git_repo_lock_fp, LOCK_UN); 59 | fclose($git_repo_lock_fp); 60 | $git_repo_lock_fp = false; 61 | } 62 | } 63 | 64 | 65 | function get_template($template, $vars=NULL, $safe_to_fail=true) 66 | { 67 | global $document, $operation, $wikiname, $wikidesc, $wiki_license, $wiki_license_url, $twitteruser; 68 | global $github_repo_main_branch, $base_url, $github_url, $wiki_offline_basename; 69 | 70 | $str = file_get_contents($template); 71 | if ($str === false) { 72 | if ($safe_to_fail) { // don't call fail503 if we'd infinitely recurse. 73 | fail503("Internal error: missing template '$template'"); 74 | } else { 75 | header('HTTP/1.0 503 Service Unavailable'); 76 | header('Content-Type: text/plain; charset=utf-8'); 77 | print("\n\nERROR: Missing template '$template' while already failing, giving up.\n\n"); 78 | exit(0); 79 | } 80 | } 81 | 82 | // let templates include common text. 83 | $str = preg_replace_callback( 84 | '/\@include (.*?)\@/', 85 | function ($matches) use ($vars, $safe_to_fail) { 86 | return get_template('templates/' . $matches[1], $vars, $safe_to_fail); 87 | }, 88 | $str 89 | ); 90 | 91 | // replace any @varname@ with the value of that variable. 92 | return preg_replace_callback( 93 | '/\@([a-z_]+)\@/', 94 | function ($matches) use ( 95 | $vars, $document, $operation, $wikiname, $wikidesc, 96 | $wiki_license, $wiki_license_url, $base_url, $github_url, 97 | $twitteruser, $github_repo_main_branch, $wiki_offline_basename) { 98 | $key = $matches[1]; 99 | if (($vars != NULL) && isset($vars[$key])) { return $vars[$key]; } 100 | else if ($key == 'page') { return $document; } 101 | else if ($key == 'operation') { return $operation; } 102 | else if ($key == 'url') { return $_SERVER['PHP_SELF']; } 103 | else if ($key == 'domain') { return $_SERVER['SERVER_NAME']; } 104 | else if ($key == 'baseurl') { return $base_url; } 105 | else if ($key == 'github_id') { return isset($_SESSION['github_id']) ? $_SESSION['github_id'] : '[not logged in]'; } 106 | else if ($key == 'github_user') { return isset($_SESSION['github_user']) ? $_SESSION['github_user'] : '[not logged in]'; } 107 | else if ($key == 'github_name') { return isset($_SESSION['github_name']) ? $_SESSION['github_name'] : '[not logged in]'; } 108 | else if ($key == 'github_email') { return isset($_SESSION['github_email']) ? $_SESSION['github_email'] : '[not logged in]'; } 109 | else if ($key == 'github_avatar') { return isset($_SESSION['github_avatar']) ? $_SESSION['github_avatar'] : ''; } // !!! FIXME: return a generic image instead of '' 110 | else if ($key == 'wikiname') { return $wikiname; } 111 | else if ($key == 'wikidesc') { return $wikidesc; } 112 | else if ($key == 'wikilicense') { return $wiki_license; } 113 | else if ($key == 'wikilicenseurl') { return $wiki_license_url; } 114 | else if ($key == 'twitteruser') { return $twitteruser; } 115 | else if ($key == 'githuburl') { return $github_url; } 116 | else if ($key == 'githubmainbranch') { return $github_repo_main_branch; } 117 | else if ($key == 'offlinebasename') { return $wiki_offline_basename; } 118 | else if ($key == 'title') { return "$document - $wikiname"; } // this is often overridden by $vars. 119 | return "@$key@"; // ignore it. 120 | }, 121 | $str 122 | ); 123 | } 124 | 125 | function print_template($template, $vars=NULL, $safe_to_fail=true) 126 | { 127 | print(get_template("templates/$template", $vars, $safe_to_fail)); 128 | } 129 | 130 | 131 | function fail($response, $msg, $url=NULL, $template='fail', $extravars=NULL) 132 | { 133 | global $git_repo_lock_fp; 134 | if ($git_repo_lock_fp !== false) { // make sure we're definitely released. 135 | flock($git_repo_lock_fp, LOCK_UN); 136 | fclose($git_repo_lock_fp); 137 | $git_repo_lock_fp = false; 138 | } 139 | 140 | if ($response != NULL) { 141 | header("HTTP/1.0 $response"); 142 | if ($url != NULL) { header("Location: $url"); } 143 | } 144 | 145 | $vars = [ 'httpresponse' => $response, 'message' => $msg ]; 146 | 147 | if ($url != NULL) { 148 | $vars['url'] = $url; 149 | } 150 | 151 | if ($extravars != NULL) { 152 | foreach($extravars as $k => $v) { 153 | $vars[$k] = $v; 154 | } 155 | } 156 | 157 | print_template($template, $vars, false); 158 | exit(0); 159 | } 160 | 161 | function fail400($msg, $template='fail', $extravars=NULL) { fail('400 Bad Request', $msg, NULL, $template, $extravars); } 162 | function fail404($msg, $template='fail', $extravars=NULL) { fail('404 Not Found', $msg, NULL, $template, $extravars); } 163 | function fail503($msg, $template='fail', $extravars=NULL) { fail('503 Service Unavailable', $msg, NULL, $template, $extravars); } 164 | 165 | function redirect($url, $msg=NULL) 166 | { 167 | if ($msg == NULL) { 168 | $msg = "We need your browser to go here now: $url"; 169 | } 170 | fail('302 Found', $msg, $url, 'redirect'); 171 | } 172 | 173 | function require_session() 174 | { 175 | if (!session_id()) { 176 | if (!session_start()) { 177 | fail503('Session failed to start, please try again later.'); 178 | } 179 | } 180 | } 181 | 182 | function cook_string($str, $from_pandoc_format) 183 | { 184 | if ($from_pandoc_format == NULL) { 185 | return "
\n$str\n
\n"; // oh well. 186 | } 187 | 188 | $descriptorspec = array( 189 | 0 => array('pipe', 'r'), // stdin 190 | 1 => array('pipe', 'w'), // stdout 191 | ); 192 | 193 | $proc = proc_open([ 'pandoc', '-f', $from_pandoc_format, '-t', 'html', '--toc', '--lua-filter=./pandoc-filter.lua' ], $descriptorspec, $pipes); 194 | 195 | $retval = ''; 196 | if (is_resource($proc)) { 197 | fwrite($pipes[0], $str); 198 | fclose($pipes[0]); 199 | $retval = stream_get_contents($pipes[1]); 200 | fclose($pipes[1]); 201 | proc_close($proc); 202 | } 203 | 204 | return $retval; 205 | } 206 | 207 | function cook_page_from_files($rawfname, $cookedfname) 208 | { 209 | global $supported_formats; 210 | 211 | $cooked = NULL; 212 | 213 | $from_format = NULL; 214 | $ext = strrchr($rawfname, '.'); 215 | if ($ext !== false) { 216 | $ext = substr($ext, 1); 217 | if (isset($supported_formats[$ext])) { 218 | $from_format = $supported_formats[$ext]; 219 | } 220 | } 221 | 222 | obtain_git_repo_lock(); 223 | if (!file_exists($rawfname)) { // was deleted? 224 | @unlink($cookedfname); 225 | } else { 226 | $raw = file_get_contents($rawfname); 227 | if ($raw === false) { 228 | fail503("Failed to read $rawfname to cook it. Please try again later."); 229 | } 230 | $cooked = cook_string($raw, $from_format); 231 | if (($cooked != '') || ($raw == '')) { 232 | @mkdir(dirname($cookedfname)); 233 | $tmppath = "$cookedfname.tmp"; 234 | if (file_put_contents($tmppath, $cooked) != strlen($cooked)) { 235 | fail503("Failed to write $cookedfname when cooking. Please try again later."); 236 | } else if (!rename($tmppath, $cookedfname)) { 237 | unlink($tmppath); 238 | fail503("Failed to rename $tmppath to $cookedfname when cooking. Please try again later."); 239 | } 240 | } 241 | } 242 | release_git_repo_lock(); 243 | 244 | return $cooked; 245 | } 246 | 247 | function cook_page($page) 248 | { 249 | global $raw_data, $cooked_data, $supported_formats; 250 | 251 | $dst = "$cooked_data/$page.html"; 252 | foreach ($supported_formats as $ext => $format) { 253 | $src = "$raw_data/$page.$ext"; 254 | if (file_exists($src)) { 255 | return cook_page_from_files($src, $dst); 256 | } 257 | } 258 | 259 | fail503("Internal error: Couldn't find page's file to cook!"); 260 | } 261 | 262 | function recook_tree($rawbasedir, $cookedbasedir, $dir, &$updated) 263 | { 264 | global $supported_formats; 265 | 266 | $dirp = opendir("$rawbasedir/$dir"); 267 | if ($dirp === false) { 268 | return; 269 | } 270 | 271 | while (($dent = readdir($dirp)) !== false) { 272 | if (substr($dent, 0, 1) == '.') { continue; } // skip ".", "..", and metadata. 273 | $page = ($dir == '') ? $dent : "$dir/$dent"; 274 | if (is_dir("$rawbasedir/$page")) { 275 | recook_tree($rawbasedir, $cookedbasedir, $page, $updated); 276 | } else { 277 | $src = "$rawbasedir/$page"; 278 | $dst = "$cookedbasedir/$page"; 279 | $dst = preg_replace('/^(.*)\..*?$/', '$1.html', $dst); 280 | $srcmtime = filemtime($src); 281 | $dstmtime = @filemtime($dst); 282 | if (($srcmtime === false) || ($dstmtime === false) || ($srcmtime >= $dstmtime)) { 283 | cook_page_from_files($src, $dst); 284 | $updated[] = "+ $page"; 285 | } 286 | } 287 | } 288 | closedir($dirp); 289 | 290 | // prune cooked dir... 291 | $dirp = opendir("$cookedbasedir/$dir"); 292 | if ($dirp === false) { 293 | return; 294 | } 295 | 296 | while (($dent = readdir($dirp)) !== false) { 297 | if (substr($dent, 0, 1) == '.') { continue; } // skip ".", "..", and metadata. 298 | $page = ($dir == '') ? $dent : "$dir/$dent"; 299 | $dst = "$cookedbasedir/$page"; 300 | $src = "$rawbasedir/$page"; 301 | $src = preg_replace('/\.html$/', '', $src); 302 | $found = false; 303 | foreach ($supported_formats as $ext => $format) { 304 | if (file_exists("$src.$ext")) { 305 | $found = true; 306 | break; 307 | } 308 | } 309 | 310 | if (!$found) { 311 | if (is_dir($dst)) { 312 | @rmdir($dst); // we don't recurse here, since we're recursing above. 313 | } else { 314 | unlink($dst); 315 | } 316 | $updated[] = "- $page"; 317 | } 318 | } 319 | closedir($dirp); 320 | } 321 | 322 | function recook_search_index() 323 | { 324 | global $raw_data; 325 | unset($output); 326 | $escrawdata = escapeshellarg($raw_data); 327 | $failed = (exec("cindex $escrawdata", $output, $result) === false) || ($result != 0); 328 | // !!! FIXME: check for failure here. 329 | } 330 | 331 | 332 | // Convert any relative links in a wiki page to full links. You only want to do this for 333 | // throwaway cooks for preview content while editing, since that's running off a subdir 334 | // of where the edited page would normally be, which messes up links. 335 | function fixup_preview_links($page, $data) 336 | { 337 | global $base_url; 338 | 339 | $stripped_page = dirname($page); // this just happens to work. 340 | $pattern = '/(\)/i'; 341 | $replacement = '$1' . "$base_url/$stripped_page/" . '$3$4'; 342 | return preg_replace($pattern, $replacement, $data); 343 | } 344 | 345 | 346 | // Stole some of this from https://github.com/dintel/php-github-webhook/blob/master/src/Handler.php 347 | function validate_webhook_signature($gitHubSignatureHeader, $payload) 348 | { 349 | global $github_webhook_secret; 350 | 351 | list ($algo, $gitHubSignature) = explode("=", $gitHubSignatureHeader); 352 | 353 | if (($algo !== 'sha256') && ($algo !== 'sha1')) { 354 | // see https://developer.github.com/webhooks/securing/ 355 | return false; 356 | } 357 | 358 | $payloadHash = hash_hmac($algo, $payload, $github_webhook_secret); 359 | return ($payloadHash === $gitHubSignature); 360 | } 361 | 362 | function github_webhook() 363 | { 364 | global $raw_data, $cooked_data, $github_repo_main_branch; 365 | 366 | header('Content-Type: text/plain; charset=utf-8'); 367 | 368 | $str = ''; 369 | 370 | $signature = isset($_SERVER['HTTP_X_HUB_SIGNATURE_256']) ? $_SERVER['HTTP_X_HUB_SIGNATURE_256'] : NULL; 371 | $event = isset($_SERVER['HTTP_X_GITHUB_EVENT']) ? $_SERVER['HTTP_X_GITHUB_EVENT'] : NULL; 372 | $delivery = isset($_SERVER['HTTP_X_GITHUB_DELIVERY']) ? $_SERVER['HTTP_X_GITHUB_DELIVERY'] : NULL; 373 | 374 | if (!isset($signature, $event, $delivery)) { 375 | fail400("$str\nBAD SIGNATURE\n", 'fail_plaintext'); 376 | } 377 | 378 | $starttime = microtime(true); 379 | $payload = file_get_contents('php://input'); 380 | $totaltime = (int) (microtime(true) - $starttime); 381 | $str .= "OBTAINED JSON DATA IN $totaltime SECONDS.\n"; 382 | 383 | $starttime = microtime(true); 384 | 385 | // Check if the payload is json or urlencoded. 386 | if (strpos($payload, 'payload=') === 0) { 387 | $payload = substr(urldecode($payload), 8); 388 | } 389 | 390 | if (!validate_webhook_signature($signature, $payload)) { 391 | fail400("$str\nBAD SIGNATURE\n", 'fail_plaintext'); 392 | } 393 | 394 | $totaltime = (int) (microtime(true) - $starttime); 395 | $str .= "VALIDATED WEBHOOK SIGNATURE IN $totaltime SECONDS.\n"; 396 | 397 | $str .= "SIGNATURE OK IN $totaltime SECONDS.\n"; 398 | 399 | $str .= "GITHUB EVENT: $event\n"; 400 | 401 | if ($event == 'push') { // we only care about push events. 402 | $json = json_decode($payload, TRUE); 403 | if (($json['ref'] != "refs/heads/$github_repo_main_branch") && $json['deleted']) { 404 | $str .= "PUSH EVENT IS A BRANCH DELETION, DOING A FETCH FOR PRUNING.\n"; // probably a topic branch we just pushed for a pull request. 405 | $starttime = microtime(true); 406 | obtain_git_repo_lock(); 407 | $totaltime = (int) (microtime(true) - $starttime); 408 | $str .= "OBTAINED GIT REPO LOCK IN $totaltime SECONDS.\n"; 409 | 410 | $starttime = microtime(true); 411 | $escrawdata = escapeshellarg($raw_data); 412 | $cmd = "( cd $escrawdata && git fetch --prune ) 2>&1"; 413 | unset($output); 414 | $failed = (exec($cmd, $output, $result) === false) || ($result != 0); 415 | 416 | $str .= "\nOUTPUT of '$cmd':\n\n"; 417 | foreach ($output as $l) { 418 | $str .= " $l\n"; 419 | } 420 | 421 | if ($failed) { 422 | fail503("$str\nGIT FETCH FAILED!\n", 'fail_plaintext'); 423 | } 424 | 425 | $totaltime = (int) (microtime(true) - $starttime); 426 | $str .= "GIT FETCH COMPLETE IN $totaltime SECONDS.\n"; 427 | 428 | $starttime = microtime(true); 429 | $escrawdata = escapeshellarg($raw_data); 430 | $branch = preg_replace('/^refs\/heads\//', '', $json['ref']); 431 | $escbranch = escapeshellarg($branch); 432 | $cmd = "( cd $escrawdata && git branch -D $escbranch ) 2>&1"; 433 | unset($output); 434 | $failed = (exec($cmd, $output, $result) === false) || ($result != 0); 435 | 436 | $str .= "\nOUTPUT of '$cmd':\n\n"; 437 | foreach ($output as $l) { 438 | $str .= " $l\n"; 439 | } 440 | 441 | if ($failed) { 442 | fail503("$str\nGIT BRANCH DELETE FAILED!\n", 'fail_plaintext'); 443 | } 444 | 445 | $totaltime = (int) (microtime(true) - $starttime); 446 | $str .= "GIT BRANCH DELETE COMPLETE IN $totaltime SECONDS.\n"; 447 | } else if ($json['ref'] != "refs/heads/$github_repo_main_branch") { 448 | $str .= "PUSH EVENT ISN'T FOR MAIN BRANCH, IGNORING.\n"; // probably a topic branch we just pushed for a pull request. 449 | } else { 450 | $starttime = microtime(true); 451 | obtain_git_repo_lock(); 452 | $totaltime = (int) (microtime(true) - $starttime); 453 | $str .= "OBTAINED GIT REPO LOCK IN $totaltime SECONDS.\n"; 454 | 455 | $starttime = microtime(true); 456 | $escrawdata = escapeshellarg($raw_data); 457 | $cmd = "( cd $escrawdata && git pull --rebase --prune ) 2>&1"; 458 | unset($output); 459 | $failed = (exec($cmd, $output, $result) === false) || ($result != 0); 460 | 461 | $str .= "\nOUTPUT of '$cmd':\n\n"; 462 | foreach ($output as $l) { 463 | $str .= " $l\n"; 464 | } 465 | 466 | if ($failed) { 467 | fail503("$str\nGIT PULL FAILED!\n", 'fail_plaintext'); 468 | } 469 | 470 | $totaltime = (int) (microtime(true) - $starttime); 471 | $str .= "GIT PULL COMPLETE IN $totaltime SECONDS.\n"; 472 | 473 | $starttime = microtime(true); 474 | $updated = array(); 475 | $failed = false; // !!! FIXME: recook_tree returns void atm 476 | recook_tree($raw_data, $cooked_data, '', $updated); 477 | 478 | $str .= "\n"; 479 | foreach ($updated as $f) { 480 | $str .= "$f\n"; 481 | } 482 | $str .= "\n"; 483 | 484 | if ($failed) { 485 | fail503("$str\nFAILED TO RECOOK DATA!\n", 'fail_plaintext'); 486 | } 487 | 488 | recook_search_index(); 489 | 490 | $totaltime = (int) (microtime(true) - $starttime); 491 | 492 | release_git_repo_lock(); 493 | 494 | $str .= "\nTREE RECOOKED IN $totaltime SECONDS.\n"; 495 | } 496 | } 497 | 498 | // handle other events we care about here. 499 | 500 | print("$str\n\nOK\n\n"); 501 | 502 | exit(0); // never return from this function. 503 | } 504 | 505 | 506 | function handle_oauth_error() 507 | { 508 | if (isset($_REQUEST['error'])) { // this is probably a failure from GitHub's OAuth page. 509 | require_session(); 510 | if (isset($_SESSION['github_oauth_state'])) { unset($_SESSION['github_oauth_state']); } 511 | $reason = isset($_REQUEST['error_description']) ? $_REQUEST['error_description'] : 'unknown error'; 512 | fail400('GitHub auth error', $template='auth_error', [ 'error' => $_REQUEST['error'], 'reason' => $reason ]); 513 | } 514 | } 515 | 516 | function call_github_api($url, $args=NULL, $token=NULL, $ispost=false, $failonerror=true) 517 | { 518 | global $user_agent; 519 | 520 | if (!$ispost && ($args != NULL) && (!empty($args)) ) { 521 | $url .= '?' . http_build_query($args); 522 | } 523 | 524 | $reqheaders = array( 525 | "User-Agent: $user_agent", 526 | 'Accept: application/json' 527 | ); 528 | 529 | if ($token != NULL) { 530 | $reqheaders[] = 'Authorization: token ' . $token; 531 | } 532 | 533 | if ($ispost) { 534 | $reqheaders[] = 'Content-Type: application/json; charset=utf-8'; 535 | } 536 | 537 | $curl = curl_init($url); 538 | $curlopts = array( 539 | CURLOPT_AUTOREFERER => TRUE, 540 | CURLOPT_FOLLOWLOCATION => TRUE, 541 | CURLOPT_RETURNTRANSFER => TRUE, 542 | CURLOPT_HTTPHEADER => $reqheaders 543 | ); 544 | 545 | if ($ispost) { 546 | $curlopts[CURLOPT_POST] = TRUE; 547 | $curlopts[CURLOPT_POSTFIELDS] = json_encode(($args == NULL) ? array() : $args); 548 | } 549 | 550 | //print("CURLOPTS:\n"); print_r($curlopts); 551 | //print("REQHEADERS:\n"); print_r($reqheaders); 552 | 553 | curl_setopt_array($curl, $curlopts); 554 | $responsejson = curl_exec($curl); 555 | $curlfailed = ($responsejson === false); 556 | $curlerr = $curlfailed ? curl_error($curl) : NULL; 557 | $httprc = $curlfailed ? 0 : curl_getinfo($curl, CURLINFO_RESPONSE_CODE); 558 | curl_close($curl); 559 | unset($curl); 560 | 561 | //print("RESPONSE:\n"); print_r(json_decode($responsejson, TRUE)); 562 | 563 | if ($curlfailed) { 564 | fail503("Couldn't talk to GitHub API, try again later ($curlerr)"); 565 | } else if (($httprc != 200) && ($httprc != 201)) { 566 | if (!$failonerror) { 567 | return NULL; 568 | } 569 | fail503("GitHub API reported error $httprc, try again later"); 570 | } 571 | 572 | $retval = json_decode($responsejson, TRUE); 573 | if ($retval == NULL) { 574 | fail503('GitHub API sent us bogus JSON, try again later.'); 575 | } 576 | return $retval; 577 | } 578 | 579 | function endsWith($haystack, $needle) 580 | { 581 | $length = strlen($needle); 582 | return $length > 0 ? substr($haystack, -$length) === $needle : true; 583 | } 584 | 585 | function authorize_with_github($force=false) 586 | { 587 | global $github_oauth_clientid; 588 | global $github_oauth_secret; 589 | global $blocked_data, $trusted_data, $admin_data, $always_admin, $base_url; 590 | 591 | require_session(); 592 | 593 | handle_oauth_error(); // die now if we think GitHub refused to auth user. 594 | 595 | //print_r($_SESSION); 596 | 597 | // $force==true means we're being asked to make sure GitHub is still cool 598 | // with this session, which we do before requests that make changes, like 599 | // committing an edit. If the user auth'd with GitHub but then logged 600 | // out over there since our last check, we might be letting the user do 601 | // things they shouldn't be allowed to do anymore. For the basic workflow 602 | // we don't care if they are still _actually_ logged in at every step, 603 | // though. 604 | 605 | if ( !$force && 606 | isset($_SESSION['github_id']) && 607 | isset($_SESSION['github_user']) && 608 | isset($_SESSION['github_email']) && 609 | isset($_SESSION['github_name']) && 610 | isset($_SESSION['github_access_token']) && 611 | isset($_SESSION['last_auth_time']) && 612 | isset($_SESSION['expected_ipaddr']) && 613 | isset($_SESSION['is_blocked']) && 614 | isset($_SESSION['is_trusted']) && 615 | isset($_SESSION['is_admin']) && 616 | ( (time() - $_SESSION['last_auth_time']) < (60 * 60) ) && 617 | ($_SESSION['expected_ipaddr'] == $_SERVER['REMOTE_ADDR']) ) { 618 | //print("ALREADY LOGGED IN\n"); 619 | 620 | if ($_SESSION['is_blocked']) { 621 | fail400('Sorry, but you are blocked from this wiki.'); 622 | } 623 | return; // we're already logged in. 624 | 625 | // if there's a "code" arg, this is probably a redirect back from GitHub's OAuth page. 626 | // make sure this isn't the user just reloading the page before using it again, as that will fail; we'll reauth to get a new code in that case. 627 | } else if ( isset($_REQUEST['code']) && (!isset($_SESSION['github_last_used_oauth_code']) || ($_SESSION['github_last_used_oauth_code'] != $_REQUEST['code'])) ) { 628 | if ( !isset($_REQUEST['state']) || !isset($_SESSION['github_oauth_state']) ) { 629 | fail400('GitHub authorization appears to be confused, please try again.'); 630 | } else if ($_SESSION['github_oauth_state'] != $_REQUEST['state']) { 631 | fail400('This appears to be a bogus attempt to authorize with GitHub. If in error, please try again.'); 632 | } 633 | 634 | $_SESSION['github_last_used_oauth_code'] = $_REQUEST['code']; 635 | 636 | $tokenurl = 'https://github.com/login/oauth/access_token'; 637 | $response = call_github_api($tokenurl, [ 638 | 'client_id' => $github_oauth_clientid, 639 | 'client_secret' => $github_oauth_secret, 640 | 'state' => $_REQUEST['state'], 641 | 'code' => $_REQUEST['code'] 642 | ]); 643 | 644 | //print("\n
\n"); print_r($response); print("\n
\n\n"); 645 | if (!isset($response['access_token'])) { 646 | fail503("GitHub OAuth didn't provide an access token! Please try again later."); 647 | } 648 | 649 | $token = $response['access_token']; 650 | $_SESSION['github_access_token'] = $token; 651 | } 652 | 653 | // If we already have an access token (or just got one, above), see if 654 | // it's still valid. If it isn't, force a full reauth. If it still 655 | // works, GitHub still thinks we're cool. 656 | if (isset($_SESSION['github_access_token'])) 657 | { 658 | $response = call_github_api('https://api.github.com/user', NULL, $_SESSION['github_access_token'], false, false); 659 | 660 | if ($response != NULL) { 661 | //print("\n
\nGITHUB USER API RESPONSE:\n"); print_r($response); print("\n
\n\n"); 662 | 663 | if ( !isset($response['name']) ) { 664 | $response['name'] = $response['login']; // real name not given, just use the username instead. 665 | } 666 | 667 | if ( !isset($response['id']) || 668 | !isset($response['login']) || 669 | !isset($response['name']) ) { 670 | $msg = "GitHub didn't tell us everything we need to know about you to use this wiki. Please try again later.

\n" . 671 | "
some debug info...\n\n" .
 672 |                        "Response from GitHub:\n" . print_r($response, true) . "\n\n\n" .
 673 |                        "\$_SESSION:\n" . print_r($_SESSION, true) . "\n\n\n" .
 674 |                        "
\n
\n" . 675 | "You can report this bug at the
bug tracker" . 676 | " or privately to Ryan's email.\n"; 677 | unset($_SESSION['github_access_token']); 678 | fail503($msg); 679 | } 680 | 681 | // Find a public email, favoring the one marked "primary" if possible. 682 | $emailresponse = call_github_api('https://api.github.com/user/emails', NULL, $_SESSION['github_access_token'], false, false); 683 | //print("\n
\nGITHUB USER EMAIL API RESPONSE:\n"); print_r($emailresponse); print("\n
\n\n"); 684 | 685 | $fakeemail = NULL; 686 | $bestemail = NULL; 687 | if (is_array($emailresponse)) { 688 | foreach ($emailresponse as $e) { 689 | if (!isset($e['email'])) { continue; } 690 | if (isset($e['visibility']) && ($e['visibility'] == 'private')) { continue; } 691 | if ($bestemail == NULL) { $bestemail = $e['email']; } 692 | if (isset($e['primary']) && (((int) $e['primary']) == 1)) { $bestemail = $e['email']; } 693 | if (endsWith($e['email'], '@users.noreply.github.com')) { $fakeemail = $e['email']; } 694 | } 695 | } 696 | 697 | if ($bestemail == NULL) { 698 | $bestemail = $fakeemail; 699 | } 700 | 701 | if ($bestemail == NULL) { 702 | if (isset($response['email'])) { // take the one from the initial user request, which isn't always set for some reason. 703 | $bestemail = $response['email']; 704 | } else { 705 | $msg = "GitHub won't tell us your email address, which we need to make edits to the wiki. Check your settings on GitHub?

\n" . 706 | "
some debug info...\n\n" .
 707 |                            "Response from GitHub/user:\n" . print_r($response, true) . "\n\n\n" .
 708 |                            "Response from GitHub/user/emails:\n" . print_r($emailresponse, true) . "\n\n\n" .
 709 |                            "\$_SESSION:\n" . print_r($_SESSION, true) . "\n\n\n" .
 710 |                            "
\n
\n" . 711 | "You can report this bug at the bug tracker" . 712 | " or privately to Ryan's email.\n"; 713 | unset($_SESSION['github_access_token']); 714 | fail503($msg); 715 | } 716 | } 717 | 718 | //print("\n
\nWe think your email address is $bestemail\n
\n\n"); 719 | 720 | $_SESSION['expected_ipaddr'] = $_SERVER['REMOTE_ADDR']; 721 | $_SESSION['last_auth_time'] = time(); 722 | $_SESSION['github_id'] = $response['id']; 723 | $_SESSION['github_user'] = $response['login']; 724 | $_SESSION['github_email'] = $bestemail; 725 | $_SESSION['github_name'] = $response['name']; 726 | $_SESSION['github_avatar'] = isset($response['avatar_url']) ? $response['avatar_url'] : ''; 727 | $_SESSION['is_blocked'] = file_exists("$blocked_data/" . $response['id']); 728 | $_SESSION['is_trusted'] = file_exists("$trusted_data/" . $response['id']); 729 | $_SESSION['is_admin'] = file_exists("$admin_data/" . $response['id']); 730 | 731 | if (!$_SESSION['is_admin'] && isset($always_admin)) { 732 | $thisuser = $_SESSION['github_user']; 733 | if (is_array($always_admin)) { 734 | foreach ($always_admin as $u) { 735 | if ($u == $thisuser) { 736 | $_SESSION['is_admin'] = 1; 737 | break; 738 | } 739 | } 740 | } else { 741 | $_SESSION['is_admin'] = ($thisuser == $always_admin); 742 | } 743 | } 744 | 745 | //print("SESSION:\n"); print_r($_SESSION); 746 | 747 | if ($_SESSION['is_blocked']) { 748 | fail400('Sorry, but you are blocked from this wiki.'); 749 | } 750 | 751 | //print("AUTH TO GITHUB VALID AND READY\n"); 752 | // logged in, verified, and good to go! 753 | 754 | if (isset($_REQUEST['code'])) { //Force a redirect to dump the code and state URL args. 755 | redirect($_SERVER['PHP_SELF'], 'Reloading to finish login!'); 756 | } 757 | 758 | return; // logged in and ready to go as-is. 759 | } 760 | 761 | // still here? We'll try reauthing, below. 762 | } 763 | 764 | 765 | // we're starting authorization from scratch. 766 | 767 | // kill any lingering state. 768 | unset($_SESSION['github_id']); 769 | unset($_SESSION['github_user']); 770 | unset($_SESSION['github_email']); 771 | unset($_SESSION['github_name']); 772 | unset($_SESSION['github_avatar']); 773 | unset($_SESSION['github_access_token']); 774 | unset($_SESSION['expected_ipaddr']); 775 | unset($_SESSION['last_auth_time']); 776 | unset($_SESSION['is_blocked']); 777 | unset($_SESSION['is_trusted']); 778 | unset($_SESSION['is_admin']); 779 | 780 | // !!! FIXME: no idea if this is a good idea. 781 | $_SESSION['github_oauth_state'] = hash('sha256', microtime(TRUE) . rand() . $_SERVER['REMOTE_ADDR']); 782 | 783 | $github_authorize_url = 'https://github.com/login/oauth/authorize'; 784 | redirect($github_authorize_url . '?' . http_build_query([ 785 | 'client_id' => $github_oauth_clientid, 786 | 'redirect_uri' => $base_url . $_SERVER['PHP_SELF'], 787 | 'state' => $_SESSION['github_oauth_state'], 788 | 'scope' => 'user:email' 789 | ]), 'Sending you to GitHub to log in...'); 790 | 791 | // if everything goes okay, GitHub will redirect the user back to our 792 | // same URL and we can try again, this time with authorization. 793 | } 794 | 795 | function force_authorize_with_github() 796 | { 797 | authorize_with_github(true); 798 | } 799 | 800 | function calculate_branch_name($document, $userid) 801 | { 802 | return "edit-$userid-$document"; 803 | } 804 | 805 | function find_existing_pull_request($document, $userid) 806 | { 807 | global $raw_data; 808 | $branch = calculate_branch_name($document, $userid); 809 | $escbranch = escapeshellarg("refs/heads/$branch"); 810 | $escrawdata = escapeshellarg($raw_data); 811 | $cmd = "cd $escrawdata && git show-ref --verify --quiet $escbranch"; 812 | unset($output); 813 | $failed = (exec($cmd, $output, $result) === false); 814 | if ($failed) { 815 | fail503('Failed to lookup existing edit branch in git; please try again later.'); 816 | } 817 | return ($result == 0) ? $branch : NULL; 818 | } 819 | 820 | 821 | function make_new_page_version($page, $ext, $newtext, $comment) 822 | { 823 | global $raw_data, $git_commit_message_file, $git_committer_user, $github_url, $base_url; 824 | global $github_committer_token, $github_repo_owner, $github_repo, $supported_formats; 825 | global $github_repo_main_branch; 826 | 827 | force_authorize_with_github(); // only returns if we are authorized. 828 | 829 | $newtext = str_replace("\r\n", "\n", $newtext); 830 | $comment = str_replace("\r\n", "\n", $comment); 831 | 832 | $gitfname = "$raw_data/$page.$ext"; 833 | $escpage = escapeshellarg("$page.$ext"); 834 | $escrawdata = escapeshellarg($raw_data); 835 | 836 | $escmain = escapeshellarg($github_repo_main_branch); 837 | $trusted_author = $_SESSION['is_trusted'] || $_SESSION['is_admin']; // trusted/admin authors push right to main. Untrusted authors generate pull requests. 838 | $author = $_SESSION['github_name'] . ' <' . $_SESSION['github_email'] . '>'; 839 | $escauthor = escapeshellarg($author); 840 | $escmsgfile = escapeshellarg($git_commit_message_file); 841 | 842 | //if ($_SESSION['github_user'] == 'icculus') { $trusted_author = false; } // uncomment for debugging purposes. 843 | 844 | obtain_git_repo_lock(); 845 | 846 | $existing_branch = find_existing_pull_request($page, $_SESSION['github_id']); 847 | $branch = $existing_branch; 848 | if ($existing_branch == NULL) { 849 | $branch = calculate_branch_name($page, $_SESSION['github_id']); 850 | $escbranch = escapeshellarg($branch); 851 | } else { 852 | $escbranch = escapeshellarg($branch); 853 | $cmd = "( cd $escrawdata && git checkout $escbranch ) 2>&1"; 854 | unset($output); 855 | if ((exec($cmd, $output, $result) === false) || ($result != 0)) { 856 | exec("cd $escrawdata && git checkout $escmain"); 857 | fail503('Failed to checkout topic branch from git. Please try again later.'); 858 | } 859 | } 860 | 861 | if ($comment == '') { 862 | $comment = is_readable($gitfname) ? 'Updated.' : 'Added.'; 863 | } 864 | 865 | $comment = "$page: $comment"; 866 | $full_comment = "$comment\n\nLive page is here: $base_url/$page\n\n"; 867 | 868 | if (file_put_contents($git_commit_message_file, $full_comment) != strlen($full_comment)) { 869 | unlink($git_commit_message_file); // just in case. 870 | exec("cd $escrawdata && git checkout $escmain"); 871 | fail503('Failed to write new content to disk. Please try again later.'); 872 | } 873 | 874 | @mkdir(dirname($gitfname)); 875 | if (file_put_contents($gitfname, $newtext) != strlen($newtext)) { 876 | unlink($git_commit_message_file); 877 | exec("cd $escrawdata && git checkout -- $escpage && git checkout $escmain"); 878 | fail503('Failed to write new content to disk. Please try again later.'); 879 | } 880 | 881 | // remove files if the file extension changed. 882 | $rmcmd = ''; 883 | foreach ($supported_formats as $findext => $format) { 884 | if ($ext != $findext) { 885 | $rmpage = "$page.$findext"; 886 | if (file_exists("$raw_data/$rmpage")) { 887 | $escrmpage = escapeshellarg($rmpage); 888 | $rmcmd .= " && git rm $escrmpage"; 889 | } 890 | } 891 | } 892 | 893 | $hash = ''; 894 | $cmd = ''; 895 | 896 | if ($existing_branch != NULL) { 897 | $cmd = "( cd $escrawdata && git add $escpage $rmcmd && git commit -F $escmsgfile --author=$escauthor && git push && git checkout $escmain) 2>&1"; 898 | } else if ($trusted_author) { 899 | $cmd = "( cd $escrawdata && git checkout $escmain && git add $escpage $rmcmd && git commit -F $escmsgfile --author=$escauthor && git push ) 2>&1"; 900 | } else { 901 | $cmd = "( cd $escrawdata && git checkout -b $escbranch && git add $escpage $rmcmd && git commit -F $escmsgfile --author=$escauthor && git push --set-upstream origin $escbranch && git checkout $escmain) 2>&1"; 902 | } 903 | 904 | unset($output); 905 | $failed = (exec($cmd, $output, $result) === false) || ($result != 0); 906 | unlink($git_commit_message_file); 907 | 908 | if (!$failed && $trusted_author) { 909 | $hash = `cd $escrawdata ; git show-ref -s HEAD`; 910 | } 911 | 912 | exec("cd $escrawdata && git checkout $escmain && ( git reset --hard HEAD ; git clean -df )"); // just in case. 913 | 914 | $cooked = ''; 915 | if ($trusted_author && ($existing_branch == NULL)) { 916 | $cooked = cook_page($page); // this is going to recook in a moment when GitHub sends a push notification, but let's keep the state sane. 917 | } else { 918 | $cooked = cook_string($newtext, $supported_formats[$ext]); 919 | } 920 | 921 | release_git_repo_lock(); 922 | 923 | if ($failed) { 924 | $str = "\n
Output:
\n
\n";
 925 |         foreach ($output as $l) {
 926 |             $str .= "$l\n";
 927 |         }
 928 |         $str .= "
\n"; 929 | fail503("Failed to push your changes. Please try again later.$str"); 930 | } 931 | 932 | if ($existing_branch != NULL) { 933 | print_template('added_to_pull_request', [ 'branch' => $branch, 'cooked' => fixup_preview_links($page, $cooked) ]); 934 | } else if ($trusted_author) { 935 | print_template('pushed_to_main', [ 'hash' => $hash, 'commiturl' => "$github_url/commit/$hash", 'cooked' => fixup_preview_links($page, $cooked) ]); 936 | } else { // generate a pull request so we can review before applying. 937 | $user = $_SESSION['github_user']; 938 | $body = "This edit was made by @{$user}.\n\n" . 939 | "Live page is here: $base_url/$page\n\n" . 940 | "If this user should be blocked from further edits, an admin should go to $base_url/$user/block\n" . 941 | "If this user should be trusted to make direct pushes to $github_repo_main_branch, without a pull request, an admin should go to $base_url/$user/trust\n\n" . 942 | "WHETHER YOU MERGE OR REJECT THIS PULL REQUEST, DON'T FORGET TO DELETE THE BRANCH. Otherwise, $user won't be able to start a new PR for this page.\n"; 943 | $response = call_github_api("https://api.github.com/repos/$github_repo_owner/$github_repo/pulls", 944 | [ 'head' => $branch, 945 | 'base' => $github_repo_main_branch, 946 | 'title' => "$comment", 947 | 'body' => $body, 948 | 'maintainer_can_modify' => true, 949 | 'draft' => false ], 950 | $github_committer_token, true); 951 | print_template('made_pull_request', [ 'branch' => $branch, 'prurl' => $response['html_url'], 'cooked' => fixup_preview_links($page, $cooked) ]); 952 | } 953 | } 954 | 955 | // !!! FIXME: a lot of copy/paste from make_new_page_version 956 | function delete_page($page, $comment) 957 | { 958 | global $cooked_data, $raw_data, $git_commit_message_file, $git_committer_user, $github_url; 959 | global $github_committer_token, $github_repo_owner, $github_repo, $supported_formats, $base_url; 960 | global $github_repo_main_branch; 961 | 962 | force_authorize_with_github(); // only returns if we are authorized. 963 | 964 | $comment = str_replace("\r\n", "\n", $comment); 965 | 966 | if ($comment == '') { 967 | $comment = 'Deleted.'; 968 | } 969 | 970 | $comment = "$page: $comment"; 971 | 972 | $escrawdata = escapeshellarg($raw_data); 973 | 974 | obtain_git_repo_lock(); 975 | 976 | if (file_put_contents($git_commit_message_file, $comment) != strlen($comment)) { 977 | unlink($git_commit_message_file); // just in case. 978 | fail503('Failed to write new content to disk. Please try again later.'); 979 | } 980 | 981 | $now = time(); 982 | $branch = "delete-" . $_SESSION['github_user'] . '-' . $now; 983 | $author = $_SESSION['github_name'] . ' <' . $_SESSION['github_email'] . '>'; 984 | $escbranch = escapeshellarg($branch); 985 | $escauthor = escapeshellarg($author); 986 | $escmsgfile = escapeshellarg($git_commit_message_file); 987 | 988 | $rmcmd = ''; 989 | foreach ($supported_formats as $findext => $format) { 990 | $rmpage = "$page.$findext"; 991 | if (file_exists("$raw_data/$rmpage")) { 992 | $escrmpage = escapeshellarg($rmpage); 993 | $rmcmd .= " $escrmpage"; 994 | } 995 | } 996 | 997 | if ($rmcmd == '') { 998 | fail400("No such page to delete."); 999 | } 1000 | 1001 | $hash = ''; 1002 | $cmd = ''; 1003 | 1004 | $trusted_author = $_SESSION['is_trusted'] || $_SESSION['is_admin']; // trusted/admin authors push right to main. Untrusted authors generate pull requests. 1005 | $escmain = $github_repo_main_branch; 1006 | if ($trusted_author) { 1007 | $cmd = "( cd $escrawdata && git checkout $escmain && git rm $rmcmd && git commit -F $escmsgfile --author=$escauthor && git push ) 2>&1"; 1008 | } else { 1009 | $cmd = "( cd $escrawdata && git checkout -b $escbranch && git rm $rmcmd && git commit -F $escmsgfile --author=$escauthor && git push --set-upstream origin $escbranch && git checkout $escmain && git branch -d $escbranch ) 2>&1"; 1010 | } 1011 | 1012 | unset($output); 1013 | $failed = (exec($cmd, $output, $result) === false) || ($result != 0); 1014 | unlink($git_commit_message_file); 1015 | 1016 | if (!$failed && $trusted_author) { 1017 | $hash = `cd $escrawdata ; git show-ref -s HEAD`; 1018 | } 1019 | 1020 | exec("cd $escrawdata && git checkout $escmain && ( git reset --hard HEAD ; git clean -df )"); // just in case. 1021 | 1022 | release_git_repo_lock(); 1023 | 1024 | if ($failed) { 1025 | $str = "\n
Output:
\n
\n";
1026 |         foreach ($output as $l) {
1027 |             $str .= "$l\n";
1028 |         }
1029 |         $str .= "
\n"; 1030 | fail503("Failed to push your changes. Please try again later.$str"); 1031 | } 1032 | 1033 | $cooked = '[page deleted]'; 1034 | 1035 | if ($trusted_author) { 1036 | print_template('pushed_to_main', [ 'hash' => $hash, 'commiturl' => "$github_url/commit/$hash", 'cooked' => $cooked ]); 1037 | } else { // generate a pull request so we can review before applying. 1038 | $user = $_SESSION['github_user']; 1039 | $body = "This edit was made by @{$user}.\n\n" . 1040 | "Live page is here: $base_url/$page\n\n" . 1041 | "If this user should be blocked from further edits, an admin should go to $base_url/$user/block\n" . 1042 | "If this user should be trusted to make direct pushes to $github_repo_main_branch, without a pull request, an admin should go to $base_url/$user/trust\n\n" . 1043 | "WHETHER YOU MERGE OR REJECT THIS PULL REQUEST, DON'T FORGET TO DELETE THE BRANCH. Otherwise, $user won't be able to start a new PR for this page.\n"; 1044 | $response = call_github_api("https://api.github.com/repos/$github_repo_owner/$github_repo/pulls", 1045 | [ 'head' => $branch, 1046 | 'base' => $github_repo_main_branch, 1047 | 'title' => "$comment", 1048 | 'body' => $body, 1049 | 'maintainer_can_modify' => true, 1050 | 'draft' => false ], 1051 | $github_committer_token, true); 1052 | print_template('made_pull_request', [ 'branch' => $branch, 'prurl' => $response['html_url'], 'cooked' => $cooked ]); 1053 | } 1054 | } 1055 | 1056 | function must_be_admin() 1057 | { 1058 | force_authorize_with_github(); // only returns if we are authorized. 1059 | if (!$_SESSION['is_admin']) { 1060 | fail400('This is only available to admins.'); 1061 | } 1062 | } 1063 | 1064 | function perform_action_on_user($action, $statedir, $user, $is_do) // !$is_do == undo. 1065 | { 1066 | global $github_committer_token, $wikiname; 1067 | 1068 | must_be_admin(); 1069 | 1070 | $response = call_github_api("https://api.github.com/users/$user", NULL, $github_committer_token); 1071 | 1072 | // we block by user id number, not username, so the user can't evade blocking by renaming the account. 1073 | $id = isset($response['id']) ? $response['id'] : ''; 1074 | if ($id == '') { 1075 | fail503('Bogus user id from GitHub API. Try again later.'); 1076 | } 1077 | 1078 | $fname = "$statedir/$id"; 1079 | if ($is_do) { 1080 | $str = "$user, {$action}ed by " . $_SESSION['github_user'] . "\n"; 1081 | @mkdir($statedir); // don't care if this fails, it's probably EEXIST, and we'll catch the file_put_contents failure anyhow. 1082 | if (file_put_contents($fname, $str) != strlen($str)) { 1083 | fail503("Failed to mark user as {$action}ed. Please try again later."); 1084 | } 1085 | } else { 1086 | if (file_exists($fname) && !unlink($fname)) { 1087 | fail503("Failed to mark user as un{$action}ed. Please try again later."); 1088 | } 1089 | } 1090 | 1091 | print_template('action_on_user_complete', [ 1092 | 'action' => $is_do ? "{$action}ed" : "un{$action}ed", 1093 | 'revaction' => $action, 1094 | 'user' => $user, 1095 | 'username' => ($response['name'] != NULL) ? $response['name'] : $user, 1096 | 'user_avatar' => $response['avatar_url'], 1097 | 'title' => "@$user {$action}ed - $wikiname" 1098 | ]); 1099 | } 1100 | 1101 | function confirm_action_on_user($action, $user, $statedir, $explanation) 1102 | { 1103 | global $wikiname; 1104 | 1105 | must_be_admin(); // only returns if we are an admin. 1106 | $response = call_github_api("https://api.github.com/users/$user", NULL, $github_committer_token); 1107 | $is_do = file_exists("$statedir/" . $response['id']); 1108 | print_template('action_on_user_confirmation', [ 1109 | 'currently' => $is_do ? "{$action}ed" : "un{$action}ed", 1110 | 'action' => $is_do ? "un{$action}" : $action, 1111 | 'user' => $user, 1112 | 'username' => ($response['name'] != NULL) ? $response['name'] : $user, 1113 | 'user_avatar' => $response['avatar_url'], 1114 | 'explanation' => $explanation, 1115 | 'title' => "$action @$user?- $wikiname" 1116 | ]); 1117 | } 1118 | 1119 | // this does not hold the git repo lock (but YOU SHOULD) and does not 1120 | // sort the output (BUT YOU SHOULD). 1121 | function build_index($base, $dname, &$output) 1122 | { 1123 | global $supported_formats; 1124 | 1125 | //print("build_index base='$base' dname='$dname'\n"); 1126 | 1127 | $dirp = opendir($dname); 1128 | if ($dirp === false) { 1129 | fail503('Failed to read directory index; please try again later.'); 1130 | } 1131 | 1132 | $sep = ($base == NULL) ? '' : '/'; 1133 | 1134 | while (($dent = readdir($dirp)) !== false) { 1135 | //print("dent='$dent'\n"); 1136 | if (substr($dent, 0, 1) == '.') { 1137 | continue; // skip ".", "..", and metadata. 1138 | } else if (is_dir("$dname/$dent")) { 1139 | build_index("$base$sep$dent", "$dname/$dent", $output); 1140 | continue; 1141 | } else if (preg_match('/^(.*)\.(.*)$/', $dent, $matches) != 1) { 1142 | continue; 1143 | } 1144 | $pagename = $matches[1]; 1145 | $ext = $matches[2]; 1146 | if (!isset($supported_formats[$ext])) { continue; } 1147 | //print("Adding page '$base$sep$pagename'\n"); 1148 | $output[] = "$base$sep$pagename"; 1149 | } 1150 | 1151 | closedir($dirp); 1152 | 1153 | //print("done with base='$base'\n"); 1154 | 1155 | return $output; 1156 | } 1157 | 1158 | 1159 | $search_query = ''; 1160 | function sort_search_results($a, $b) 1161 | { 1162 | $retval = strcasecmp($a, $b); 1163 | if ($retval != 0) { 1164 | $bias = 0; 1165 | 1166 | global $search_query; 1167 | $abase = (strcasecmp(basename($a), $search_query) == 0); 1168 | $bbase = (strcasecmp(basename($b), $search_query) == 0); 1169 | if ($abase && $bbase) { 1170 | $retval = ($bias != 0) ? $bias : $retval; 1171 | } else if ($abase) { 1172 | $retval = -1; // exact page name match? Move it to the front. 1173 | } else if ($bbase) { 1174 | $retval = 1; // exact page name match? Move it to the front. 1175 | } else if ($bias != 0) { 1176 | $retval = $bias; 1177 | } 1178 | } 1179 | 1180 | return $retval; 1181 | } 1182 | 1183 | 1184 | // Main line! 1185 | 1186 | // can't have a '/' at the end of the URL or we'll generate incorrect links. 1187 | $stripped_url = preg_replace('/\/+$/', '', $_SERVER['PHP_SELF'], -1, $count); 1188 | if ($count && ($stripped_url != '')) { 1189 | redirect($stripped_url); 1190 | } 1191 | unset($count); 1192 | 1193 | $reqargs = explode('/', preg_replace('/^\/?(.*?)\/?$/', '$1', $_SERVER['PHP_SELF'])); 1194 | $reqargcount = count($reqargs); 1195 | 1196 | if ( ($reqargcount < 1) || ($reqargs[0] == '') ) { 1197 | $document = 'FrontPage'; // Feed the main wiki page by default. 1198 | } else { 1199 | // eat arguments until we run out of existing subdirectories... 1200 | $document = ''; 1201 | $sep = ''; 1202 | $drop_args = 0; 1203 | foreach ($reqargs as $a) { 1204 | $document .= "$sep$a"; 1205 | $sep = '/'; # set this after first item so there's a separator everywhere but at the start of the string. 1206 | if (substr($a, 0, 1) == '.') { // Make sure we don't catch ".git" or ".." or whatever. 1207 | fail400("Invalid page name '$document'"); 1208 | } 1209 | 1210 | // Note that this (intentionally!) will not let you create files in subdirectories that don't exist. 1211 | // Creating a new subdir should be a significant admin-controlled event. Push through git directly for now. 1212 | // We can add UI for it later if we want. 1213 | if (is_dir("$raw_data/$document")) { 1214 | $drop_args++; 1215 | } else { 1216 | break; // We're done looking for subdirs. 1217 | } 1218 | } 1219 | 1220 | // drop subdirs from $reqargs, replace $reqargs[0] with the full document path just in case. 1221 | if ($drop_args > 0) { 1222 | $reqargcount -= $drop_args; 1223 | if ($reqargcount == 0) { 1224 | // each subdir gets a default FrontPage. Redirect to it so there's 1225 | // no confusion about relative URLs on the page going to the right subdir. 1226 | redirect("$stripped_url/FrontPage"); 1227 | } else { 1228 | while ($drop_args > 0) { 1229 | array_shift($reqargs); // drop subdirs from $reqargs 1230 | $drop_args--; 1231 | } 1232 | $reqargs[0] = $document; 1233 | } 1234 | } 1235 | 1236 | // Drop Markdown and MediaWiki file extensions, so bridged documentation can refer to files and have it work on the wiki, too. 1237 | $document = preg_replace('/\.(md|mediawiki)$/', '', $document, -1, $count); 1238 | if ($count == 1) { 1239 | $reqargs[0] = $document; 1240 | redirect("$base_url/" . implode('/', $reqargs)); 1241 | } 1242 | } 1243 | 1244 | $operation = ($reqargcount >= 2) ? $reqargs[1] : 'view'; 1245 | 1246 | if ($operation == 'view') { // just serve the existing page. 1247 | // !!! FIXME: GitHub doesn't redirect properly if you click "Cancel" on their OAuth page. :/ 1248 | handle_oauth_error(); 1249 | 1250 | $published_path = "$cooked_data/$document.html"; 1251 | 1252 | $preamble = ''; // unused, but you can hack something into this source code if you need it! 1253 | $cooked = ''; 1254 | 1255 | if (!file_exists($published_path)) { 1256 | fail404('This page does not exist...yet!', 'not_yet_a_page'); 1257 | } else { 1258 | print_template('view', [ 'cooked' => file_get_contents($published_path), 'preamble' => $preamble ]); 1259 | } 1260 | 1261 | } else if ($operation == 'raw') { // just serve the existing raw page. 1262 | foreach ($supported_formats as $ext => $format) { 1263 | $raw = @file_get_contents("$raw_data/$document.$ext"); 1264 | if ($raw !== false) { 1265 | header('Content-Type: text/plain; charset=utf-8'); 1266 | print($raw); 1267 | print("\n"); 1268 | exit(0); 1269 | } 1270 | } 1271 | 1272 | print_template('not_yet_a_page'); 1273 | 1274 | } else if ($operation == 'edit') { 1275 | authorize_with_github(); // only returns if we are authorized. 1276 | 1277 | $template_vars = array( 'title' => "Edit $document - $wikiname" ); 1278 | 1279 | obtain_git_repo_lock(); 1280 | 1281 | $existing_branch = find_existing_pull_request($document, $_SESSION['github_id']); 1282 | 1283 | $content_choice = NULL; 1284 | if (($existing_branch != NULL) && (isset($_POST['use_content_choice']))) { 1285 | $content_choice = $_POST['use_content_choice']; 1286 | if (($content_choice != 'use_current_page') && ($content_choice != 'use_last_changes')) { 1287 | $content_choice = NULL; 1288 | } 1289 | } 1290 | 1291 | $template_vars['existing_branch'] = ($existing_branch != NULL) ? $existing_branch : ''; 1292 | 1293 | if (($existing_branch != NULL) && ($content_choice == NULL)) { 1294 | release_git_repo_lock(); 1295 | print_template('edit_existing_pull_request', $template_vars); 1296 | } else if (($existing_branch != NULL) && ($content_choice == 'use_last_changes')) { 1297 | $escbranch = escapeshellarg($existing_branch); 1298 | $escdocument = escapeshellarg($document); 1299 | $escrawdata = escapeshellarg($raw_data); 1300 | 1301 | $raw = false; 1302 | $from_format = false; 1303 | foreach ($supported_formats as $ext => $format) { 1304 | $selectedstr = 'fmt_' . $ext . '_selected'; 1305 | $template_vars[$selectedstr] = ''; 1306 | if ($raw === false) { 1307 | $cmd = "cd $escrawdata && git cat-file -p $escbranch:$escdocument.$ext 2>/dev/null"; 1308 | unset($output); 1309 | $failed = (exec($cmd, $output, $result) === false); 1310 | if ($failed) { 1311 | fail503('Failed to retrieve latest changes; please try again later.'); 1312 | } else if ($result == 0) { 1313 | $raw = implode("\n", $output); 1314 | $from_format = $format; 1315 | $template_vars[$selectedstr] = 'selected'; 1316 | } 1317 | } 1318 | } 1319 | 1320 | release_git_repo_lock(); 1321 | 1322 | $template_vars['cooked'] = ($raw === false) ? '' : fixup_preview_links($document, cook_string($raw, $from_format)); 1323 | $template_vars['raw'] = ($raw === false) ? '' : $raw; 1324 | print_template('edit', $template_vars); 1325 | 1326 | } else { // no existing branch, or $content_choice == 'use_current_page' 1327 | $cooked = @file_get_contents("$cooked_data/$document.html"); 1328 | 1329 | $raw = false; 1330 | foreach ($supported_formats as $ext => $format) { 1331 | $selectedstr = 'fmt_' . $ext . '_selected'; 1332 | $template_vars[$selectedstr] = ''; 1333 | if ($raw === false) { 1334 | $raw = @file_get_contents("$raw_data/$document.$ext"); 1335 | if ($raw !== false) { 1336 | $template_vars[$selectedstr] = 'selected'; 1337 | } 1338 | } 1339 | } 1340 | 1341 | release_git_repo_lock(); 1342 | 1343 | $template_vars['cooked'] = ($cooked === false) ? '' : fixup_preview_links($document, $cooked); 1344 | $template_vars['raw'] = ($raw === false) ? '' : $raw; 1345 | print_template('edit', $template_vars); 1346 | } 1347 | 1348 | } else if ($operation == 'post') { 1349 | // don't lose the changes if GitHub forces a redirect. 1350 | require_session(); 1351 | 1352 | if (isset($_POST['newversion'])) { 1353 | $_SESSION['post_newversion'] = $_POST['newversion']; 1354 | } 1355 | 1356 | if (isset($_POST['comment'])) { 1357 | $_SESSION['post_comment'] = $_POST['comment']; 1358 | } 1359 | 1360 | if (isset($_POST['format'])) { 1361 | $_SESSION['post_format'] = $_POST['format']; 1362 | } 1363 | 1364 | if (!isset($_SESSION['post_newversion'])) { 1365 | fail400("New version of page was not posted with this request. Try editing again?"); 1366 | } 1367 | 1368 | $data = $_SESSION['post_newversion']; 1369 | $comment = isset($_SESSION['post_comment']) ? $_SESSION['post_comment'] : ''; 1370 | $format = isset($_SESSION['post_format']) ? $_SESSION['post_format'] : 'md'; 1371 | make_new_page_version($document, $format, $data, $comment); 1372 | 1373 | unset($_SESSION['post_newversion']); 1374 | unset($_SESSION['post_comment']); 1375 | unset($_SESSION['post_format']); 1376 | 1377 | } else if ($operation == 'delete') { 1378 | authorize_with_github(); // only returns if we are authorized. 1379 | print_template('delete_confirmation', [ 'title' => "Delete $document? - $wikiname" ]); 1380 | 1381 | } else if ($operation == 'postdelete') { 1382 | // don't lose the changes if GitHub forces a redirect. 1383 | require_session(); 1384 | 1385 | if (isset($_POST['comment'])) { 1386 | $_SESSION['post_comment'] = $_POST['comment']; 1387 | } 1388 | 1389 | $comment = isset($_SESSION['post_comment']) ? $_SESSION['post_comment'] : ''; 1390 | delete_page($document, $comment); 1391 | 1392 | unset($_SESSION['post_comment']); 1393 | 1394 | } else if ($operation == 'format') { 1395 | // ask the server to cook a string of Markdown/MediaWiki/whatever on the fly for live previews. 1396 | 1397 | // see if user has an access token (but don't validate it) to prevent server load; drive-by 1398 | // bots can't use this unless they have auth'd with github at some point before, like when 1399 | // they clicked the 'edit' link right before they would need this. 1400 | require_session(); 1401 | if (!isset($_SESSION['github_access_token'])) { 1402 | fail400("live previews only available when logged into GitHub."); 1403 | } 1404 | 1405 | $data = isset($_POST['raw']) ? $_POST['raw'] : ''; 1406 | $format = isset($_POST['format']) ? $_POST['format'] : 'md'; 1407 | $pandoc_format = isset($supported_formats[$format]) ? $supported_formats[$format] : NULL; 1408 | 1409 | if ($pandoc_format == NULL) { 1410 | fail400('Unsupported document format'); 1411 | } 1412 | 1413 | if ($data != '') { 1414 | $data = str_replace("\r\n", "\n", $data); 1415 | $data = cook_string($data, $pandoc_format); 1416 | $data = fixup_preview_links($document, $data); 1417 | print($data); 1418 | } 1419 | } else if ($operation == 'history') { 1420 | foreach ($supported_formats as $ext => $format) { 1421 | if (file_exists("$raw_data/$document.$ext")) { 1422 | redirect("$github_url/commits/$github_repo_main_branch/$document.$ext"); 1423 | } 1424 | } 1425 | fail404("No such page '$document'"); 1426 | 1427 | } else if ($operation == 'block') { 1428 | confirm_action_on_user($operation, $document, $blocked_data, 'Blocked users may not make edits to the wiki, but may still read it.'); 1429 | 1430 | } else if (($operation == 'block_confirm') || ($operation == 'unblock_confirm')) { 1431 | perform_action_on_user('block', $blocked_data, $document, ($operation == 'block_confirm')); 1432 | 1433 | } else if ($operation == 'trust') { 1434 | confirm_action_on_user($operation, $document, $trusted_data, "Trusted users' edits are pushed directly to $github_repo_main_branch without generating pull requests."); 1435 | 1436 | } else if (($operation == 'trust_confirm') || ($operation == 'untrust_confirm')) { 1437 | perform_action_on_user('trust', $trusted_data, $document, ($operation == 'trust_confirm')); 1438 | 1439 | } else if ($operation == 'admin') { 1440 | confirm_action_on_user($operation, $document, $admin_data, "Admins can change wiki and user settings, and push changes directly to $github_repo_main_branch without generating pull requests."); 1441 | 1442 | } else if (($operation == 'admin_confirm') || ($operation == 'unadmin_confirm')) { 1443 | perform_action_on_user('admin', $admin_data, $document, ($operation == 'admin_confirm')); 1444 | 1445 | } else if ($operation == 'index') { 1446 | $pagelist = array(); 1447 | build_index(NULL, $raw_data, $pagelist); 1448 | $htmllist = ''; 1449 | asort($pagelist, SORT_STRING|SORT_FLAG_CASE); 1450 | foreach ($pagelist as $p) { 1451 | $htmllist .= "
  • $p
  • \n"; 1452 | } 1453 | 1454 | print_template('index', [ 'title' => "Index of all pages - $wikiname", 'htmllist' => $htmllist ]); 1455 | 1456 | } else if ($operation == 'search') { 1457 | // !!! FIXME: move this to a separate function. 1458 | // !!! FIXME: maybe limit searches to people auth'd with GitHub to prevent server load from crawlers? 1459 | $search_query = isset($_REQUEST['q']) ? $_REQUEST['q'] : ''; // let this be a GET option so people can post search URLs. 1460 | $htmllist = ''; 1461 | $htmlquery = ''; 1462 | $queryurl = ''; 1463 | $hideifblank = 'none'; 1464 | if ($search_query != '') { 1465 | $hideifblank = 'inline'; 1466 | $htmlquery = htmlspecialchars($search_query, ENT_QUOTES|ENT_SUBSTITUTE|ENT_DISALLOWED|ENT_HTML5, 'UTF-8'); 1467 | $queryurl = rawurlencode($search_query); 1468 | $escquery = escapeshellarg($search_query); 1469 | $cmd = "csearch -i $escquery"; 1470 | unset($output); 1471 | $failed = (exec($cmd, $output, $result) === false); 1472 | if ($failed) { 1473 | fail503('Failed to run search query; please try again later.'); 1474 | } 1475 | 1476 | $pagehits = array(); 1477 | foreach ($output as $l) { 1478 | if (preg_match("/^.*\/$raw_data\/(.*)\..*?\:(.*)$/", $l, $matches) != 1) { continue; } 1479 | $p = $matches[1]; 1480 | $txt = $matches[2]; 1481 | 1482 | // skip the title on the actual page 1483 | if (preg_match("/^# $search_query$/", $txt, $matches) == 1) { continue; } 1484 | if (preg_match("/^= $search_query =$/", $txt, $matches) == 1) { continue; } 1485 | 1486 | // skip the redirect pages (these are only in Markdown format). 1487 | if (preg_match("/^Please refer to \[.*?\]\(.*?\) for details.$/", $txt, $matches) == 1) { continue; } 1488 | 1489 | // skip what are probably See Also links. 1490 | if ((preg_match("/^\- \[(.*?)\]\((.*?)\)$/", $txt, $matches) == 1) && ($matches[1] == $matches[2])) { continue; } 1491 | if ((preg_match("/^\* \[\[(.*?)\]\]$/", $txt, $matches) == 1)) { continue; } 1492 | 1493 | if (!isset($pagehits[$p])) { 1494 | $pagehits[$p] = array(); 1495 | } 1496 | $pagehits[$p][] = $txt; 1497 | } 1498 | 1499 | if (count($pagehits) == 0) { 1500 | $htmllist .= "
  • No results found.
  • \n"; 1501 | } else { 1502 | uksort($pagehits, "sort_search_results"); 1503 | 1504 | foreach ($pagehits as $p => $txt) { 1505 | $htmllist .= "
  • $p:
    \n"; 1506 | $first = true; 1507 | foreach ($txt as $t) { 1508 | $htmltxt = $t; //htmlspecialchars($t, ENT_QUOTES|ENT_SUBSTITUTE|ENT_DISALLOWED|ENT_HTML5, 'UTF-8'); 1509 | if ($first) { 1510 | $first = false; 1511 | } else { 1512 | $htmllist .= "\n
    ...
    \n"; 1513 | } 1514 | $htmllist .= "
    $htmltxt
    "; 1515 | } 1516 | $htmllist .= "\n
  • \n"; 1517 | } 1518 | } 1519 | } 1520 | print_template('search', [ 'title' => "Search - $wikiname", 'query' => $htmlquery, 'queryurl' => $queryurl, 'htmllist' => $htmllist, 'hideifblank' => $hideifblank ]); 1521 | 1522 | } else if ($operation == 'logout') { 1523 | require_session(); 1524 | $_SESSION = array(); // nuke everything. 1525 | redirect("/$document", 'You have been logged out.'); 1526 | 1527 | } else if ($operation == 'webhook') { 1528 | github_webhook(); // GitHub POSTs here whenever the wiki repo is pushed to. 1529 | 1530 | } else if ($operation == 'recook') { // !!! FIXME: this should be a recookall, and "recook" should just redo $document. 1531 | must_be_admin(); 1532 | 1533 | // !!! FIXME: code duplication with the GitHub webhook. 1534 | 1535 | header('Content-Type: text/plain; charset=utf-8'); 1536 | $str = "\nRECOOKING...\n\n"; 1537 | 1538 | $starttime = microtime(true); 1539 | $updated = array(); 1540 | $failed = false; // !!! FIXME: recook_tree returns void atm 1541 | recook_tree($raw_data, $cooked_data, '', $updated); 1542 | foreach ($updated as $f) { 1543 | $str .= "$f\n"; 1544 | } 1545 | $str .= "\n"; 1546 | 1547 | if ($failed) { 1548 | fail503("$str\nFAILED TO RECOOK DATA!\n", 'fail_plaintext'); 1549 | } 1550 | 1551 | recook_search_index(); 1552 | 1553 | $totaltime = (int) (microtime(true) - $starttime); 1554 | $str .= "\nTREE RECOOKED IN $totaltime SECONDS.\n"; 1555 | print("$str\n"); 1556 | 1557 | } else { 1558 | fail400("Unknown operation '$operation'"); 1559 | } 1560 | 1561 | exit(0); 1562 | ?> 1563 | 1564 | --------------------------------------------------------------------------------