mirror of
https://github.com/journeyman-cc/smeagol.git
synced 2026-04-12 18:05:06 +00:00
Merge branch 'feature/4' into develop
This commit is contained in:
commit
ad152b336a
17 changed files with 457 additions and 216 deletions
|
|
@ -1 +1 @@
|
||||||
{:admin {:password "admin", :email "admin@localhost"}}
|
{:admin {:admin true, :email "info@weft.scot", :password "admin"}}
|
||||||
|
|
@ -1 +1 @@
|
||||||
This is the page linked to from the left bar.
|
This is the page linked to from the [[Introduction]] page.
|
||||||
|
|
@ -8,18 +8,16 @@ Smeagol is now a fully working small Wiki engine, and meets my own immediate nee
|
||||||
things which could be improved - see **TODO** list below - but it works now and doesn't seem to have any major problems.
|
things which could be improved - see **TODO** list below - but it works now and doesn't seem to have any major problems.
|
||||||
|
|
||||||
## Markup syntax
|
## Markup syntax
|
||||||
Smeagol uses the Markdown format as provided by [markdown-clj](https://github.com/yogthos/markdown-clj), with the addition that anything enclosed in double square brackets, \[\[like this\]\], will be treated as a link into the wiki itself.
|
Smeagol uses the Markdown format as provided by [markdown-clj](https://github.com/yogthos/markdown-clj), with the addition that anything enclosed in double square brackets, \[\[like this\]\], will be treated as a link into the wiki itself. Here's an example [[Internal Link]].
|
||||||
|
|
||||||
## Security and authentication
|
## Security and authentication
|
||||||
Security is now greatly improved. There is a file called *passwd* in the *resources* directory, which contains a clojure map which maps usernames to maps with plain-text passwords and emails thus:
|
Security is now greatly improved. There is a file called *passwd* in the *resources* directory, which contains a clojure map which maps usernames to maps with plain-text passwords and emails thus:
|
||||||
|
|
||||||
{:admin {:password "admin" :email "admin@localhost"}
|
{:admin {:password "admin" :email "admin@localhost" :admin true}
|
||||||
:adam {:password "secret" :email "adam@localhost"}}
|
:adam {:password "secret" :email "adam@localhost"}}
|
||||||
|
|
||||||
that is to say, the username is a keyword and the corresponding password is a string. However, since version 0.5.0, users can now change their own passwords, and when the user changes their password their new password is encrypted using the [scrypt](http://www.tarsnap.com/scrypt.html) one-way encryption scheme. The password file is now no longer either in the *resources/public* directory so cannot be downloaded through the browser, nor in the git archive to which the Wiki content is stored, so that even if that git archive is remotely clonable an attacker cannot get the password file that way.
|
that is to say, the username is a keyword and the corresponding password is a string. However, since version 0.5.0, users can now change their own passwords, and when the user changes their password their new password is encrypted using the [scrypt](http://www.tarsnap.com/scrypt.html) one-way encryption scheme. The password file is now no longer either in the *resources/public* directory so cannot be downloaded through the browser, nor in the git archive to which the Wiki content is stored, so that even if that git archive is remotely clonable an attacker cannot get the password file that way.
|
||||||
|
|
||||||
There's still no mechanism to add a new user to the system through the user interface; you do still have to do that by editing the password file in an editor.
|
|
||||||
|
|
||||||
## Images
|
## Images
|
||||||
Smeagol does not currently have any mechanism to upload images. You can, however, link to images already available on the web, like this:
|
Smeagol does not currently have any mechanism to upload images. You can, however, link to images already available on the web, like this:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
This is the header. There isn't yet much in it. You could [edit](edit?page=_header) it to provide internal navigation or branding.
|
This is the header. There isn't yet much in it. You could [edit](edit?page=_header) it to provide internal navigation or branding.
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@
|
||||||
USA.
|
USA.
|
||||||
|
|
||||||
# The Stylesheet
|
# The Stylesheet
|
||||||
|
|
||||||
|
## html elements generally in alphabetic order
|
||||||
*/
|
*/
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|
@ -28,6 +30,18 @@ body {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
del {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.content, form, p, pre, h1, h2, h3, h4, h5 {
|
||||||
|
padding: 0.25em 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl, menu, ol, table, ul {
|
||||||
|
margin: 0.25em 5%;
|
||||||
|
}
|
||||||
|
|
||||||
/* footer of the page - not-editable, provided by Smeagol */
|
/* footer of the page - not-editable, provided by Smeagol */
|
||||||
footer {
|
footer {
|
||||||
border-top: thin solid gray;
|
border-top: thin solid gray;
|
||||||
|
|
@ -52,6 +66,9 @@ footer div {
|
||||||
padding: 0.1em;
|
padding: 0.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
border: thin solid silver;
|
||||||
|
}
|
||||||
|
|
||||||
/* header for all pages in the Wiki - editable, provided by users. */
|
/* header for all pages in the Wiki - editable, provided by users. */
|
||||||
header {
|
header {
|
||||||
|
|
@ -69,7 +86,66 @@ header img {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ids generally in document order */
|
input {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.action {
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.action-dangerous {
|
||||||
|
color: white;
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.required:after {
|
||||||
|
content: " \*";
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
ins {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
width: 20%;
|
||||||
|
min-width: 20em;
|
||||||
|
border-right: thin solid gray;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu li {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu li::before {
|
||||||
|
content: "|| ";
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border: 2px solid black;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.music-ruled tr:nth-child(odd) {
|
||||||
|
background-color: silver;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
padding: 0.15em 1.5em;
|
||||||
|
border: 1px solid gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: silver;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
## ids generally in document order
|
||||||
|
*/
|
||||||
|
|
||||||
/* top-of-page navigation, not editable, provided by Smeagol */
|
/* top-of-page navigation, not editable, provided by Smeagol */
|
||||||
#nav{
|
#nav{
|
||||||
|
|
@ -180,22 +256,22 @@ li.nav-item a:active { background: gray; color: white; }
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.change {
|
.change {
|
||||||
background-color: rgb( 223, 223, 223);
|
background-color: rgb( 223, 223, 223);
|
||||||
border: thin solid silver;
|
border: thin solid silver;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: red;
|
background-color: red;
|
||||||
color: white;
|
color: white;
|
||||||
|
border: thin solid maroon;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
border: thin solid red;
|
color: darkgreen;
|
||||||
|
background-color: silver;
|
||||||
|
border: thin solid lime;
|
||||||
}
|
}
|
||||||
|
|
||||||
.minor-controls {
|
.minor-controls {
|
||||||
|
|
@ -233,75 +309,3 @@ li.nav-item a:active { background: gray; color: white; }
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
|
||||||
border: thin solid silver;
|
|
||||||
}
|
|
||||||
|
|
||||||
del {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.content, form, p, pre, h1, h2, h3, h4, h5 {
|
|
||||||
padding: 0.25em 5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl, menu, ol, table, ul {
|
|
||||||
margin: 0.25em 5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.action {
|
|
||||||
background-color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.action-dangerous {
|
|
||||||
color: white;
|
|
||||||
background-color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.required:after {
|
|
||||||
content: " \*";
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
ins {
|
|
||||||
color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
width: 20%;
|
|
||||||
min-width: 20em;
|
|
||||||
border-right: thin solid gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
menu li {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
menu li::before {
|
|
||||||
content: "|| ";
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border: 2px solid black;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.music-ruled tr:nth-child(odd) {
|
|
||||||
background-color: silver;
|
|
||||||
}
|
|
||||||
|
|
||||||
th, td {
|
|
||||||
text-align: left;
|
|
||||||
vertical-align: top;
|
|
||||||
padding: 0.15em 1.5em;
|
|
||||||
border: 1px solid gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
background-color: silver;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
1
resources/public/vendor/README.md
vendored
Normal file
1
resources/public/vendor/README.md
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
This folder must exist in order that the Bower package manager can deploy JavaScript packages to it.
|
||||||
|
|
@ -10,30 +10,27 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="wiki">
|
<header class="wiki">
|
||||||
<!-- navbar -->
|
<!-- navbar -->
|
||||||
<div id="nav">
|
<div id="nav">
|
||||||
{% if user %}
|
{% if user %}
|
||||||
<p class="user" id="user">You are logged in as {{user}} | <a href="passwd">change password</a></p>
|
<p class="user" id="user">You are logged in as {{user}} | <a href="passwd">change password</a></p>
|
||||||
|
{% endif %}
|
||||||
|
<img id="nav-icon" src="{{servlet-context}}/img/threelines.png" alt="Menu"/>
|
||||||
|
<ul id="nav-menu" class="nav">
|
||||||
|
<li class="{{wiki-selected}}"><a href="{{servlet-context}}/">Home</a></li>
|
||||||
|
{% if admin %}
|
||||||
|
<li class="{{admin-selected}}"><a href="{{servlet-context}}/edit-users">Edit users</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<img id="nav-icon" src="{{servlet-context}}/img/threelines.png" alt="Menu"/>
|
<li class="{{auth-selected}}"><a href="{{servlet-context}}/auth">
|
||||||
<ul id="nav-menu" class="nav">
|
{% if user %}
|
||||||
<li class="{{wiki-selected}}"><a href="{{servlet-context}}/">Home</a></li>
|
Log out
|
||||||
<li class="{{auth-selected}}"><a href="{{servlet-context}}/auth">
|
{% else %}
|
||||||
{% if user %}
|
Log in
|
||||||
Log out
|
{% endif %}</a></li>
|
||||||
{% else %}
|
</ul>
|
||||||
Log in
|
</div>
|
||||||
{% endif %}</a></li>
|
<h1>{{title}}</h1>
|
||||||
</ul>
|
{{header|safe}}
|
||||||
</div>
|
|
||||||
<h1>{{title}}</h1>
|
|
||||||
{{header|safe}}
|
|
||||||
</header>
|
|
||||||
<div id="side-bar" class="wiki">
|
|
||||||
{{side-bar|safe}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="main-container" class="container">
|
|
||||||
{% if message %}
|
{% if message %}
|
||||||
<div id="message">
|
<div id="message">
|
||||||
<p class="message">{{message}}</p>
|
<p class="message">{{message}}</p>
|
||||||
|
|
@ -44,7 +41,12 @@
|
||||||
<p class="error">{{error}}</p>
|
<p class="error">{{error}}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</header>
|
||||||
|
<div id="side-bar" class="wiki">
|
||||||
|
{{side-bar|safe}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="main-container" class="container">
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -62,8 +64,8 @@
|
||||||
<footer>
|
<footer>
|
||||||
<div id="credits">
|
<div id="credits">
|
||||||
<div>
|
<div>
|
||||||
One Wiki to rule them all ||
|
<img height="16" width="16" alt="one wiki to rule them all" src="img/smeagol.png"/>One Wiki to rule them all ||
|
||||||
<img height="16" width="16" alt="one wiki to rule them all" src="img/smeagol.png"/>Smeagol wiki engine {{version}} ||
|
Smeagol wiki engine {{version}} ||
|
||||||
<img height="16" width="16" alt="The Web Engineering Factory & Toolworks" src="http://www.weft.scot/images/weft.logo.64.png"> Developed by <a href="http://www.weft.scot/">WEFT</a>
|
<img height="16" width="16" alt="The Web Engineering Factory & Toolworks" src="http://www.weft.scot/images/weft.logo.64.png"> Developed by <a href="http://www.weft.scot/">WEFT</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
32
resources/templates/edit-user.html
Normal file
32
resources/templates/edit-user.html
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
{% extends "templates/base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div id="content" class="edit">
|
||||||
|
<form action="{{servlet-context}}/edit-user" method="POST">
|
||||||
|
<p class="widget">
|
||||||
|
<label for="target">Username</label>
|
||||||
|
<input type="text" name="target" id="target" value="{{target}}" required/>
|
||||||
|
</p>
|
||||||
|
<p class="widget">
|
||||||
|
<label for="pass1">New password</label>
|
||||||
|
<input name="pass1" id="pass1" type="password"/>
|
||||||
|
</p>
|
||||||
|
<p class="widget">
|
||||||
|
<label for="pass2">And again</label>
|
||||||
|
<input name="pass2" id="pass2" type="password"/>
|
||||||
|
</p>
|
||||||
|
<p class="widget">
|
||||||
|
<label for="email">Email address</label>
|
||||||
|
<input name="email" id="email" type="text" value="{{details.email}}" required/>
|
||||||
|
</p>
|
||||||
|
<p class="widget">
|
||||||
|
<label for="admin">Is administrator?</label>
|
||||||
|
<input name="admin" id="admin" type="checkbox" {% if details.admin %}checked{% endif %}/>
|
||||||
|
</p>
|
||||||
|
<p class="widget">
|
||||||
|
<label for="submit">When you have finished editing</label>
|
||||||
|
<input name="submit" id="submit" type="submit" class="action" value="Save!"/>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
21
resources/templates/edit-users.html
Normal file
21
resources/templates/edit-users.html
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{% extends "templates/base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div id="content">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Edit</th><th>Delete</th>
|
||||||
|
</tr>
|
||||||
|
{% for user in users %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="edit-user?target={{user}}">Edit {{user}}</a></td>
|
||||||
|
<td><a href="delete-user?target={{user}}">Delete {{user}}</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="edit-user">Add new user</a></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -3,20 +3,20 @@
|
||||||
<div id="content" class="auth">
|
<div id="content" class="auth">
|
||||||
<form action="{{servlet-context}}/passwd" method="POST">
|
<form action="{{servlet-context}}/passwd" method="POST">
|
||||||
<p class="widget">
|
<p class="widget">
|
||||||
<label for="password">Your password</label>
|
<label for="oldpass">Your password</label>
|
||||||
<input name="oldpass" id="oldpass" type="password" required/>
|
<input name="oldpass" id="oldpass" type="password" required/>
|
||||||
</p>
|
</p>
|
||||||
<p class="widget">
|
<p class="widget">
|
||||||
<label for="password">New password</label>
|
<label for="pass1">New password</label>
|
||||||
<input name="pass1" id="pass1" type="password" required/>
|
<input name="pass1" id="pass1" type="password" required/>
|
||||||
</p>
|
</p>
|
||||||
<p class="widget">
|
<p class="widget">
|
||||||
<label for="password">And again</label>
|
<label for="pass2">And again</label>
|
||||||
<input name="pass2" id="pass2" type="password" required/>
|
<input name="pass2" id="pass2" type="password" required/>
|
||||||
</p>
|
</p>
|
||||||
<p class="widget">
|
<p class="widget">
|
||||||
<label for="submit">To edit this wiki</label>
|
<label for="submit">To edit this wiki</label>
|
||||||
<input name="action" id="action" type="submit" class="action" value="Login!"/>
|
<input name="action" id="action" type="submit" class="action" value="Change password!"/>
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="wiki">
|
<div id="content" class="wiki">
|
||||||
|
{% if editable %}
|
||||||
<ul class="minor-controls">
|
<ul class="minor-controls">
|
||||||
<li><a href="{{servlet-context}}/edit?page={{title}}">Edit this page</a></li>
|
<li><a href="{{servlet-context}}/edit?page={{title}}">Edit this page</a></li>
|
||||||
<li><a href="history?page={{page}}">History</a></li>
|
<li><a href="history?page={{page}}">History</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
{{content|safe}}
|
{% endif %}
|
||||||
|
{{content|safe}}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -33,28 +33,50 @@
|
||||||
;;;;
|
;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
;; the relative path to the password file.
|
||||||
|
(def password-file-path (str (io/resource-path) "../passwd"))
|
||||||
|
|
||||||
|
|
||||||
|
(defn- get-users
|
||||||
|
"Get the whole content of the password file as a clojure map"
|
||||||
|
[]
|
||||||
|
(read-string (slurp password-file-path)))
|
||||||
|
|
||||||
|
|
||||||
(defn authenticate
|
(defn authenticate
|
||||||
"Return `true` if this `username`/`password` pair match, `false` otherwise"
|
"Return `true` if this `username`/`password` pair match, `false` otherwise"
|
||||||
[username password]
|
[username password]
|
||||||
(let [path (str (io/resource-path) "../passwd")
|
(let [user ((keyword username) (get-users))]
|
||||||
users (read-string (slurp path))
|
(timbre/info (str "Authenticating " username " against " password-file-path))
|
||||||
user ((keyword username) users)]
|
|
||||||
(timbre/info (str "Authenticating " username " against " path))
|
|
||||||
(and user
|
(and user
|
||||||
|
(:password user)
|
||||||
(or
|
(or
|
||||||
(.equals (:password user) password)
|
(.equals (:password user) password)
|
||||||
(password/check password (:password user))))))
|
(password/check password (:password user))))))
|
||||||
|
|
||||||
|
|
||||||
(defn get-email
|
(defn get-email
|
||||||
"Return the email address associated with this `username`."
|
"Return the email address associated with this `username`."
|
||||||
[username]
|
[username]
|
||||||
(let [path (str (io/resource-path) "../passwd")
|
(if username
|
||||||
users (read-string (slurp path))
|
(let [user ((keyword username) (get-users))]
|
||||||
user ((keyword username) users)]
|
(:email user))))
|
||||||
(if user (:email user))))
|
|
||||||
|
|
||||||
|
(defn get-admin
|
||||||
|
"Return a flag indicating whether the user with this username is an administrator."
|
||||||
|
[username]
|
||||||
|
(if username
|
||||||
|
(let [user ((keyword username) (get-users))]
|
||||||
|
(:admin user))))
|
||||||
|
|
||||||
|
(defn evaluate-password
|
||||||
|
"Evaluate whether this proposed password is suitable for use."
|
||||||
|
([pass1 pass2]
|
||||||
|
(and pass1 (>= (count pass1) 8) (.equals pass1 pass2)))
|
||||||
|
([password]
|
||||||
|
(evaluate-password password password)))
|
||||||
|
|
||||||
;;; TODO: worth locking the passwd file to prevent corruption if two simultaneous threads
|
|
||||||
;;; try to write it. See http://stackoverflow.com/questions/6404717/idiomatic-file-locking-in-clojure
|
|
||||||
|
|
||||||
(defn change-pass
|
(defn change-pass
|
||||||
"Change the password for the user with this `username` and `oldpass` to this `newpass`.
|
"Change the password for the user with this `username` and `oldpass` to this `newpass`.
|
||||||
|
|
@ -62,24 +84,89 @@
|
||||||
password will be encrypted."
|
password will be encrypted."
|
||||||
[username oldpass newpass]
|
[username oldpass newpass]
|
||||||
(timbre/info (format "Changing password for user %s" username))
|
(timbre/info (format "Changing password for user %s" username))
|
||||||
(let [path (str (io/resource-path) "../passwd")
|
(let [users (get-users)
|
||||||
users (read-string (slurp path))
|
|
||||||
keywd (keyword username)
|
keywd (keyword username)
|
||||||
user (if users (keywd users))
|
user (keywd users)
|
||||||
email (:email user)]
|
email (:email user)]
|
||||||
(try
|
(try
|
||||||
(cond
|
(cond
|
||||||
(and user
|
(and user
|
||||||
(or
|
(or
|
||||||
(.equals (:password user) oldpass)
|
(.equals (:password user) oldpass)
|
||||||
(password/check oldpass (:password user))))
|
(password/check oldpass (:password user))))
|
||||||
(do
|
(do
|
||||||
(spit path
|
(locking password-file-path
|
||||||
(assoc (dissoc users keywd) keywd
|
(spit password-file-path
|
||||||
{:password (password/encrypt newpass) :email email}))
|
(merge users
|
||||||
true))
|
{keywd
|
||||||
|
(merge user
|
||||||
|
{:password (password/encrypt newpass)})})))
|
||||||
|
(timbre/info (str "Successfully changed password for user " username))
|
||||||
|
true))
|
||||||
(catch Exception any
|
(catch Exception any
|
||||||
(timbre/error
|
(timbre/error
|
||||||
(format "Changing password failed for user %s failed: %s (%s)"
|
(format "Changing password failed for user %s failed: %s (%s)"
|
||||||
username (.getName (.getClass any)) (.getMessage any)))
|
username (.getName (.getClass any)) (.getMessage any)))
|
||||||
|
false))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn list-users
|
||||||
|
"Return, as strings, the names of the currently known users."
|
||||||
|
[]
|
||||||
|
(map name (keys (get-users))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn fetch-user-details
|
||||||
|
"Return the map of features of this user, if any."
|
||||||
|
[username]
|
||||||
|
(if
|
||||||
|
(and username (> (count (str username)) 0))
|
||||||
|
((keyword username) (get-users))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn add-user
|
||||||
|
"Add a user to the passwd file with this username, initial password and email address and admin flag."
|
||||||
|
[username newpass email admin]
|
||||||
|
(let [users (get-users)
|
||||||
|
user ((keyword username) users)
|
||||||
|
password (if
|
||||||
|
(and newpass (evaluate-password newpass))
|
||||||
|
(password/encrypt newpass))
|
||||||
|
details {:email email
|
||||||
|
:admin (if
|
||||||
|
(and (string? admin) (> (count admin) 0))
|
||||||
|
true
|
||||||
|
false)}
|
||||||
|
;; if we have a valid password we want to include it in the details to update.
|
||||||
|
full-details (if password
|
||||||
|
(merge details {:password password})
|
||||||
|
details)]
|
||||||
|
(try
|
||||||
|
(locking password-file-path
|
||||||
|
(spit password-file-path
|
||||||
|
(merge users
|
||||||
|
{(keyword username) (merge user full-details)}))
|
||||||
|
(timbre/info (str "Successfully added user " username))
|
||||||
|
true)
|
||||||
|
(catch Exception any
|
||||||
|
(timbre/error
|
||||||
|
(format "Adding user %s failed: %s (%s)"
|
||||||
|
username (.getName (.getClass any)) (.getMessage any)))
|
||||||
|
false))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn delete-user
|
||||||
|
"Delete the user with this `username` from the password file."
|
||||||
|
[username]
|
||||||
|
(let [users (get-users)]
|
||||||
|
(try
|
||||||
|
(locking password-file-path
|
||||||
|
(spit password-file-path
|
||||||
|
(dissoc users (keyword username)))
|
||||||
|
(timbre/info (str "Successfully deleted user " username))
|
||||||
|
true)
|
||||||
|
(catch Exception any
|
||||||
|
(timbre/error
|
||||||
|
(format "Deleting user %s failed: %s (%s)"
|
||||||
|
username (.getName (.getClass any)) (.getMessage any)))
|
||||||
false))))
|
false))))
|
||||||
|
|
|
||||||
|
|
@ -54,10 +54,6 @@
|
||||||
an app server such as Tomcat
|
an app server such as Tomcat
|
||||||
put any initialization code here"
|
put any initialization code here"
|
||||||
[]
|
[]
|
||||||
(timbre/set-config!
|
|
||||||
{:min-level :debug
|
|
||||||
:enabled? true
|
|
||||||
:output-fn timbre/default-output-fn})
|
|
||||||
(timbre/merge-config!
|
(timbre/merge-config!
|
||||||
{:appenders
|
{:appenders
|
||||||
{:rotor (rotor/rotor-appender
|
{:rotor (rotor/rotor-appender
|
||||||
|
|
|
||||||
84
src/smeagol/routes/admin.clj
Normal file
84
src/smeagol/routes/admin.clj
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
(ns ^{:doc "Render all the main pages of a very simple Wiki engine."
|
||||||
|
:author "Simon Brooke"}
|
||||||
|
smeagol.routes.admin
|
||||||
|
(:require [clojure.walk :refer :all]
|
||||||
|
[noir.session :as session]
|
||||||
|
[taoensso.timbre :as timbre]
|
||||||
|
[smeagol.authenticate :as auth]
|
||||||
|
[smeagol.layout :as layout]
|
||||||
|
[smeagol.util :as util]))
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;;;;
|
||||||
|
;;;; Smeagol: a very simple Wiki engine.
|
||||||
|
;;;;
|
||||||
|
;;;; This program is free software; you can redistribute it and/or
|
||||||
|
;;;; modify it under the terms of the GNU General Public License
|
||||||
|
;;;; as published by the Free Software Foundation; either version 2
|
||||||
|
;;;; of the License, or (at your option) any later version.
|
||||||
|
;;;;
|
||||||
|
;;;; This program is distributed in the hope that it will be useful,
|
||||||
|
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
;;;; GNU General Public License for more details.
|
||||||
|
;;;;
|
||||||
|
;;;; You should have received a copy of the GNU General Public License
|
||||||
|
;;;; along with this program; if not, write to the Free Software
|
||||||
|
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
|
||||||
|
;;;; USA.
|
||||||
|
;;;;
|
||||||
|
;;;; Copyright (C) 2016 Simon Brooke
|
||||||
|
;;;;
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
|
||||||
|
(defn edit-users
|
||||||
|
"Put a list of users on-screen for editing."
|
||||||
|
[request]
|
||||||
|
(let [params (keywordize-keys (:params request))
|
||||||
|
user (session/get :user)]
|
||||||
|
(layout/render "edit-users.html"
|
||||||
|
(merge (util/standard-params request)
|
||||||
|
{:title "Select user to edit"
|
||||||
|
:users (auth/list-users)}))))
|
||||||
|
|
||||||
|
(defn delete-user
|
||||||
|
"Delete a user."
|
||||||
|
[request]
|
||||||
|
(let [params (keywordize-keys (:params request))
|
||||||
|
target (:target params)
|
||||||
|
deleted (auth/delete-user target)
|
||||||
|
message (if deleted (str "Successfully deleted user " target))
|
||||||
|
error (if (not deleted) (str "Could not delete user " target))]
|
||||||
|
(layout/render "edit-users.html"
|
||||||
|
(merge (util/standard-params request)
|
||||||
|
{:title "Select user to edit"
|
||||||
|
:message message
|
||||||
|
:error error
|
||||||
|
:users (auth/list-users)}))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn edit-user
|
||||||
|
"Put an individual user's details on screen for editing."
|
||||||
|
[request]
|
||||||
|
(let [params (keywordize-keys (:params request))
|
||||||
|
target (:target params)
|
||||||
|
pass1 (:pass1 params)
|
||||||
|
password (if (and pass1 (auth/evaluate-password pass1 (:pass2 params))) pass1)
|
||||||
|
stored (if (:email params)
|
||||||
|
(auth/add-user target password (:email params) (:admin params)))
|
||||||
|
message (if stored (str "User " target " was stored successfully."))
|
||||||
|
error (if (and (:email params) (not stored))
|
||||||
|
(str "User " target " was not stored."))
|
||||||
|
details (auth/fetch-user-details target)]
|
||||||
|
(if message
|
||||||
|
(timbre/info message))
|
||||||
|
(if error
|
||||||
|
(timbre/warn error))
|
||||||
|
(layout/render "edit-user.html"
|
||||||
|
(merge (util/standard-params request)
|
||||||
|
{:title (str "Edit user " target)
|
||||||
|
:message message
|
||||||
|
:error error
|
||||||
|
:target target
|
||||||
|
:details details}))))
|
||||||
0
src/smeagol/routes/params.clj
Normal file
0
src/smeagol/routes/params.clj
Normal file
|
|
@ -3,10 +3,9 @@
|
||||||
smeagol.routes.wiki
|
smeagol.routes.wiki
|
||||||
(:require [clojure.walk :refer :all]
|
(:require [clojure.walk :refer :all]
|
||||||
[clojure.java.io :as cjio]
|
[clojure.java.io :as cjio]
|
||||||
[clojure.string :as cs]
|
[cemerick.url :refer (url url-encode url-decode)]
|
||||||
[compojure.core :refer :all]
|
[compojure.core :refer :all]
|
||||||
[clj-jgit.porcelain :as git]
|
[clj-jgit.porcelain :as git]
|
||||||
[cemerick.url :refer (url url-encode url-decode)]
|
|
||||||
[markdown.core :as md]
|
[markdown.core :as md]
|
||||||
[noir.io :as io]
|
[noir.io :as io]
|
||||||
[noir.response :as response]
|
[noir.response :as response]
|
||||||
|
|
@ -17,7 +16,8 @@
|
||||||
[smeagol.diff2html :as d2h]
|
[smeagol.diff2html :as d2h]
|
||||||
[smeagol.layout :as layout]
|
[smeagol.layout :as layout]
|
||||||
[smeagol.util :as util]
|
[smeagol.util :as util]
|
||||||
[smeagol.history :as hist]))
|
[smeagol.history :as hist]
|
||||||
|
[smeagol.routes.admin :as admin]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;
|
;;;;
|
||||||
|
|
@ -42,19 +42,6 @@
|
||||||
;;;;
|
;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(defn local-links
|
|
||||||
"Rewrite text in `html-src` surrounded by double square brackets as a local link into this wiki."
|
|
||||||
[^String html-src]
|
|
||||||
(cs/replace html-src #"\[\[[^\[\]]*\]\]"
|
|
||||||
#(let [text (clojure.string/replace %1 #"[\[\]]" "")
|
|
||||||
encoded (url-encode text)
|
|
||||||
;; I use '\_' to represent '_' in wiki markup, because
|
|
||||||
;; '_' is meaningful in Markdown. However, this needs to
|
|
||||||
;; be stripped out when interpreting local links.
|
|
||||||
munged (cs/replace encoded #"%26%2395%3B" "_")]
|
|
||||||
(format "<a href='wiki?page=%s'>%s</a>" munged text))))
|
|
||||||
|
|
||||||
|
|
||||||
(defn get-git-repo
|
(defn get-git-repo
|
||||||
"Get the git repository for my content, creating it if necessary"
|
"Get the git repository for my content, creating it if necessary"
|
||||||
[]
|
[]
|
||||||
|
|
@ -100,18 +87,20 @@
|
||||||
src-text (:src params)
|
src-text (:src params)
|
||||||
page (or (:page params) default)
|
page (or (:page params) default)
|
||||||
file-path (str (io/resource-path) "content/" page suffix)
|
file-path (str (io/resource-path) "content/" page suffix)
|
||||||
exists? (.exists (cjio/as-file file-path))]
|
exists? (.exists (cjio/as-file file-path))
|
||||||
(if (not exists?) (timbre/info (format "File '%s' not found; creating a new file" file-path)))
|
user (session/get :user)]
|
||||||
|
(if (not exists?)
|
||||||
|
(timbre/info (format "File '%s' not found; creating a new file" file-path))
|
||||||
|
(timbre/info (format "Opening '%s' for editing" file-path)))
|
||||||
(cond src-text (process-source params suffix)
|
(cond src-text (process-source params suffix)
|
||||||
true
|
true
|
||||||
(layout/render template
|
(layout/render template
|
||||||
{:title (str "Edit " page)
|
(merge (util/standard-params request)
|
||||||
:page page
|
{:title (str "Edit " page)
|
||||||
:side-bar (local-links (util/md->html side-bar))
|
:page page
|
||||||
:header (local-links (util/md->html "/content/_header.md"))
|
:side-bar (util/local-links (util/md->html side-bar))
|
||||||
:content (if exists? (io/slurp-resource (str "/content/" page suffix)) "")
|
:content (if exists? (io/slurp-resource (str "/content/" page suffix)) "")
|
||||||
:user (session/get :user)
|
:exists exists?}))))))
|
||||||
:exists exists?})))))
|
|
||||||
|
|
||||||
|
|
||||||
(defn edit-css-page
|
(defn edit-css-page
|
||||||
|
|
@ -129,15 +118,15 @@
|
||||||
file-path (str (io/resource-path) file-name)
|
file-path (str (io/resource-path) file-name)
|
||||||
exists? (.exists (clojure.java.io/as-file file-path))]
|
exists? (.exists (clojure.java.io/as-file file-path))]
|
||||||
(cond exists?
|
(cond exists?
|
||||||
(layout/render "wiki.html"
|
(do
|
||||||
{:title page
|
(timbre/info (format "Showing page '%s'" page))
|
||||||
:page page
|
(layout/render "wiki.html"
|
||||||
:side-bar (local-links (util/md->html "/content/_side-bar.md"))
|
(merge (util/standard-params request)
|
||||||
:header (local-links (util/md->html "/content/_header.md"))
|
{:title page
|
||||||
:content (local-links (util/md->html file-name))
|
:page page
|
||||||
:user (session/get :user)
|
:content (util/local-links (util/md->html file-name))
|
||||||
:version (System/getProperty "smeagol.version")})
|
:editable true})))
|
||||||
true (response/redirect (str "/edit?page=" page)))))
|
true (response/redirect (str "edit?page=" page)))))
|
||||||
|
|
||||||
|
|
||||||
(defn history-page
|
(defn history-page
|
||||||
|
|
@ -149,11 +138,10 @@
|
||||||
file-name (str page ".md")
|
file-name (str page ".md")
|
||||||
repo-path (str (io/resource-path) "/content/")]
|
repo-path (str (io/resource-path) "/content/")]
|
||||||
(layout/render "history.html"
|
(layout/render "history.html"
|
||||||
{:title (str "History of " page)
|
(merge (util/standard-params request)
|
||||||
:page page
|
{:title (str "History of " page)
|
||||||
:side-bar (local-links (util/md->html "/content/_side-bar.md"))
|
:page page
|
||||||
:header (local-links (util/md->html "/content/_header.md"))
|
:history (hist/find-history repo-path file-name)}))))
|
||||||
:history (hist/find-history repo-path file-name)})))
|
|
||||||
|
|
||||||
|
|
||||||
(defn version-page
|
(defn version-page
|
||||||
|
|
@ -165,17 +153,13 @@
|
||||||
file-name (str page ".md")
|
file-name (str page ".md")
|
||||||
repo-path (str (io/resource-path) "/content/")]
|
repo-path (str (io/resource-path) "/content/")]
|
||||||
(layout/render "wiki.html"
|
(layout/render "wiki.html"
|
||||||
{:title (str "Version " version " of " page)
|
(merge (util/standard-params request)
|
||||||
:page page
|
{:title (str "Version " version " of " page)
|
||||||
:side-bar (local-links
|
:page page
|
||||||
(util/md->html "/content/_side-bar.md"))
|
:content (util/local-links
|
||||||
:header (local-links
|
(md/md-to-html-string
|
||||||
(util/md->html "/content/_header.md"))
|
(hist/fetch-version
|
||||||
:content (local-links
|
repo-path file-name version)))}))))
|
||||||
(md/md-to-html-string
|
|
||||||
(hist/fetch-version
|
|
||||||
repo-path file-name version)))
|
|
||||||
:user (session/get :user)})))
|
|
||||||
|
|
||||||
|
|
||||||
(defn diff-page
|
(defn diff-page
|
||||||
|
|
@ -187,14 +171,10 @@
|
||||||
file-name (str page ".md")
|
file-name (str page ".md")
|
||||||
repo-path (str (io/resource-path) "/content/")]
|
repo-path (str (io/resource-path) "/content/")]
|
||||||
(layout/render "wiki.html"
|
(layout/render "wiki.html"
|
||||||
{:title (str "Changes since version " version " of " page)
|
(merge (util/standard-params request)
|
||||||
:page page
|
{:title (str "Changes since version " version " of " page)
|
||||||
:side-bar (local-links
|
:page page
|
||||||
(util/md->html "/content/_side-bar.md"))
|
:content (d2h/diff2html (hist/diff repo-path file-name version))}))))
|
||||||
:header (local-links
|
|
||||||
(util/md->html "/content/_header.md"))
|
|
||||||
:content (d2h/diff2html (hist/diff repo-path file-name version))
|
|
||||||
:user (session/get :user)})))
|
|
||||||
|
|
||||||
|
|
||||||
(defn auth-page
|
(defn auth-page
|
||||||
|
|
@ -218,11 +198,12 @@
|
||||||
(response/redirect redirect-to))
|
(response/redirect redirect-to))
|
||||||
true
|
true
|
||||||
(layout/render "auth.html"
|
(layout/render "auth.html"
|
||||||
|
(merge (util/standard-params request)
|
||||||
{:title (if user (str "Logout " user) "Log in")
|
{:title (if user (str "Logout " user) "Log in")
|
||||||
:redirect-to ((:headers request) "referer")
|
:redirect-to ((:headers request) "referer")
|
||||||
:side-bar (local-links (util/md->html "/content/_side-bar.md"))
|
:side-bar (util/local-links (util/md->html "/content/_side-bar.md"))
|
||||||
:header (local-links (util/md->html "/content/_header.md"))
|
:header (util/local-links (util/md->html "/content/_header.md"))
|
||||||
:user user}))))
|
:user user})))))
|
||||||
|
|
||||||
|
|
||||||
(defn passwd-page
|
(defn passwd-page
|
||||||
|
|
@ -233,28 +214,32 @@
|
||||||
pass1 (:pass1 params)
|
pass1 (:pass1 params)
|
||||||
pass2 (:pass2 params)
|
pass2 (:pass2 params)
|
||||||
user (session/get :user)
|
user (session/get :user)
|
||||||
length (if pass1 (count pass1) 0)
|
|
||||||
message (cond
|
message (cond
|
||||||
(nil? oldpass) nil
|
(nil? oldpass) nil
|
||||||
(and pass1 (>= length 8) (.equals pass1 pass2) (auth/change-pass user oldpass pass2))
|
(and (auth/evaluate-password pass1 pass2) (auth/change-pass user oldpass pass2))
|
||||||
"Your password was changed"
|
"Your password was changed"
|
||||||
(< length 8) "You proposed password wasn't long enough: 8 characters required"
|
(< (count pass1) 8) "You proposed password wasn't long enough: 8 characters required"
|
||||||
(not (= pass1 pass2)) "Your proposed passwords don't match"
|
(not (= pass1 pass2)) "Your proposed passwords don't match"
|
||||||
true "Your password was not changed")] ;; but I don't know why...
|
true "Your password was not changed")] ;; but I don't know why...
|
||||||
(layout/render "passwd.html"
|
(layout/render "passwd.html"
|
||||||
{:title (str "Change passord for " user)
|
(merge (util/standard-params request)
|
||||||
:side-bar (local-links (util/md->html "/content/_side-bar.md"))
|
{:title (str "Change passord for " user)
|
||||||
:header (local-links (util/md->html "/content/_header.md"))
|
:side-bar (util/local-links (util/md->html "/content/_side-bar.md"))
|
||||||
:message message})))
|
:header (util/local-links (util/md->html "/content/_header.md"))
|
||||||
|
:message message}))))
|
||||||
|
|
||||||
|
|
||||||
(defroutes wiki-routes
|
(defroutes wiki-routes
|
||||||
(GET "/wiki" request (wiki-page request))
|
(GET "/wiki" request (wiki-page request))
|
||||||
(GET "/" request (wiki-page request))
|
(GET "/" request (wiki-page request))
|
||||||
|
(GET "/delete-user" request (route/restricted (admin/delete-user request)))
|
||||||
(GET "/edit" request (route/restricted (edit-page request)))
|
(GET "/edit" request (route/restricted (edit-page request)))
|
||||||
(POST "/edit" request (route/restricted (edit-page request)))
|
(POST "/edit" request (route/restricted (edit-page request)))
|
||||||
(GET "/edit-css" request (route/restricted (edit-css-page request)))
|
(GET "/edit-css" request (route/restricted (edit-css-page request)))
|
||||||
(POST "/edit-css" request (route/restricted (edit-css-page request)))
|
(POST "/edit-css" request (route/restricted (edit-css-page request)))
|
||||||
|
(GET "/edit-users" request (route/restricted (admin/edit-users request)))
|
||||||
|
(GET "/edit-user" request (route/restricted (admin/edit-user request)))
|
||||||
|
(POST "/edit-user" request (route/restricted (admin/edit-user request)))
|
||||||
(GET "/history" request (history-page request))
|
(GET "/history" request (history-page request))
|
||||||
(GET "/version" request (version-page request))
|
(GET "/version" request (version-page request))
|
||||||
(GET "/changes" request (diff-page request))
|
(GET "/changes" request (diff-page request))
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
(ns ^{:doc "Miscellaneous utility functions supporting Smeagol."
|
(ns ^{:doc "Miscellaneous utility functions supporting Smeagol."
|
||||||
:author "Simon Brooke"}
|
:author "Simon Brooke"}
|
||||||
smeagol.util
|
smeagol.util
|
||||||
(:require [noir.io :as io]
|
(:require [clojure.string :as cs]
|
||||||
[markdown.core :as md]))
|
[cemerick.url :refer (url url-encode url-decode)]
|
||||||
|
[noir.io :as io]
|
||||||
|
[noir.session :as session]
|
||||||
|
[markdown.core :as md]
|
||||||
|
[smeagol.authenticate :as auth]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;
|
;;;;
|
||||||
|
|
@ -31,3 +35,28 @@
|
||||||
"reads a markdown file from public/md and returns an HTML string"
|
"reads a markdown file from public/md and returns an HTML string"
|
||||||
[filename]
|
[filename]
|
||||||
(md/md-to-html-string (io/slurp-resource filename)))
|
(md/md-to-html-string (io/slurp-resource filename)))
|
||||||
|
|
||||||
|
|
||||||
|
(defn local-links
|
||||||
|
"Rewrite text in `html-src` surrounded by double square brackets as a local link into this wiki."
|
||||||
|
[^String html-src]
|
||||||
|
(cs/replace html-src #"\[\[[^\[\]]*\]\]"
|
||||||
|
#(let [text (clojure.string/replace %1 #"[\[\]]" "")
|
||||||
|
encoded (url-encode text)
|
||||||
|
;; I use '\_' to represent '_' in wiki markup, because
|
||||||
|
;; '_' is meaningful in Markdown. However, this needs to
|
||||||
|
;; be stripped out when interpreting local links.
|
||||||
|
munged (cs/replace encoded #"%26%2395%3B" "_")]
|
||||||
|
(format "<a href='wiki?page=%s'>%s</a>" munged text))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn standard-params
|
||||||
|
"Return a map of standard parameters to pass to the template renderer."
|
||||||
|
[request]
|
||||||
|
(let [user (session/get :user)]
|
||||||
|
{:user user
|
||||||
|
:admin (auth/get-admin user)
|
||||||
|
:side-bar (local-links (md->html "/content/_side-bar.md"))
|
||||||
|
:header (local-links (md->html "/content/_header.md"))
|
||||||
|
:version (System/getProperty "smeagol.version")}))
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue