PageRenderTime 33ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/dylan/main.dylan

http://github.com/cgay/wiki
Unknown | 454 lines | 412 code | 42 blank | 0 comment | 0 complexity | 10612c4e82b7e3df76187c8fc7a383f2 MD5 | raw file
  1. Module: %wiki
  2. // These are sent as the text and URL for the Atom feed generator element.
  3. define variable *site-name* :: <string> = "Dylan Wiki";
  4. define variable *site-url* :: <string> = "";
  5. // The realm used for authentication. Configurable.
  6. define variable *wiki-realm* :: <string> = "wiki";
  7. define variable *mail-host* = #f;
  8. define variable *mail-port* :: <integer> = $default-smtp-port;
  9. define constant $administrator-user-name :: <string> = "administrator";
  10. /// This is called when the <wiki> element in the config file is
  11. /// processed.
  12. define sideways method process-config-element
  13. (server :: <http-server>, node :: xml/<element>, name == #"wiki")
  14. // TODO(cgay): error out if any of the files configured here don't exist,
  15. // including executables.
  16. let git-exe = get-attr(node, #"git-executable")
  17. | "git";
  18. let wiki-root = get-attr(node, #"wiki-root")
  19. | error("The wiki-root setting is required.");
  20. let wiki-root-directory = as(<directory-locator>, wiki-root);
  21. let main-root = get-attr(node, #"git-main-repository-root")
  22. | error("The git-main-repository-root setting is required.");
  23. let user-root = get-attr(node, #"git-user-repository-root")
  24. | error("The git-user-repository-root setting is required.");
  25. *storage* := make(<git-storage>,
  26. repository-root: as(<directory-locator>, main-root),
  27. user-repository-root: as(<directory-locator>, user-root),
  28. executable: as(<file-locator>, git-exe));
  29. initialize-storage-for-reads(*storage*);
  30. local method child-node-named (name)
  31. block (return)
  32. for (child in xml/node-children(node))
  33. if (xml/name(child) = name)
  34. return(child);
  35. end;
  36. end;
  37. end;
  38. end;
  39. let admin-element = child-node-named(#"administrator");
  40. if (~admin-element)
  41. error("An <administrator> element must be specified in the config file.");
  42. end;
  43. let (admin-user, changed?) = process-administrator-configuration(admin-element);
  44. initialize-storage-for-writes(*storage*, admin-user);
  45. if (changed?)
  46. store(*storage*, admin-user, admin-user, "Change due to config file edit",
  47. standard-meta-data(admin-user, "edit"));
  48. end;
  49. *admin-user* := admin-user;
  50. *users*[admin-user.user-name] := admin-user;
  51. *site-name* := get-attr(node, #"site-name") | *site-name*;
  52. log-info("Site name: %s", *site-name*);
  53. *site-url* := get-attr(node, #"site-url") | *site-url*;
  54. // TODO: set site-url to http://<local host name>:<port>
  55. log-info("Site URL: %s", *site-url*);
  56. *wiki-url-prefix* := get-attr(node, #"url-prefix") | *wiki-url-prefix*;
  57. log-info("Wiki URL prefix: %s", *wiki-url-prefix*);
  58. *static-directory* := subdirectory-locator(wiki-root-directory, "www");
  59. *template-directory* := subdirectory-locator(*static-directory*, "dsp");
  60. log-info("Wiki static directory: %s", *static-directory*);
  61. let auth-element = child-node-named(#"authentication");
  62. if (auth-element)
  63. process-authentication-configuration(auth-element);
  64. end;
  65. let mail-element = child-node-named(#"mail");
  66. if (mail-element)
  67. process-mail-configuration(mail-element);
  68. else
  69. error("A <mail> element must be specified in the config file.");
  70. end;
  71. *python-executable* := get-attr(node, #"python-executable")
  72. | error("The 'python-executable' attribute must be specified in the 'wiki' "
  73. "config file element.");
  74. *rst2html* := get-attr(node, #"rst2html")
  75. | error("The 'rst2html' attribute must be specified in the 'wiki' "
  76. "config file element.");
  77. *rst2html-template* := as(<string>,
  78. merge-locators(as(<file-locator>, "rst2html-template.txt"),
  79. wiki-root-directory));
  80. end method process-config-element;
  81. define method process-administrator-configuration
  82. (admin-element :: xml/<element>)
  83. let password = get-attr(admin-element, #"password");
  84. let email = get-attr(admin-element, #"email");
  85. if (~(password & email))
  86. error("The <administrator> element must be specified in the config file "
  87. "with a password and email.");
  88. end;
  89. let password = validate-password(password);
  90. let email = validate-email(email);
  91. let admin = find-user($administrator-user-name);
  92. let admin-changed? = #f;
  93. if (admin)
  94. if (admin.user-password ~= password)
  95. admin.user-password := password;
  96. admin-changed? := #t;
  97. log-info("Administrator user (%s) password changed.", $administrator-user-name);
  98. end;
  99. if (admin.user-email ~= email)
  100. admin.user-email := email;
  101. admin-changed? := #t;
  102. log-info("Administrator user (%s) email changed to %=.",
  103. $administrator-user-name, email);
  104. end;
  105. else
  106. admin := make(<wiki-user>,
  107. name: $administrator-user-name,
  108. password: password,
  109. email: email,
  110. administrator?: #t,
  111. activated?: #t);
  112. admin-changed? := #t;
  113. log-info("Administrator user (%s) created.", $administrator-user-name);
  114. end;
  115. *users*[admin.user-name] := admin;
  116. values(*admin-user* := admin, admin-changed?)
  117. end method process-administrator-configuration;
  118. define method process-authentication-configuration
  119. (auth-element :: xml/<element>)
  120. let realm = get-attr(auth-element, #"realm");
  121. if (realm)
  122. *wiki-realm* := realm;
  123. log-info("Authentication realm set to %=", realm);
  124. end;
  125. end process-authentication-configuration;
  126. define method process-mail-configuration
  127. (mail-element :: xml/<element>)
  128. let host = get-attr(mail-element, #"host");
  129. let port = get-attr(mail-element, #"port");
  130. if (host)
  131. *mail-host* := host;
  132. else
  133. error("The <mail> configuration element must have a 'host' attribute.");
  134. end;
  135. if (port)
  136. *mail-port* := string-to-integer(port);
  137. end;
  138. end method process-mail-configuration;
  139. define function restore-from-text-files
  140. () => (num-page-revs)
  141. let wikidata = as(<directory-locator>, "/home/cgay/wiki-data");
  142. format-out("Restoring wiki data from %s\n", as(<string>, wikidata));
  143. let page-data = make(<stretchy-vector>);
  144. local method gather-page-data (directory, filename, type)
  145. // look for "page-<page-num>-<rev-num>.props"
  146. let parts = split(filename, '.');
  147. if (type = #"file" & parts.size = 2 & parts[1] = "props")
  148. let parts = split(parts[0], '-');
  149. if (parts.size = 3 & parts[0] = "page")
  150. let page-num = string-to-integer(parts[1]);
  151. let rev-num = string-to-integer(parts[2]);
  152. add!(page-data, pair(page-num, rev-num));
  153. end;
  154. end;
  155. end;
  156. local method less? (pd1, pd2)
  157. pd1.head < pd2.head | (pd1.head = pd2.head & pd1.tail < pd2.tail)
  158. end;
  159. local method page-locator (page-num, rev-num, extension)
  160. let filename = format-to-string("page-%d-%d.%s",
  161. page-num, rev-num, extension);
  162. merge-locators(as(<file-locator>, filename), wikidata)
  163. end;
  164. local method parse-line (stream)
  165. // e.g. "author: hannes"
  166. let line = read-line(stream);
  167. let parts = split(line, ':', count: 2);
  168. copy-sequence(parts[1], start: min(parts[1].size, 1))
  169. end;
  170. // Load users in this format:
  171. // username
  172. // password
  173. // email
  174. // <blank line>
  175. let user-locator = merge-locators(as(<file-locator>, "users.txt"), wikidata);
  176. with-open-file(stream = user-locator)
  177. let user-count = 0;
  178. block ()
  179. while (#t)
  180. let username = read-line(stream);
  181. let password = read-line(stream);
  182. let email = read-line(stream);
  183. if (~find-user(username))
  184. let user = make(<wiki-user>,
  185. name: username,
  186. password: password,
  187. email: email,
  188. administrator?: #f,
  189. activated?: #t);
  190. store(*storage*, user, *admin-user*, "New user",
  191. standard-meta-data(user, "create"));
  192. inc!(user-count);
  193. end;
  194. assert(empty?(read-line(stream)));
  195. end;
  196. exception (ex :: <end-of-stream-error>)
  197. // done
  198. end;
  199. end;
  200. do-directory(gather-page-data, wikidata);
  201. page-data := sort(page-data, test: less?);
  202. let administrator = find-user("administrator")
  203. | error("No 'administrator' user found. Run the new wiki without "
  204. "the --restore option first, so the administrator account "
  205. "will be created when the config file is loaded.");
  206. let previous-page-num = #f;
  207. for (pd in page-data)
  208. let page-num = pd.head;
  209. let rev-num = pd.tail;
  210. with-open-file(stream = page-locator(page-num, rev-num, "props"))
  211. let title = parse-line(stream);
  212. let author = find-user(parse-line(stream)) | administrator;
  213. let timestamp = parse-iso8601-string(parse-line(stream));
  214. let comment = parse-line(stream);
  215. let page = find-page(title);
  216. if (~page)
  217. let source = file-contents(page-locator(page-num, rev-num, "content"));
  218. page := make(<wiki-page>,
  219. title: title,
  220. source: source,
  221. owner: author);
  222. end;
  223. store(*storage*, page, author, comment, standard-meta-data(page, "create"));
  224. end;
  225. end for;
  226. page-data.size
  227. end function restore-from-text-files;
  228. // There is method to this madness.... In general a GET generates a "view"
  229. // or "confirm" page and a POST actually performs the operation, such as
  230. // create, edit, or delete. The same basic scheme is used for each type of
  231. // object: pages, users, and groups. Here's the example for groups:
  232. // GET /group/list => list groups (has <form> to create group)
  233. // POST /group/list => create new group
  234. // GET /group/view/<name> => view group
  235. // GET /group/edit/<name> => display "edit group" form
  236. // POST /group/edit/<name> => save group from form fields
  237. // GET /group/remove/<name> => display "remove group" form
  238. // POST /group/remove/<name> => remove group
  239. // ...
  240. // In most cases a single URL points to an instance of <wiki-dsp> for both
  241. // GET and POST, and the methods for respond-to-get and respond-to-post
  242. // handle the logic for the given HTTP request method.
  243. define function add-wiki-responders
  244. (http-server :: <http-server>)
  245. initialize-pages();
  246. local method add (url, resource, #rest args)
  247. apply(add-resource,
  248. http-server, concatenate(*wiki-url-prefix*, url), resource,
  249. args);
  250. end;
  251. add("/static", make(<directory-resource>, directory: *static-directory*));
  252. add("/", make(<redirecting-resource>, target: wiki-url("/page/view/Home")),
  253. url-name: "wiki.home");
  254. add("/login", function-resource(curry(login, realm: *wiki-realm*)),
  255. url-name: "wiki.login");
  256. add("/logout", function-resource(logout),
  257. url-name: "wiki.logout");
  258. add("/recent-changes",
  259. make(<recent-changes-page>, source: "list-recent-changes.dsp"),
  260. url-name: "wiki.recent-changes");
  261. /* TODO:
  262. add("/feed/{type?}/{name?}", function-resource(atom-feed-responder),
  263. url-name: "wiki.atom-feed");
  264. */
  265. add("/user/list", *list-users-page*,
  266. url-name: "wiki.user.list");
  267. // TODO: support the {revision?} path element. Requires a revision
  268. // slot in wiki-object.
  269. add("/user/view/{name}/{revision?}", *view-user-page*,
  270. url-name: "wiki.user.view");
  271. add("/user/edit/{name}", *edit-user-page*,
  272. url-name: "wiki.user.edit");
  273. add("/user/activate/{name}/{key}",
  274. function-resource(respond-to-user-activation-request),
  275. url-name: "wiki.user.activate");
  276. add("/user/deactivate/{name}", *deactivate-user-page*,
  277. url-name: "wiki.user.deactivate");
  278. add("/register", *registration-page*,
  279. url-name: "wiki.register");
  280. // Provide backward compatibility with old wiki URLs.
  281. // Note no url-name argument since we don't want this URL generated.
  282. add("/wiki/view.dsp", function-resource(show-page-back-compatible));
  283. add("/page/list",
  284. make(<list-pages-page>, source: "list-pages.dsp"),
  285. url-name: "wiki.page.list");
  286. add("/page/view/{title}/{version?}", *view-page-page*,
  287. url-name: "wiki.page.view");
  288. // TODO: rename {version} to {revision}
  289. add("/page/edit/{title}/{version?}",
  290. make(<edit-page-page>, source: "edit-page.dsp"),
  291. url-name: "wiki.page.edit");
  292. add("/page/remove/{title}/{version?}", *remove-page-page*,
  293. url-name: "wiki.page.remove");
  294. add("/page/history/{title}/{revision?}", *page-history-page*,
  295. url-name: "wiki.page.versions");
  296. add("/page/diff/{title}/{version1}", *view-diff-page*,
  297. url-name: "wiki.page.diff");
  298. add("/page/connections/{title}", *connections-page*,
  299. url-name: "wiki.page.connections");
  300. add("/page/access/{title}", *edit-access-page*,
  301. url-name: "wiki.page.access");
  302. add("/group/list", *list-groups-page*,
  303. url-name: "wiki.group.list");
  304. // TODO: support the {revision?} path element. Requires a revision
  305. // slot in wiki-object.
  306. add("/group/view/{name}/{revision?}", *view-group-page*,
  307. url-name: "wiki.group.view");
  308. add("/group/edit/{name}", *edit-group-page*,
  309. url-name: "wiki.group.edit");
  310. add("/group/remove/{name}", *remove-group-page*,
  311. url-name: "wiki.group.remove");
  312. add("/group/members/{name}", *edit-group-members-page*,
  313. url-name: "wiki.group.members");
  314. /***** We'll use Google or Yahoo custom search, at least for a while
  315. url wiki-url("/search")
  316. action (get, post) () => $search-page;
  317. */
  318. end function add-wiki-responders;
  319. // Called after config file loaded.
  320. define function initialize-wiki
  321. (server :: <http-server>)
  322. if (~get-option-value(*command-line-parser*, "config"))
  323. error("You must specify a config file with the --config option.");
  324. end;
  325. add-wiki-responders(server);
  326. preload-wiki-data();
  327. end function initialize-wiki;
  328. define function preload-wiki-data ()
  329. // Load all wiki data. Not serving yet, so no lock needed.
  330. for (user in load-all(*storage*, <wiki-user>))
  331. *users*[user.user-name] := user;
  332. end;
  333. for (group in load-all(*storage*, <wiki-group>))
  334. *groups*[group.group-name] := group;
  335. end;
  336. // TODO: This won't scale.
  337. for (page in load-all(*storage*, <wiki-page>))
  338. *pages*[page.page-title] := page;
  339. end;
  340. for (page in *pages*)
  341. update-reference-tables!(page, #(), page.outbound-references);
  342. end;
  343. end function preload-wiki-data;
  344. // This is pretty horrifying, but the plan is to eventually make it all
  345. // disappear behind a somewhat less horrifying macro like "define site".
  346. //
  347. define function initialize-pages
  348. ()
  349. // page pages
  350. *view-diff-page* := make(<view-diff-page>, source: "view-diff.dsp");
  351. *edit-page-page* := make(<edit-page-page>, source: "edit-page.dsp");
  352. *view-page-page* := make(<view-page-page>, source: "view-page.dsp");
  353. *remove-page-page* := make(<remove-page-page>, source: "remove-page.dsp");
  354. *page-history-page* := make(<page-history-page>, source: "view-page-history.dsp");
  355. *connections-page* := make(<connections-page>, source: "page-connections.dsp");
  356. *search-page* := make(<wiki-dsp>, source: "search-page.dsp");
  357. *non-existing-page-page* := make(<wiki-dsp>, source: "non-existing-page.dsp");
  358. // user pages
  359. *list-users-page* := make(<list-users-page>, source: "list-users.dsp");
  360. *view-user-page* := make(<view-user-page>, source: "view-user.dsp");
  361. *edit-user-page* := make(<edit-user-page>, source: "edit-user.dsp");
  362. *deactivate-user-page* := make(<deactivate-user-page>, source: "deactivate-user.dsp");
  363. *non-existing-user-page* := make(<wiki-dsp>, source: "non-existing-user.dsp");
  364. *not-logged-in-page* := make(<wiki-dsp>, source: "not-logged-in.dsp");
  365. // group pages
  366. *list-groups-page* := make(<list-groups-page>, source: "list-groups.dsp");
  367. *non-existing-group-page* := make(<wiki-dsp>, source: "non-existing-group.dsp");
  368. *view-group-page* := make(<view-group-page>, source: "view-group.dsp");
  369. *edit-group-page* := make(<edit-group-page>, source: "edit-group.dsp");
  370. *remove-group-page* := make(<remove-group-page>, source: "remove-group.dsp");
  371. *edit-group-members-page* := make(<edit-group-members-page>,
  372. source: "edit-group-members.dsp");
  373. // other pages
  374. *registration-page* := make(<registration-page>, source: "register.dsp");
  375. *edit-access-page* := make(<acls-page>, source: "edit-page-access.dsp");
  376. end function initialize-pages;
  377. /*
  378. The conversion procedure probably is like this:
  379. * Run the modified old wiki code, which will write out all the wiki
  380. pages to text files. /home/cgay/wiki-conversion-libraries/
  381. * BACKUP THE WIKI DATABASE!
  382. * Run the new wiki code briefly, just so it can read in the config
  383. file and create the administrator user.
  384. The next step will use these if it finds them. Shut down the wiki.
  385. * Run the new wiki code again with the --restore command-line argument.
  386. */
  387. define function main
  388. ()
  389. if (member?("--restore", application-arguments(), test: \=))
  390. // need to handle the --config argument here so the content directory is set.
  391. // Remove this when the old, pre-turbo wiki is dead.
  392. let parser = *command-line-parser*;
  393. parse-command-line(parser, application-arguments());
  394. let config-file = get-option-value(parser, "config");
  395. if (config-file)
  396. // we just cons up a server here because all we care about is that
  397. // the <wiki> setting is processed.
  398. configure-server(make(<http-server>), config-file);
  399. end;
  400. format-out("Restored %d page revisions\n", restore-from-text-files());
  401. else
  402. let filename = locator-name(as(<file-locator>, application-name()));
  403. if (split(filename, ".")[0] = "wiki")
  404. // This eventually causes process-config-element (above) to be called.
  405. http-server-main(description: "Dylan Wiki",
  406. before-startup: initialize-wiki);
  407. end;
  408. end;
  409. end function main;
  410. begin
  411. main();
  412. end;