mirror of
https://github.com/journeyman-cc/smeagol.git
synced 2026-04-12 18:05:06 +00:00
Merge branch 'release/1.0.0-rc3'
This commit is contained in:
commit
835dec0f1c
55 changed files with 2693 additions and 647 deletions
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -7,12 +7,14 @@ pom.xml.asc
|
||||||
/target/
|
/target/
|
||||||
/checkouts/
|
/checkouts/
|
||||||
/resources/public/content/.git
|
/resources/public/content/.git
|
||||||
|
/resources/public/vendor
|
||||||
.lein-deps-sum
|
.lein-deps-sum
|
||||||
.lein-repl-history
|
.lein-repl-history
|
||||||
.lein-plugins/
|
.lein-plugins/
|
||||||
.lein-failures
|
.lein-failures
|
||||||
.lein-env
|
.lein-env
|
||||||
|
.nrepl-port
|
||||||
|
smeagol.log*
|
||||||
/node_modules/
|
/node_modules/
|
||||||
/resources/public/vendor/
|
.DS_Store
|
||||||
|
|
||||||
|
|
|
||||||
3
Dockerfile
Normal file
3
Dockerfile
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
FROM tomcat:alpine
|
||||||
|
COPY target/smeagol-*-standalone.war $CATALINA_HOME/webapps/smeagol.war
|
||||||
|
|
||||||
103
README.md
103
README.md
|
|
@ -5,12 +5,86 @@ Smeagol is a simple Wiki engine inspired by [Gollum](https://github.com/gollum/g
|
||||||
|
|
||||||
So at this stage Smeagol is a Wiki engine written in Clojure which uses Markdown as its text format, which does have user authentication, and which uses Git as its versioning and backup system.
|
So at this stage Smeagol is a Wiki engine written in Clojure which uses Markdown as its text format, which does have user authentication, and which uses Git as its versioning and backup system.
|
||||||
|
|
||||||
|
<a href="https://zenhub.com"><img src="https://raw.githubusercontent.com/ZenHubIO/support/master/zenhub-badge.png"></a>
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
Smeagol is now a fully working small Wiki engine, and meets my own immediate needs.
|
Smeagol is now a fully working small Wiki engine, and meets my own immediate needs.
|
||||||
|
|
||||||
## 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.
|
||||||
|
|
||||||
|
### Pluggable extensible markup
|
||||||
|
|
||||||
|
A system of pluggable, extensible formatters is supported. In normal markdown, code blocks may be delimited by three backticks at start and end, and often the syntax of the code can be indicated by a token immediately following the opening three backticks. This has been extended to allow custom formatters to be provided for such code blocks. Two example formatters are provided:
|
||||||
|
|
||||||
|
#### The Vega formatter
|
||||||
|
|
||||||
|
Inspired by [visdown](http://visdown.amitkaps.com/) and [vega-lite](https://vega.github.io/vega-lite/docs/), the Vega formatter allows you to embed vega data visualisations into Smeagol pages. The graph description should start with a line comprising three back-ticks and then the word '`vega`', and end with a line comprising just three backticks.
|
||||||
|
|
||||||
|
Here's an example cribbed in its entirety from [here](http://visdown.amitkaps.com/london):
|
||||||
|
|
||||||
|
##### Flight punctuality at London airports
|
||||||
|
|
||||||
|
```vega
|
||||||
|
data:
|
||||||
|
url: "data/london.csv"
|
||||||
|
transform:
|
||||||
|
-
|
||||||
|
filter: datum.year == 2016
|
||||||
|
mark: rect
|
||||||
|
encoding:
|
||||||
|
x:
|
||||||
|
type: nominal
|
||||||
|
field: source
|
||||||
|
y:
|
||||||
|
type: nominal
|
||||||
|
field: dest
|
||||||
|
color:
|
||||||
|
type: quantitative
|
||||||
|
field: flights
|
||||||
|
aggregate: sum
|
||||||
|
```
|
||||||
|
|
||||||
|
Data files can be uploaded in the same way as images, by using the **upload a file** link.
|
||||||
|
|
||||||
|
Note that this visualisation will not be rendered in the GitHub wiki, as it doesn't have Smeagol's data visualisation magic. This is what it should look like:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### The Mermaid formatter
|
||||||
|
|
||||||
|
Graphs can now be embedded in a page using the [Mermaid](http://knsv.github.io/mermaid/index.html) graph description language. The graph description should start with a line comprising three back-ticks and then the word `mermaid`, and end with a line comprising just three backticks.
|
||||||
|
|
||||||
|
Here's an example culled from the Mermaid documentation.
|
||||||
|
|
||||||
|
##### GANTT Chart
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
gantt
|
||||||
|
dateFormat YYYY-MM-DD
|
||||||
|
title Adding GANTT diagram functionality to mermaid
|
||||||
|
section A section
|
||||||
|
Completed task :done, des1, 2014-01-06,2014-01-08
|
||||||
|
Active task :active, des2, 2014-01-09, 3d
|
||||||
|
Future task : des3, after des2, 5d
|
||||||
|
Future task2 : des4, after des3, 5d
|
||||||
|
section Critical tasks
|
||||||
|
Completed task in the critical line :crit, done, 2014-01-06,24h
|
||||||
|
Implement parser and jison :crit, done, after des1, 2d
|
||||||
|
Create tests for parser :crit, active, 3d
|
||||||
|
Future task in critical line :crit, 5d
|
||||||
|
Create tests for renderer :2d
|
||||||
|
Add to mermaid :1d
|
||||||
|
```
|
||||||
|
|
||||||
|
To add your own formatter, compile it into a jar file which is on the classpath - it does *not* have to be part of the Smeagol project directly, and then edit the value of the key `:formatters` in the file `config.edn`; whose standard definition is:
|
||||||
|
|
||||||
|
:formatters {"vega" smeagol.formatting/process-vega
|
||||||
|
"vis" smeagol.formatting/process-vega
|
||||||
|
"mermaid" smeagol.formatting/process-mermaid}
|
||||||
|
|
||||||
|
The added key should be the word which will follow the opening three backticks of your code block, and the value of that key should be a symbol which evaluates to a function which can format the code block as required.
|
||||||
|
|
||||||
## 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:
|
||||||
|
|
||||||
|
|
@ -20,15 +94,12 @@ Security is now greatly improved. There is a file called *passwd* in the *resour
|
||||||
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.
|
||||||
|
|
||||||
## 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:
|
You can (if you're logged in) upload files, including images, using the **Upload a file** link on the top menu bar. You can link to an uploaded image, or other images already available on the web, like this:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Todo
|
|
||||||
* Mechanism to add users through the user interface;
|
|
||||||
|
|
||||||
## Advertisement
|
## Advertisement
|
||||||
If you like what you see here, I am available for work on open source Clojure projects. Contact me vis [WEFT](http://www.weft.scot/).
|
If you like what you see here, I am available for work on open source Clojure projects.
|
||||||
|
|
||||||
### Phoning home
|
### Phoning home
|
||||||
Smeagol currently requests the WEFT logo in the page footer from my home site. This is mainly so I can get a feel for how many people are using the product. If you object to this, edit the file
|
Smeagol currently requests the WEFT logo in the page footer from my home site. This is mainly so I can get a feel for how many people are using the product. If you object to this, edit the file
|
||||||
|
|
@ -67,3 +138,25 @@ Alternatively, if you want to deploy to a servlet container (which I would stron
|
||||||
|
|
||||||
(a command which I'm sure Smeagol would entirely appreciate) and deploy the resulting war file.
|
(a command which I'm sure Smeagol would entirely appreciate) and deploy the resulting war file.
|
||||||
|
|
||||||
|
## Experimental Docker image
|
||||||
|
|
||||||
|
You can now run Smeagol as a [Docker](http://www.docker.com) image. To run my Docker image, use
|
||||||
|
|
||||||
|
docker run simonbrooke/smeagol
|
||||||
|
|
||||||
|
Smeagol will run, obviously, on the IP address of your Docker image, on port 8080. To find the IP address, start the image using the command above and then use
|
||||||
|
|
||||||
|
docker inspect --format '{{ .NetworkSettings.IPAddress }}' $(docker ps -q)
|
||||||
|
|
||||||
|
Suppose this prints '10.10.10.10', then the URL to browse to will be http://10.10.10.10:8080/smeagol/
|
||||||
|
|
||||||
|
This image is _experimental_, but it does seem to work fairly well. What it does **not** yet do, however, is push the git repository to a remote location, so when you tear the Docker image down your edits will be lost. My next objective for this image is for it to have a cammand line parameter being the git address of a repository from which it can initialise the Wiki content, and to which it will periodically push local changes to the Wiki content.
|
||||||
|
|
||||||
|
To build your own Docker image, run:
|
||||||
|
|
||||||
|
lein clean
|
||||||
|
lein bower install
|
||||||
|
lein ring uberwar
|
||||||
|
lein docker build
|
||||||
|
|
||||||
|
This will build a new Docker image locally; you can, obviously, push it to your own Docker repository if you wish.
|
||||||
|
|
|
||||||
106
project.clj
106
project.clj
|
|
@ -1,54 +1,86 @@
|
||||||
(defproject smeagol "0.5.0-rc3"
|
(defproject smeagol "1.0.0-rc3"
|
||||||
:description "A simple Git-backed Wiki inspired by Gollum"
|
:description "A simple Git-backed Wiki inspired by Gollum"
|
||||||
:url "https://github.com/simon-brooke/smeagol"
|
:url "https://github.com/simon-brooke/smeagol"
|
||||||
:dependencies [[org.clojure/clojure "1.7.0"]
|
:license {:name "GNU General Public License,version 2.0 or (at your option) any later version"
|
||||||
[org.clojure/core.memoize "0.5.9"]
|
:url "https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html"}
|
||||||
[com.taoensso/encore "2.91.1"]
|
:dependencies [[clj-jgit "0.8.10"]
|
||||||
[lib-noir "0.9.9" :exclusions [org.clojure/tools.reader]]
|
[clj-yaml "0.4.0"]
|
||||||
|
[com.cemerick/url "0.1.1"]
|
||||||
|
[com.fzakaria/slf4j-timbre "0.3.7"]
|
||||||
|
[com.taoensso/encore "2.92.0"]
|
||||||
[com.cemerick/url "0.1.1"]
|
[com.cemerick/url "0.1.1"]
|
||||||
[ring-server "0.4.0"]
|
|
||||||
[selmer "1.10.9"]
|
|
||||||
|
|
||||||
[org.clojure/tools.logging "0.4.0"]
|
|
||||||
[com.taoensso/timbre "4.10.0"]
|
[com.taoensso/timbre "4.10.0"]
|
||||||
[com.fzakaria/slf4j-timbre "0.3.7"]
|
[com.fzakaria/slf4j-timbre "0.3.7"]
|
||||||
|
[com.taoensso/tower "3.0.2" :exclusions [com.taoensso/encore]]
|
||||||
|
[crypto-password "0.2.0"]
|
||||||
|
[environ "1.1.0"]
|
||||||
|
[hiccup "1.0.5"]
|
||||||
|
[im.chit/cronj "1.4.4"]
|
||||||
|
[lib-noir "0.9.9" :exclusions [org.clojure/tools.reader]]
|
||||||
|
[markdown-clj "0.9.99" :exclusions [com.keminglabs/cljx]]
|
||||||
|
[noir-exception "0.2.5"]
|
||||||
|
[org.clojars.simon_brooke/internationalisation "1.0.3"]
|
||||||
|
[org.clojure/clojure "1.8.0"]
|
||||||
|
[org.clojure/core.memoize "0.5.9"]
|
||||||
|
[org.clojure/data.json "0.2.6"]
|
||||||
|
[org.clojure/tools.logging "0.4.0"]
|
||||||
[org.slf4j/slf4j-api "1.7.25"]
|
[org.slf4j/slf4j-api "1.7.25"]
|
||||||
[org.slf4j/log4j-over-slf4j "1.7.25"]
|
[org.slf4j/log4j-over-slf4j "1.7.25"]
|
||||||
[org.slf4j/jul-to-slf4j "1.7.25"]
|
[org.slf4j/jul-to-slf4j "1.7.25"]
|
||||||
[org.slf4j/jcl-over-slf4j "1.7.25"]
|
[org.slf4j/jcl-over-slf4j "1.7.25"]
|
||||||
|
[prone "1.1.4"]
|
||||||
[com.taoensso/tower "3.0.2" :exclusions [com.taoensso/encore]]
|
[ring/ring-anti-forgery "1.1.0"]
|
||||||
[markdown-clj "0.9.99" :exclusions [com.keminglabs/cljx]]
|
[ring-server "0.4.0"]
|
||||||
[crypto-password "0.2.0"]
|
[selmer "1.11.0"]]
|
||||||
[clj-jgit "0.8.9"]
|
|
||||||
[environ "1.1.0"]
|
|
||||||
[im.chit/cronj "1.4.4"]
|
|
||||||
[noir-exception "0.2.5"]
|
|
||||||
[prone "1.1.4"]]
|
|
||||||
|
|
||||||
:repl-options {:init-ns smeagol.repl}
|
:repl-options {:init-ns smeagol.repl}
|
||||||
|
|
||||||
:jvm-opts ["-server"]
|
:jvm-opts ["-server"]
|
||||||
:plugins [[lein-ring "0.8.13" :exclusions [org.clojure/clojure]]
|
|
||||||
[lein-environ "1.0.0"]
|
:plugins [[lein-ancient "0.5.5" :exclusions [org.clojure/clojure org.clojure/data.xml]]
|
||||||
[lein-bower "0.5.1"]
|
[lein-bower "0.5.1"]
|
||||||
[lein-ancient "0.5.5" :exclusions [org.clojure/clojure org.clojure/data.xml]]
|
[lein-codox "0.10.3"]
|
||||||
[lein-marginalia "0.7.1" :exclusions [org.clojure/clojure]]]
|
[io.sarnowski/lein-docker "1.0.0"]
|
||||||
:bower-dependencies [[simplemde "1.11.2"]]
|
[lein-environ "1.0.0"]
|
||||||
|
[lein-marginalia "0.7.1" :exclusions [org.clojure/clojure]]
|
||||||
|
[lein-ring "0.8.13" :exclusions [org.clojure/clojure]]]
|
||||||
|
|
||||||
|
:bower-dependencies [[simplemde "1.11.2"]
|
||||||
|
;; [vega-embed "3.0.0-beta.20"] ;; vega-embed currently not loaded from Bower because of
|
||||||
|
;; dependency conflict which will hopefully be resolved soon.
|
||||||
|
[vega-lite "2.0.0-beta.11"]
|
||||||
|
[mermaid "6.0.0"]]
|
||||||
|
|
||||||
|
:docker {:image-name "simonbrooke/smeagol"
|
||||||
|
:dockerfile "Dockerfile"}
|
||||||
|
|
||||||
:ring {:handler smeagol.handler/app
|
:ring {:handler smeagol.handler/app
|
||||||
:init smeagol.handler/init
|
:init smeagol.handler/init
|
||||||
:destroy smeagol.handler/destroy}
|
:destroy smeagol.handler/destroy}
|
||||||
:lein-release {:scm :git :deploy-via :lein-install}
|
|
||||||
:profiles
|
:release-tasks [["vcs" "assert-committed"]
|
||||||
{:uberjar {:omit-source true
|
["change" "version" "leiningen.release/bump-version" "release"]
|
||||||
:env {:production true}
|
["vcs" "commit"]
|
||||||
:aot :all}
|
;; ["vcs" "tag"] -- not working, problems with secret key
|
||||||
:production {:ring {:open-browser? false
|
["clean"]
|
||||||
:stacktraces? false
|
["bower" "install"]
|
||||||
:auto-reload? false}}
|
["ring" "uberwar"]
|
||||||
:dev {:dependencies [[ring-mock "0.1.5"]
|
["docker" "build"]
|
||||||
[ring/ring-devel "1.6.2"]
|
["docker" "push"]
|
||||||
[pjstadig/humane-test-output "0.8.2"]]
|
["change" "version" "leiningen.release/bump-version"]
|
||||||
:injections [(require 'pjstadig.humane-test-output)
|
["vcs" "commit"]]
|
||||||
(pjstadig.humane-test-output/activate!)]
|
|
||||||
:env {:dev true}}}
|
:profiles {:uberjar {:omit-source true
|
||||||
|
:env {:production true}
|
||||||
|
:aot :all}
|
||||||
|
:production {:ring {:open-browser? false
|
||||||
|
:stacktraces? false
|
||||||
|
:auto-reload? false}}
|
||||||
|
:dev {:dependencies [[ring-mock "0.1.5"]
|
||||||
|
[ring/ring-devel "1.6.2"]
|
||||||
|
[pjstadig/humane-test-output "0.8.2"]]
|
||||||
|
:injections [(require 'pjstadig.humane-test-output)
|
||||||
|
(pjstadig.humane-test-output/activate!)]
|
||||||
|
:env {:dev true}}}
|
||||||
|
|
||||||
:min-lein-version "2.0.0")
|
:min-lein-version "2.0.0")
|
||||||
|
|
|
||||||
35
resources/config.edn
Normal file
35
resources/config.edn
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;;;;
|
||||||
|
;;;; 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) 2017 Simon Brooke
|
||||||
|
;;;;
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
;;; config.edn: a simple configuration map for Smeagol; inspired by Cryogen.
|
||||||
|
;;; This is top-level configuration.
|
||||||
|
|
||||||
|
;; ; ; ; ; ; ; ; ; ;
|
||||||
|
{
|
||||||
|
:site-title "Smeagol" ;; overall title of the site, used in page headings
|
||||||
|
:default-locale "en-GB" ;; default language used for messages
|
||||||
|
:formatters {"vega" smeagol.formatting/process-vega
|
||||||
|
"vis" smeagol.formatting/process-vega
|
||||||
|
"mermaid" smeagol.formatting/process-mermaid
|
||||||
|
"backticks" smeagol.formatting/process-backticks}
|
||||||
|
}
|
||||||
143
resources/i18n/en-GB.edn
Normal file
143
resources/i18n/en-GB.edn
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;;;;
|
||||||
|
;;;; 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) 2017 Simon Brooke
|
||||||
|
;;;;
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
;;; en-GB.edn: English-language messages.
|
||||||
|
;;; This is essentially all the text in the chrome - that which isn't editable
|
||||||
|
;;; through the wiki itself; and the test in the sanity check report.
|
||||||
|
|
||||||
|
;; ; ; ; ; ; ; ; ; ;
|
||||||
|
{:add-user-label "Add new user" ;; label for the add user link on edit users page
|
||||||
|
:change-pass-label "Change password!"
|
||||||
|
;; text of the change password widget itself on the
|
||||||
|
;; change password page
|
||||||
|
:change-pass-link "Change password"
|
||||||
|
;; text of the change password link on the menu
|
||||||
|
:change-pass-prompt "To change your password"
|
||||||
|
;; text of the change password widget prompt on the
|
||||||
|
;; change password page
|
||||||
|
:change-col-hdr "Changes" ;; header for the changes column in history
|
||||||
|
:chpass-bad-match "Your proposed passwords don't match"
|
||||||
|
;; error text if proposed passwords don't match
|
||||||
|
:chpass-fail "Your password was not changed"
|
||||||
|
;; error text on fail other htan too short or bad match
|
||||||
|
:chpass-success "Your password was changed"
|
||||||
|
;; confirmation text on password change
|
||||||
|
:chpass-too-short "You proposed password wasn't long enough: eight characters required"
|
||||||
|
;; error text if proposed password is too short
|
||||||
|
:chpass-title-prefix "Change password for"
|
||||||
|
;; prefix for title of change password page
|
||||||
|
:content-dir "The content directory"
|
||||||
|
;; used in sanity check report
|
||||||
|
:content-dir-exists "The content directory exists"
|
||||||
|
;; used in sanity check report
|
||||||
|
:content-dir-is-dir "The content directory is a directory"
|
||||||
|
;; used in sanity check report
|
||||||
|
:cookies-about "About cookies" ;; about cookies text
|
||||||
|
:cookies-more "This website stores session information as a 'cookie' on your browser. This helps us show you the content you want to see. This cookie does not identify you, and cannot be read by other websites. It is deleted by your browser as soon as you leave this site. This website does not use any third party cookies, so your visit here cannot be tracked by other websites."
|
||||||
|
;; more about cookies text
|
||||||
|
:default-page-title "Introduction" ;; title of the default page in this wiki
|
||||||
|
:del-col-hdr "Delete" ;; header for delete column on edit users page
|
||||||
|
:del-user-fail "Could not delete user"
|
||||||
|
;; error message on failure to delete user
|
||||||
|
:del-user-success "Successfully deleted user"
|
||||||
|
;; confirmation message on deletion of user
|
||||||
|
:diff-title-prefix "Changes since version"
|
||||||
|
;; prefix for the header of the changes page
|
||||||
|
:does-not-exist "does not exist"
|
||||||
|
;; (of a file or directory); used in sanity check report
|
||||||
|
:edit-col-hdr "Edit" ;; header for edit column on edit users page
|
||||||
|
:edit-page-link "Edit this page"
|
||||||
|
;; text of the edit page link on the content frame
|
||||||
|
:edit-title-prefix "Edit" ;; prefix for title of edit content page
|
||||||
|
:edit-users-link "Edit users" ;; text of the edit users link on the menu
|
||||||
|
:edit-users-title "Select user to edit"
|
||||||
|
;; title of edit users page
|
||||||
|
:email-prompt "Email address" ;; text of the email widget prompt on edit user page
|
||||||
|
:file-or-directory "File or directory"
|
||||||
|
;; used in sanity check report
|
||||||
|
:file-summary-prompt "Description/what's changed"
|
||||||
|
;; prompt for the file upload summary input
|
||||||
|
:file-upload-link-text "You may link to this file using a link of the form"
|
||||||
|
;; Text introducing the link to an uploaded file
|
||||||
|
:file-upload-prompt "File to upload" ;; prompt string for the file upload widget
|
||||||
|
:file-upload-title "Upload a file" ;; title for the file upload page
|
||||||
|
:is-admin-prompt "Is administrator?"
|
||||||
|
:here "here" ;; used in sanity check report
|
||||||
|
:home-link "Home" ;; text of the home link on the menu
|
||||||
|
:is-not-directory "is not a directory"
|
||||||
|
;; (of a file or directory) used in sanity check report
|
||||||
|
:is-not-readable "is not readable"
|
||||||
|
;; (of a file or directory) used in sanity check report
|
||||||
|
:is-not-writable "is not writable"
|
||||||
|
;; (of a file or directory) used in sanity check report
|
||||||
|
:login-label "Log in!" ;; text of the login widget on the login page
|
||||||
|
:login-link "Log in" ;; text of the login link on the menu
|
||||||
|
:login-prompt "To edit this wiki"
|
||||||
|
;; text of the action widget prompt on the login page
|
||||||
|
:logout-label "Log out!" ;; text of the logout widget on the logout page
|
||||||
|
:logout-link "Log out" ;; text of the logout link on the menu
|
||||||
|
:logged-in-as "You are logged in as"
|
||||||
|
;; text of the 'logged in as' label on the menu
|
||||||
|
:history-link "History" ;; text of the history link on the content frame
|
||||||
|
:history-title-prefix "History of" ;; prefix of the title on the history page
|
||||||
|
:new-pass-prompt "New password" ;; text of the new password widget prompt on the change
|
||||||
|
;; password and edit user pages
|
||||||
|
:no-admin-users "There are no users in the 'passwd' file with administrative privileges"
|
||||||
|
;; used in sanity check report
|
||||||
|
:old-pass-prompt "Your password"
|
||||||
|
;; text of the old password widget prompt on the change
|
||||||
|
;; password page, and password widget on login page
|
||||||
|
:password-file "the password ('passwd') file"
|
||||||
|
;; used in sanity check report
|
||||||
|
:problems-found "problems were found"
|
||||||
|
;; used in sanity check report
|
||||||
|
:rpt-pass-prompt "And again" ;; text of the new password widget prompt on the change
|
||||||
|
;; password and edit user pages
|
||||||
|
:save-prompt "When you have finished editing"
|
||||||
|
;; text of the save widget label on edit content
|
||||||
|
;; and edit user page
|
||||||
|
:save-label "Save!" ;; text of the save widget itself
|
||||||
|
:save-user-fail "Failed to store user"
|
||||||
|
:save-user-success "Successfully stored user"
|
||||||
|
:see-documentation "For more information please see documentation "
|
||||||
|
;; used in sanity check report
|
||||||
|
:smeagol-not-initialised
|
||||||
|
"Smeagol is not initialised correctly"
|
||||||
|
;; title of the sanity check report
|
||||||
|
:smeagol-misconfiguration
|
||||||
|
"Smeagol has been unable to find some of the resources on which it depends,
|
||||||
|
possibly because of misconfiguration or missing environment variables."
|
||||||
|
;; used in sanity check report
|
||||||
|
:user-lacks-field "User record in the passwd file lacks a field"
|
||||||
|
;; used in sanity check report
|
||||||
|
:username-prompt "Username" ;; text of the username widget prompt on edit user page
|
||||||
|
;; text of the is admin widget prompt on edit user page
|
||||||
|
:user-title-prefix "Edit user" ;; prefix for title of edit user page
|
||||||
|
:vers-col-hdr "Version" ;; header for the version column in history
|
||||||
|
:what-col-hdr "What" ;; header for the what column in history
|
||||||
|
:what-changed-prompt "What have you changed?"
|
||||||
|
;; text of the summary widget prompt on edit
|
||||||
|
;; content page
|
||||||
|
:when-col-hdr "When" ;; header for the when column in history
|
||||||
|
:your-uname-prompt "Your username" ;; text of the username widget prompt on the login page
|
||||||
|
}
|
||||||
1
resources/i18n/en.edn
Symbolic link
1
resources/i18n/en.edn
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
en-GB.edn
|
||||||
|
|
@ -1 +1 @@
|
||||||
{:admin {:admin true, :email "info@weft.scot", :password "admin"}}
|
{:admin {:admin true, :email "info@weft.scot", :password "admin"}, :jenny {:email "jenny@auchencairn.org", :admin false, :password "$s0$f0801$1uniQfftB37G5e5GklJANQ==$kQ0+/YcCuaz2x5iYjwhNlDlnWX/exE/8pSC+R4C0WvQ="}}
|
||||||
20
resources/public/content/Configuration.md
Normal file
20
resources/public/content/Configuration.md
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
Smeagol reads a configuration file, whose content should be formatted as a clojure map.
|
||||||
|
|
||||||
|
The default content is as follows:
|
||||||
|
|
||||||
|
{
|
||||||
|
:site-title "Smeagol" ;; overall title of the site, used in page headings
|
||||||
|
:default-locale "en-GB" ;; default language used for messages
|
||||||
|
:formatters {"vega" smeagol.formatting/process-vega
|
||||||
|
"vis" smeagol.formatting/process-vega
|
||||||
|
"mermaid" smeagol.formatting/process-mermaid
|
||||||
|
"backticks" smeagol.formatting/process-backticks}
|
||||||
|
}
|
||||||
|
|
||||||
|
The three keys given above should be present. The values should be:
|
||||||
|
|
||||||
|
* **:site-title** The title for your wiki
|
||||||
|
* **:default-locale** A string comprising a lower-case [ISO 639](https://en.wikipedia.org/wiki/ISO_639) code specifying a language, optionally followed by a hyphen and an upper-case [ISO 3166](https://en.wikipedia.org/wiki/ISO_3166) specifying a country.
|
||||||
|
* **:formatters** A map of formatters used in [[Extensible Markup]], q.v.
|
||||||
|
|
||||||
|
The default file is at `resources/config.edn`; this default can be overridden by providing an environment variable, `SMEAGOL_CONFIG`, whose value is the full or relative pathname of a suitable file.
|
||||||
60
resources/public/content/Deploying Smeagol.md
Normal file
60
resources/public/content/Deploying Smeagol.md
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
## Deploying as a stand-alone application
|
||||||
|
To deploy Smeagol as a stand-alone application, either download the jar file for the release you want to deploy, or clone the source and compile it with:
|
||||||
|
|
||||||
|
lein bower install
|
||||||
|
lein ring uberjar
|
||||||
|
|
||||||
|
This will create a jar file in the `target` directory, named `smeagol-`*VERSION*`-standalone.jar`.
|
||||||
|
|
||||||
|
Smeagol cannot access either its configuration or its content from the jar file, as otherwise they would not be editable. Consequently you should set up three environment variables:
|
||||||
|
|
||||||
|
1. `SMEAGOL_CONFIG` should be the full or relative pathname of a Smeagol [[Configuration]] file;
|
||||||
|
2. `SMEAGOL_CONTENT_DIR` should be the full or relative pathname of the directory from which Smeagol should serve content (which may initially be empty, but must be writable by the process which runs Smeagol)'
|
||||||
|
3. `SMEAGOL_PASSWD` should be the full or relative pathname of a Smeagol Passwd file - see [[Security and authentication]]. This file must contain an entry for at least your initial user, and, if you want to administer users through the user interface, must be writable by the process which runs Smeagol.
|
||||||
|
|
||||||
|
**NOTE** that `SMEAGOL_CONTENT_DIR` must contain at least the following files:
|
||||||
|
|
||||||
|
1. `_edit-side-bar.md` - the side-bar that should be displayed when editing pages;
|
||||||
|
2. `_header.md` - the header to be displayed on all pages;
|
||||||
|
3. `_side-bar.md` - the side-bar that should be displayed when not editing pages.
|
||||||
|
|
||||||
|
Standard versions of these files can be found in the [source repository](https://github.com/journeyman-cc/smeagol/tree/master/resources/public/content). All these files should be in markdown format - see [[Extensible Markup]].
|
||||||
|
|
||||||
|
You can run the jar file with:
|
||||||
|
|
||||||
|
java -jar smeagol-VERSION-standalone.jar
|
||||||
|
|
||||||
|
## Deploying within a servlet container
|
||||||
|
To deploy Smeagol within a servlet container, either download the jar file for the release you want to deploy, or clone the source and compile it with:
|
||||||
|
|
||||||
|
lein bower install
|
||||||
|
lein ring uberwar
|
||||||
|
|
||||||
|
This will create a war file in the `target` directory, named `smeagol-`*VERSION*`-standalone.war`. Deploy this to your servlet container in the normal way; details will depend on your container. Instructions for Tomcat are [here](https://tomcat.apache.org/tomcat-8.0-doc/deployer-howto.html).
|
||||||
|
|
||||||
|
The problem with this is that unless the environment variables (see above) were already set up in the environment of the servlet container at the time when the servlet container were launched, Smeagol will run with its built-in defaults. This will run perfectly satisfactorily provided your servlet container is configured to unpack war files, which most are.
|
||||||
|
|
||||||
|
## Experimental Docker image
|
||||||
|
|
||||||
|
You can now run Smeagol as a [Docker](http://www.docker.com) image. Read more about [[Using the Docker Image]].
|
||||||
|
|
||||||
|
To run my Docker image, use
|
||||||
|
|
||||||
|
docker run simonbrooke/smeagol
|
||||||
|
|
||||||
|
Smeagol will run, obviously, on the IP address of your Docker image, on port 8080. To find the IP address, start the image using the command above and then use
|
||||||
|
|
||||||
|
docker inspect --format '{{ .NetworkSettings.IPAddress }}' $(docker ps -q)
|
||||||
|
|
||||||
|
Suppose this prints '10.10.10.10', then the URL to browse to will be http://10.10.10.10:8080/smeagol/
|
||||||
|
|
||||||
|
This image is _experimental_, but it does seem to work fairly well. What it does **not** yet do, however, is push the git repository to a remote location, so when you tear the Docker image down your edits will be lost. My next objective for this image is for it to have a cammand line parameter being the git address of a repository from which it can initialise the Wiki content, and to which it will periodically push local changes to the Wiki content.
|
||||||
|
|
||||||
|
To build your own Docker image, run:
|
||||||
|
|
||||||
|
lein clean
|
||||||
|
lein bower install
|
||||||
|
lein ring uberwar
|
||||||
|
lein docker build
|
||||||
|
|
||||||
|
This will build a new Docker image locally; you can, obviously, push it to your own Docker repository if you wish.
|
||||||
24
resources/public/content/Developing Smeagol.md
Normal file
24
resources/public/content/Developing Smeagol.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
You will need [Leiningen](https://github.com/technomancy/leiningen) 2.0 or above installed.
|
||||||
|
|
||||||
|
You will need [node](https://nodejs.org/en/) and [bower](https://bower.io/) installed.
|
||||||
|
|
||||||
|
## Running in development
|
||||||
|
To start a web server for the application during development, run:
|
||||||
|
|
||||||
|
lein bower install
|
||||||
|
lein ring server
|
||||||
|
|
||||||
|
This should start a development server, and open a new window or tab in your default browser with the default page of the wiki loaded into it.
|
||||||
|
|
||||||
|
## Editing
|
||||||
|
I generally use [LightTable](http://lighttable.com/) as my `Clojure` editor, but it doesn't really matter what you use; if you run Smeagol as described above, then all changes you make in the code (and save) will instantly be applied to the running system. This makes for a productive development environment.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
It is my intention that the code should be sufficiently well documented to be easy to understand. Documentation may be generated from the code by running
|
||||||
|
|
||||||
|
lein codox
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
If you make changes to Smeagol which you think are useful, please contribute them in the form of a [pull request on github](https://help.github.com/articles/creating-a-pull-request/).
|
||||||
101
resources/public/content/Extensible Markup.md
Normal file
101
resources/public/content/Extensible Markup.md
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
The basic format of Smeagol pages is [Markdown](https://daringfireball.net/projects/markdown/); documentation on how to format them is [here](https://daringfireball.net/projects/markdown/syntax). Note that there are a number of slightly different variants of Markdown; the version used by Smeagol does not currently allow tables.
|
||||||
|
|
||||||
|
A system of pluggable, extensible formatters is supported. In normal markdown, code blocks may be delimited by three backticks at start and end, and often the syntax of the code can be indicated by a token immediately following the opening three backticks. This has been extended to allow custom formatters to be provided for such code blocks. Two example formatters are provided:
|
||||||
|
|
||||||
|
## The Vega formatter
|
||||||
|
|
||||||
|
Inspired by [visdown](http://visdown.amitkaps.com/) and [vega-lite](https://vega.github.io/vega-lite/docs/), the Vega formatter allows you to embed vega data visualisations into Smeagol pages. The graph description should start with a line comprising three back-ticks and then the word '`vega`', and end with a line comprising just three backticks.
|
||||||
|
|
||||||
|
Here's an example cribbed in its entirety from [here](http://visdown.amitkaps.com/london):
|
||||||
|
|
||||||
|
### Flight punctuality at London airports
|
||||||
|
|
||||||
|
```vega
|
||||||
|
data:
|
||||||
|
url: "data/london.csv"
|
||||||
|
transform:
|
||||||
|
-
|
||||||
|
filter: datum.year == 2016
|
||||||
|
mark: rect
|
||||||
|
encoding:
|
||||||
|
x:
|
||||||
|
type: nominal
|
||||||
|
field: source
|
||||||
|
y:
|
||||||
|
type: nominal
|
||||||
|
field: dest
|
||||||
|
color:
|
||||||
|
type: quantitative
|
||||||
|
field: flights
|
||||||
|
aggregate: sum
|
||||||
|
```
|
||||||
|
|
||||||
|
Data files can be uploaded in the same way as images, by using the **upload a file** link.
|
||||||
|
|
||||||
|
## The Mermaid formatter
|
||||||
|
|
||||||
|
Graphs can now be embedded in a page using the [Mermaid](http://knsv.github.io/mermaid/index.html) graph description language. The graph description should start with a line comprising three back-ticks and then the word `mermaid`, and end with a line comprising just three backticks.
|
||||||
|
|
||||||
|
Here's an example culled from the Mermaid documentation.
|
||||||
|
|
||||||
|
### GANTT Chart
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
gantt
|
||||||
|
dateFormat YYYY-MM-DD
|
||||||
|
title Adding GANTT diagram functionality to mermaid
|
||||||
|
section A section
|
||||||
|
Completed task :done, des1, 2014-01-06,2014-01-08
|
||||||
|
Active task :active, des2, 2014-01-09, 3d
|
||||||
|
Future task : des3, after des2, 5d
|
||||||
|
Future task2 : des4, after des3, 5d
|
||||||
|
section Critical tasks
|
||||||
|
Completed task in the critical line :crit, done, 2014-01-06,24h
|
||||||
|
Implement parser and jison :crit, done, after des1, 2d
|
||||||
|
Create tests for parser :crit, active, 3d
|
||||||
|
Future task in critical line :crit, 5d
|
||||||
|
Create tests for renderer :2d
|
||||||
|
Add to mermaid :1d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Writing your own custom formatters
|
||||||
|
|
||||||
|
A custom formatter is simply a Clojure function which takes a string and an integer as arguments and produces a string as output. The string is the text the user has typed into their markdown; the integer is simply a number you can use to keep track of which addition to the page this is, in order, for example, to fix up some JavaScript to render it.
|
||||||
|
|
||||||
|
For example, here's the formatter which handles the Vega charts:
|
||||||
|
|
||||||
|
(defn process-vega
|
||||||
|
"Process this `vega-src` string, assumed to be in YAML format, into a specification
|
||||||
|
of a Vega chart, and add the plumbing to render it."
|
||||||
|
[^String vega-src ^Integer index]
|
||||||
|
(str
|
||||||
|
"<div class='data-visualisation' id='vis" index "'></div>\n"
|
||||||
|
"<script>\n//<![CDATA[\nvar vl"
|
||||||
|
index
|
||||||
|
" = "
|
||||||
|
(yaml->json (str "$schema: https://vega.github.io/schema/vega-lite/v2.json\n" vega-src))
|
||||||
|
";\nvega.embed('#vis"
|
||||||
|
index
|
||||||
|
"', vl"
|
||||||
|
index
|
||||||
|
");\n//]]\n</script>"))
|
||||||
|
|
||||||
|
### Configuring Smeagol to use your formatter
|
||||||
|
|
||||||
|
To add your own formatter, compile it into a jar file which is on the classpath - it does *not* have to be part of the Smeagol project directly - and then edit the value of the key `:formatters` in the file `config.edn`; whose standard definition is:
|
||||||
|
|
||||||
|
:formatters {"vega" smeagol.formatting/process-vega
|
||||||
|
"vis" smeagol.formatting/process-vega
|
||||||
|
"mermaid" smeagol.formatting/process-mermaid}
|
||||||
|
|
||||||
|
The added key should be the word which will follow the opening three backticks of your code block, and the value of that key should be a symbol which evaluates to the function you have written. So suppose your formatter was called `my-magic-formatter`; you'd written it in a namespace called `magic.core`; and you wanted users to identify it with the word `magic`, you'd add the following to the `:formatters` map:
|
||||||
|
|
||||||
|
"magic" magic.core/my-magic-formatter
|
||||||
|
|
||||||
|
Users could then put a section in their markdown text:
|
||||||
|
|
||||||
|
```backticks magic
|
||||||
|
wingardium leviosa
|
||||||
|
```
|
||||||
|
|
||||||
|
and your function would be called with "wingardium leviosa" as the first argument.
|
||||||
|
|
@ -1,71 +1,41 @@
|
||||||
|

|
||||||
|
|
||||||
# Welcome to Smeagol!
|
# Welcome to Smeagol!
|
||||||
Smeagol is a simple Wiki engine inspired by [Gollum](https://github.com/gollum/gollum/wiki). Gollum is a Wiki engine written in Ruby, which uses a number of simple text formats including [Markdown](http://daringfireball.net/projects/markdown/), and which uses [Git](http://git-scm.com/) to provide versioning and backup. I needed a new Wiki for a project and thought Gollum would be ideal - but unfortunately it doesn't provide user authentication, which I needed, and it was simpler for me to reimplement the bits I did need in Clojure than to modify Gollum.
|
Smeagol is a simple Wiki engine inspired by [Gollum](https://github.com/gollum/gollum/wiki). Gollum is a Wiki engine written in Ruby, which uses a number of simple text formats including [Markdown](http://daringfireball.net/projects/markdown/), and which uses [Git](http://git-scm.com/) to provide versioning and backup. I needed a new Wiki for a project and thought Gollum would be ideal - but unfortunately it doesn't provide user authentication, which I needed, and it was simpler for me to reimplement the bits I did need in Clojure than to modify Gollum.
|
||||||
|
|
||||||
So at this stage Smeagol is a Wiki engine written in Clojure which uses Markdown as its text format, which does have user authentication, and which uses Git as its versioning and backup system.
|
So at this stage Smeagol is a Wiki engine written in Clojure which uses Markdown as its text format, which does have user authentication, and which uses Git as its versioning and backup system.
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
Smeagol is now a fully working small Wiki engine, and meets my own immediate needs. There are some obvious
|
Smeagol is now a fully working small Wiki engine, and meets my own immediate needs.
|
||||||
things which could be improved - see **TODO** list below - but it works now and doesn't seem to have any major problems.
|
|
||||||
|
## Using Smeagol
|
||||||
|
Read the [[User Documentation]] for an introduction to all Smeagol's features.
|
||||||
|
|
||||||
## 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. Here's an example [[Internal Link]].
|
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. Read more about [[Extensible Markup]].
|
||||||
|
|
||||||
## 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:
|
Smeagol now has good security and authentication. While the initial password supplied with the system is not encrypted, when it is changed it will be; and passwords for new users added through the user administration pages are encrypted. Read more about [[Security and authentication]].
|
||||||
|
|
||||||
{:admin {:password "admin" :email "admin@localhost" :admin true}
|
|
||||||
: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.
|
|
||||||
|
|
||||||
## 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:
|
You can (if you're logged in) upload files, including images, using the **Upload a file** link on the top menu bar. You can link to an uploaded image, or to other images already available on the web, like this:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Todo
|
## Running Smeagol
|
||||||
* Mechanism to add users through the user interface;
|
You can run Smeagol from the [[Docker Image]]; alternatively you can run it from an executable jar file or as a war file in a servlet container. Read how in [[Deploying Smeagol]].
|
||||||
|
|
||||||
## Advertisement
|
## Developing Smeagol
|
||||||
If you like what you see here, I am available for work on open source Clojure projects. Contact me vis [WEFT](http://www.weft.scot/).
|
Smeagol is an open source project; you're entitled to make changes yourself. Read more about [[Developing Smeagol]].
|
||||||
|
|
||||||
### Phoning home
|
|
||||||
Smeagol currently requests the WEFT logo in the page footer from my home site. This is mainly so I can get a feel for how many people are using the product. If you object to this, edit the file
|
|
||||||
|
|
||||||
resources/templates/base.html
|
|
||||||
|
|
||||||
and replace the line
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
with the line
|
|
||||||
|
|
||||||
<img height="16" width="16" alt="The Web Engineering Factory & Toolworks" src="img/weft.logo.64.png"> Developed by <a href="http://www.weft.scot/">WEFT</a>
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
Copyright © 2014-2015 Simon Brooke. Licensed under the GNU General Public License,
|
Copyright © 2014-2017 Simon Brooke. Licensed under the GNU General Public License,
|
||||||
version 2.0 or (at your option) any later version. If you wish to incorporate
|
version 2.0 or (at your option) any later version. If you wish to incorporate
|
||||||
parts of Smeagol into another open source project which uses a less restrictive
|
parts of Smeagol into another open source project which uses a less restrictive
|
||||||
license, please contact me; I'm open to dual licensing it.
|
license, please contact me; I'm open to dual licensing it.
|
||||||
|
|
||||||
## Prerequisites
|
## Phoning home
|
||||||
You will need [Leiningen](https://github.com/technomancy/leiningen) 2.0 or above installed.
|
Smeagol does currently fetch one image from my home site. Read more about [[Phoning Home]], and how to prevent it (if you want to).
|
||||||
|
|
||||||
You will need [node](https://nodejs.org/en/) and [bower](https://bower.io/) installed.
|
## Advertisement
|
||||||
|
If you like what you see here, I am available for work on open source Clojure projects.
|
||||||
## Running
|
|
||||||
To start a web server for the application, run:
|
|
||||||
|
|
||||||
lein bower install
|
|
||||||
lein ring server
|
|
||||||
|
|
||||||
Alternatively, if you want to deploy to a servlet container (which I would strongly recommend), the simplest thing is to run:
|
|
||||||
|
|
||||||
lein bower install
|
|
||||||
lein ring uberwar
|
|
||||||
|
|
||||||
(a command which I'm sure Smeagol would entirely appreciate) and deploy the resulting war file.
|
|
||||||
|
|
||||||
|
|
||||||
## Editing the framing content
|
|
||||||
You can edit the [stylesheet](/edit-css?page=stylesheet), the [[\_left-bar]], the [[\_edit-left-bar]], and the [[\_header]].
|
|
||||||
|
|
|
||||||
11
resources/public/content/Phoning Home.md
Normal file
11
resources/public/content/Phoning Home.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
Smeagol currently requests the WEFT logo in the page footer from my home site. This is mainly so I can get a feel for how many people are using the product. If you object to this, edit the file
|
||||||
|
|
||||||
|
resources/templates/base.html
|
||||||
|
|
||||||
|
and replace the line
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
with the line
|
||||||
|
|
||||||
|
<img height="16" width="16" alt="The Web Engineering Factory & Toolworks" src="img/weft.logo.64.png"> Developed by <a href="http://www.weft.scot/">WEFT</a>
|
||||||
18
resources/public/content/Security and authentication.md
Normal file
18
resources/public/content/Security and authentication.md
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
Security is now greatly improved over earlier releases of Smeagol. There is a file called `passwd` which by default is in the `resources` directory, which contains a [`Clojure` map](https://clojure.org/reference/data_structures#Maps) which maps usernames to maps with plain-text passwords and emails thus:
|
||||||
|
|
||||||
|
{:admin {:password "admin" :email "admin@localhost" :admin true}
|
||||||
|
: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.
|
||||||
|
|
||||||
|
## Fields in the user record
|
||||||
|
Keys and their associated values in the individual user's record are as follows:
|
||||||
|
|
||||||
|
* `:password` The user's password, which can be plain text (if set via the user interface, an encrypted password is stored)
|
||||||
|
* `:email` The user's email address (not currently used; may be used in future for sending password reset messages)
|
||||||
|
* `:admin` If present and set to `true`, the user has access to the user administration functions.
|
||||||
|
|
||||||
|
## Maintaining the passwd file outside the Smeagol deployment
|
||||||
|
You may set an environment variable, `SMEAGOL_PASSWD`, to indicate a `passwd` file anywhere you like on the file system provided the process running Smeagol can read it; but unless the file is writable by the process which Smeagol runs as you will not be able to administer users through the user interface.
|
||||||
|
|
||||||
|
Of course, it is possible to edit the file using a text editor and maintain the list of allowed users in that way.
|
||||||
124
resources/public/content/User Documentation.md
Normal file
124
resources/public/content/User Documentation.md
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
## If you're using a small device
|
||||||
|
If you're using a small device, like a mobile phone, the top menu isn't usually displayed. Instead there will be an image like this:
|
||||||
|

|
||||||
|
|
||||||
|
at the top left of the page. Touching this image will cause the top menu to be displayed, and it will have all the options described in this documentation.
|
||||||
|
|
||||||
|
## Logging in
|
||||||
|
If you are not logged in, there will be an option `Log in` on the top menu. Note that if you're not an English language speaker, the menu items may be in your own language (provided we have a suitable language file). You must log in to edit pages, to change your password, or to perform administration.
|
||||||
|
|
||||||
|
Selecting the 'Log in' option will take you to the log in page. This will prompt you for your username and password. If you have just set up a new instance of Smeagol, your username will be `admin` and your password will be `admin`. **Note** It is very important to change this default password after logging in.
|
||||||
|
|
||||||
|
+-------------------------------------------------------------------------+
|
||||||
|
| +----------------------------------+ |
|
||||||
|
| Your username: | | |
|
||||||
|
| +----------------------------------+ |
|
||||||
|
| Your password: | | |
|
||||||
|
| +----------------------------------+ |
|
||||||
|
| +-----------+ |
|
||||||
|
| To edit this wiki | Log in! | |
|
||||||
|
| +-----------+ |
|
||||||
|
+-------------------------------------------------------------------------+
|
||||||
|
|
||||||
|
Once you have entered your username and password, select the green `Log in!` button, or press `return` on your keyboard.
|
||||||
|
|
||||||
|
### Changing your password
|
||||||
|
To change your password, select the `Change password` option from the top menu. This will take you to the `Change password for...` page. This will prompt you for your (old) password, and your new password, twice. Complete the form and select the green `Change password!` button, or hit return.
|
||||||
|
|
||||||
|
+-------------------------------------------------------------------------+
|
||||||
|
| +----------------------------------+ |
|
||||||
|
| Your password: | | |
|
||||||
|
| +----------------------------------+ |
|
||||||
|
| New password: | | |
|
||||||
|
| +----------------------------------+ |
|
||||||
|
| And again: | | |
|
||||||
|
| +----------------------------------+ |
|
||||||
|
| +--------------------+ |
|
||||||
|
| To change your password | Change password! | |
|
||||||
|
| +--------------------+ |
|
||||||
|
+-------------------------------------------------------------------------+
|
||||||
|
|
||||||
|
If there is a problem (for example, the password wasn't long enough, or your new passwords didn't match), a message will show in red below the header of the page to explain the problem. Complete the form again.
|
||||||
|
|
||||||
|
If the form was submitted successfully and your password is changed, a message will be shown in green below the header of the page confirming this.
|
||||||
|
|
||||||
|
## Viewing page history
|
||||||
|
You can view the edit history of any page in the Wiki. At the top right of the content area, you will find a link `History`. Selecting this will take you to a page listing all the edits that have been made. Each row shows you:
|
||||||
|
|
||||||
|
* When the change was made
|
||||||
|
* What was changed (and who changed it)
|
||||||
|
|
||||||
|
It also provides a link `Show Version`, and a link `What's changed since?`
|
||||||
|
|
||||||
|
### Viewing a specific version
|
||||||
|
Selecting the `Show Version` link from any version in the page history will open the version of the page exactly as it was immediately after that edit. Selecting the `What's changed since?` link will show a page which highlights all the changes made to the page since that edit, with added text shown in green and deleted text in red.
|
||||||
|
|
||||||
|
## Editing pages
|
||||||
|
If you are logged in you can edit any page in the Wiki. At the top right of the content area, you will find a link `Edit this page`. Selecting this opens the page in the editor.
|
||||||
|
|
||||||
|
The editor is not strictly 'what you see is what you get', but it's fairly close to it. Icons on the ribbon above the content area allow you to apply simple styling. There are some prompts in the sidebar which will help with more complicate things.
|
||||||
|
|
||||||
|
### 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 also supports [[Extensible Markup]].
|
||||||
|
|
||||||
|
## Uploading files
|
||||||
|
To upload a file (including an image file), select the link `Upload a file` from the top menu. **Warning:** do not do this while you are editing a page, or you will lose your edit!
|
||||||
|
|
||||||
|
Selecting the link will take you to the `Upload a file` page. This will prompt you for the file you wish to upload. Select your file, and then select the green `Save!` button.
|
||||||
|
|
||||||
|
After your file has uploaded, you will be shown a link which can be copied and pasted into a Wiki page to link to that file.
|
||||||
|
|
||||||
|
You must be logged in to upload files.
|
||||||
|
|
||||||
|
## Administering users
|
||||||
|
|
||||||
|
If you are an administrator, you can administer the users who are entitled to edit and administer the Wiki. When you are logged in, there will be a a link `Edit users` on the top menu. Selecting this will take you to the `Edit users` page which lists users, with an `Edit...` and a `Delete...` link for each. Below the existing users will be a link `Add new user`.
|
||||||
|
|
||||||
|
### Editing a user
|
||||||
|
|
||||||
|
Selecting the `Edit...` link for any user from the `Edit users` page, or the `Add new user` link from the same page, will take you to the `Edit user` form.
|
||||||
|
|
||||||
|
This page has the following inputs:
|
||||||
|
|
||||||
|
+-------------------------------------------------------------------------+
|
||||||
|
| +----------------------------------+ |
|
||||||
|
| Username: | | |
|
||||||
|
| +----------------------------------+ |
|
||||||
|
| New password: | | |
|
||||||
|
| +----------------------------------+ |
|
||||||
|
| And again: | | |
|
||||||
|
| +----------------------------------+ |
|
||||||
|
| Email address: | | |
|
||||||
|
| +----------------------------------+ |
|
||||||
|
| Is Administrator? [ ] |
|
||||||
|
| +---------+ |
|
||||||
|
| When you have finished editing | Save! | |
|
||||||
|
| +---------+ |
|
||||||
|
+-------------------------------------------------------------------------+
|
||||||
|
|
||||||
|
#### To add a new user
|
||||||
|
|
||||||
|
When using this form after selecting the `Add new user` link, all fields will be blank. Complete at least the fields `Username`, `New password`, `And again`, and `Email address`. If the new user is to be an administrator, check the box labelled `Is Administrator`. Finally, select the green button marked `Save!`. Your new user will be saved.
|
||||||
|
|
||||||
|
#### To edit an existing user
|
||||||
|
|
||||||
|
When using this form after selecting the `Edit...` link against a particular user, the `Username` field will already be filled in (and you won't be able to edit it). The `Email address` field will also probably be filled in. If the user is an administrator, the `Is Administrator` box will be checked. The `New password` and `And again` fields will be blank. You may alter the email address or change the `Is Administrator` status.
|
||||||
|
|
||||||
|
#### To change the password of an existing user
|
||||||
|
|
||||||
|
Smeagol does not have a mechanism to allow users to reset their own password if they have forgotten it. Instead, they will have to ask an administrator to do this.
|
||||||
|
|
||||||
|
On the `Edit user` page for the existing user, enter their new password in the fields `New password` and `And again`; then select the green button marked `Save!`. **Warning** If you do not want to change the password, leave these fields blank!
|
||||||
|
|
||||||
|
## To log out
|
||||||
|
|
||||||
|
When you've finished editing the Wiki, you should log out. Select the `Log out` link from the top menu. This will take you to a very simple form:
|
||||||
|
|
||||||
|
+-------------------------------------------------------------------------+
|
||||||
|
| +------------+ |
|
||||||
|
| When you have finished editing | Log out! | |
|
||||||
|
| +------------+ |
|
||||||
|
+-------------------------------------------------------------------------+
|
||||||
|
|
||||||
|
Select the red `Log out!` button to log out.
|
||||||
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
This is the header. There isn't yet much in it. You could [edit](edit?page=_header) it to provide internal navigation or branding.
|
[[Introduction]] | [[User Documentation]] | [[Deploying Smeagol]] | [[Developing Smeagol]] ||
|
||||||
|
This is the header. You could [edit](edit?page=_header) it to provide internal navigation or branding.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,10 @@
|
||||||
This is the side bar. There's nothing in it yet. You could [edit](edit?page=_side-bar) it to provide internal navigation or branding.
|
* [[Introduction]]
|
||||||
|
* [[User Documentation]]
|
||||||
|
* [[Deploying Smeagol]]
|
||||||
|
* [[Developing Smeagol]]
|
||||||
|
|
||||||
If you don't like it on the left, float it to the right (or do something entirely different) by editing the [stylesheet](/edit-css?page=stylesheet).
|
<hr/>
|
||||||
|
|
||||||
|
This is the side bar. You could [edit](edit?page=_side-bar) it to provide internal navigation or branding.
|
||||||
|
|
||||||
|
If you don't like it on the left, float it to the right (or do something entirely different) by editing the [stylesheet](edit-css?page=stylesheet).
|
||||||
|
|
|
||||||
|
|
@ -34,14 +34,22 @@ del {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.content, form, p, pre, h1, h2, h3, h4, h5 {
|
div.content, div.error, div.message, form, p, pre, h1, h2, h3, h4, h5 {
|
||||||
padding: 0.25em 5%;
|
padding: 0.1em 5% 0 5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
dl, menu, ol, table, ul {
|
dl, menu, ol, table, ul {
|
||||||
margin: 0.25em 5%;
|
margin: 0.25em 5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
/* 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;
|
||||||
|
|
@ -74,6 +82,7 @@ form {
|
||||||
header {
|
header {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
width:100%;
|
width:100%;
|
||||||
|
max-width: 100%;
|
||||||
background-color: gray;
|
background-color: gray;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
@ -82,8 +91,19 @@ header h1 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
header img {
|
header a {
|
||||||
float: right;
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
header a:hover {
|
||||||
|
color: darkgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
|
|
@ -115,14 +135,6 @@ label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
menu li {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
menu li::before {
|
|
||||||
content: "|| ";
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
table {
|
||||||
border: 2px solid black;
|
border: 2px solid black;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
|
@ -147,58 +159,6 @@ th {
|
||||||
## ids generally in document order
|
## ids generally in document order
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* top-of-page navigation, not editable, provided by Smeagol */
|
|
||||||
#nav{
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
_position: absolute;
|
|
||||||
_top: expression(document.documentElement.scrollTop);
|
|
||||||
z-index: 149;
|
|
||||||
background:rgba(40,40,40,0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
#user {
|
|
||||||
font-height: 66%;
|
|
||||||
float: right;
|
|
||||||
padding: 0.1em 0.75em;
|
|
||||||
margin: 0;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#user a {
|
|
||||||
color: silver;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* only needed for fly-out menu effect on tablet and phone stylesheets */
|
|
||||||
#nav-icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav-menu {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav ul li {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav ul li a {
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 0.1em 0.75em;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav ul li.active a { background: silver;}
|
|
||||||
li.nav-item a:hover { background: rgb( 240, 240, 240) }
|
|
||||||
li.nav-item a:active { background: gray; color: white; }
|
|
||||||
|
|
||||||
/* Overall container div, holds all content of page. Yes, I know it shouldn't have fixed width */
|
/* Overall container div, holds all content of page. Yes, I know it shouldn't have fixed width */
|
||||||
#main-container{
|
#main-container{
|
||||||
}
|
}
|
||||||
|
|
@ -211,33 +171,25 @@ li.nav-item a:active { background: gray; color: white; }
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* content of the current in the Wiki - editable, provided by users. Within main-container */
|
|
||||||
#content {
|
|
||||||
border: thin solid silver;
|
|
||||||
width: 80%;
|
|
||||||
float: right;
|
|
||||||
padding-bottom: 5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* cookies information box, fixed, in right margin, just above footer */
|
/* cookies information box, fixed, in right margin, just above footer */
|
||||||
#cookies {
|
#cookies {
|
||||||
width: 30%;
|
width: 30%;
|
||||||
float: right;
|
float: right;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 1.5em;
|
bottom: 3.5em;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 150;
|
z-index: 175;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* about-cookies box: permanently visible part of cookies information box */
|
/* about-cookies box: permanently visible part of cookies information box */
|
||||||
#about-cookies {
|
#about-cookies {
|
||||||
clear: right;
|
clear: right;
|
||||||
width: 10em;
|
|
||||||
font-size: 66%;
|
font-size: 66%;
|
||||||
float: right;
|
float: right;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
padding: 0.25em 2em;
|
padding: 0.25em 2em;
|
||||||
|
border-radius: 0.25em;
|
||||||
color: white;
|
color: white;
|
||||||
background:rgba(40,40,40,0.8);
|
background:rgba(40,40,40,0.8);
|
||||||
}
|
}
|
||||||
|
|
@ -245,7 +197,8 @@ li.nav-item a:active { background: gray; color: white; }
|
||||||
/* more-about-cookies box, normally hidden */
|
/* more-about-cookies box, normally hidden */
|
||||||
#more-about-cookies {
|
#more-about-cookies {
|
||||||
display: none;
|
display: none;
|
||||||
padding: 0.25em 2em;
|
padding: 0.5em 2em;
|
||||||
|
border-radius: 0.5em;
|
||||||
color: white;
|
color: white;
|
||||||
background:rgba(40,40,40,0.8);
|
background:rgba(40,40,40,0.8);
|
||||||
border-bottom: thin solid white;
|
border-bottom: thin solid white;
|
||||||
|
|
@ -256,11 +209,20 @@ li.nav-item a:active { background: gray; color: white; }
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#uploaded-image {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
.change {
|
.change {
|
||||||
background-color: rgb( 223, 223, 223);
|
background-color: rgb( 223, 223, 223);
|
||||||
border: thin solid silver;
|
border: thin solid silver;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.data-visualisation {
|
||||||
|
padding: 0.25em 5%;
|
||||||
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: red;
|
background-color: red;
|
||||||
|
|
@ -277,7 +239,9 @@ li.nav-item a:active { background: gray; color: white; }
|
||||||
.minor-controls {
|
.minor-controls {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
float: right;
|
float: right;
|
||||||
|
right: 0;
|
||||||
padding: 0.25em 2em;
|
padding: 0.25em 2em;
|
||||||
|
border-radius: 0.25em;
|
||||||
color: white;
|
color: white;
|
||||||
background:rgba(40,40,40,0.8);
|
background:rgba(40,40,40,0.8);
|
||||||
font-size: 66%;
|
font-size: 66%;
|
||||||
|
|
@ -293,6 +257,23 @@ li.nav-item a:active { background: gray; color: white; }
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pseudo-input {
|
||||||
|
border: inset thin;
|
||||||
|
background-color: white;
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 7.5em;
|
||||||
|
padding: 0 2em 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vega-bindings, .vega-actions {
|
||||||
|
font-size: 66%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add space between Vega-Embed links */
|
||||||
|
.vega-actions a {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.warn {
|
.warn {
|
||||||
color: maroon;
|
color: maroon;
|
||||||
}
|
}
|
||||||
|
|
@ -309,3 +290,212 @@ li.nav-item a:active { background: gray; color: white; }
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* desktops and laptops, primarily. Adapted to mouse; targets may be small */
|
||||||
|
@media all and (min-device-width: 1025px) {
|
||||||
|
/* content of the current page in the Wiki - editable, provided by users. Within main-container */
|
||||||
|
#content {
|
||||||
|
border: thin solid silver;
|
||||||
|
width: 80%;
|
||||||
|
float: right;
|
||||||
|
padding-bottom: 5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#phone-side-bar, #phone-credits {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* top-of-page navigation, not editable, provided by Smeagol */
|
||||||
|
#nav{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
_position: absolute;
|
||||||
|
_top: expression(document.documentElement.scrollTop);
|
||||||
|
z-index: 149;
|
||||||
|
background:rgba(40,40,40,0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* only needed for fly-out menu effect on tablet and phone stylesheets */
|
||||||
|
#nav-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-menu {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav menu li {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav menu li a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0.1em 0.75em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav menu li.active a { background: gray;}
|
||||||
|
li.nav-item a:hover { background: rgb( 240, 240, 240) }
|
||||||
|
li.nav-item a:active { background: gray; color: white; }
|
||||||
|
|
||||||
|
#nav menu li#user {
|
||||||
|
padding: 0 1em;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tablets, primarily. Adapted to touch; targets are larger */
|
||||||
|
@media all and (min-device-width: 769px) and (max-device-width: 1024px) {
|
||||||
|
h1 {
|
||||||
|
/* I wouldn't normally use a px value, but the menu icon is 49px wide */
|
||||||
|
padding: 0.25em 5%;
|
||||||
|
padding-left: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* content of the current page in the Wiki - editable, provided by users. Within main-container */
|
||||||
|
#content {
|
||||||
|
border: thin solid silver;
|
||||||
|
width: 80%;
|
||||||
|
float: right;
|
||||||
|
padding-bottom: 5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 149;
|
||||||
|
color: silver;
|
||||||
|
background:rgba(40,40,40,0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav:hover #nav-menu {
|
||||||
|
display: block;
|
||||||
|
list-style-type: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-icon {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-menu, #phone-side-bar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav menu li {
|
||||||
|
padding: 0.5em;
|
||||||
|
margin: 0.5 em;
|
||||||
|
font-size: 150%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav menu li a {
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav ul li.active a { background: silver;}
|
||||||
|
li.nav-item a:hover { background: rgb( 240, 240, 240) }
|
||||||
|
li.nav-item a:active { background: gray; color: white; }
|
||||||
|
|
||||||
|
#nav menu #user {
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* phones, and, indeed, smaller phones. Adapted to touch; display radically
|
||||||
|
* decluttered */
|
||||||
|
@media all and (max-device-width: 768px) {
|
||||||
|
footer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
/* I wouldn't normally use a px value, but the menu icon is 49px wide */
|
||||||
|
padding: 0.25em 5%;
|
||||||
|
padding-left: 100px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* content of the current in the Wiki - editable, provided by users. Within main-container */
|
||||||
|
#content {
|
||||||
|
border: thin solid silver;
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-container {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#cookies {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 149;
|
||||||
|
color: silver;
|
||||||
|
background:rgba(40,40,40,0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav:hover #nav-menu, #nav:hover #phone-side-bar {
|
||||||
|
display: block;
|
||||||
|
list-style-type: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-icon {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-menu, #phone-side-bar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav menu li {
|
||||||
|
padding: 0.5em;
|
||||||
|
margin: 0.5 em;
|
||||||
|
font-size: 150%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav menu li a {
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav ul li.active a { background: silver;}
|
||||||
|
li.nav-item a:hover { background: rgb( 240, 240, 240) }
|
||||||
|
li.nav-item a:active { background: gray; color: white; }
|
||||||
|
|
||||||
|
#nav menu #user {
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#side-bar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
BIN
resources/public/content/uploads/smeagol.png
Normal file
BIN
resources/public/content/uploads/smeagol.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -42,13 +42,13 @@ body {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#nav ul li {
|
#nav menu li {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
#nav ul li a {
|
#nav menu li a {
|
||||||
color: white;
|
color: white;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
|
||||||
158
resources/public/data/london.csv
Normal file
158
resources/public/data/london.csv
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
"source","dest","airline","flights","onTimePerf","delayAverage","year"
|
||||||
|
"LHR","ORD","AA",2490,66.33,21.11,2010
|
||||||
|
"LHR","ORD","BA",1413,57.63,23.3,2010
|
||||||
|
"LHR","ORD","UA",2105,73.24,14.57,2010
|
||||||
|
"LHR","ORD","VS",218,77.06,11.1,2010
|
||||||
|
"LHR","LAX","AA",706,66.38,17.66,2010
|
||||||
|
"LHR","LAX","BA",1914,50.18,31.01,2010
|
||||||
|
"LHR","LAX","UA",698,81.81,13.62,2010
|
||||||
|
"LHR","LAX","VS",1285,56.86,21.92,2010
|
||||||
|
"LHR","JFK","AA",3205,67.36,20.31,2010
|
||||||
|
"LHR","JFK","BA",4181,66.59,20.84,2010
|
||||||
|
"LHR","JFK","DL",1611,62.12,23.74,2010
|
||||||
|
"LHR","JFK","KU",305,46.56,38.75,2010
|
||||||
|
"LHR","JFK","VS",2067,59.62,23.69,2010
|
||||||
|
"LHR","EWR","BA",1828,70.25,17.67,2010
|
||||||
|
"LHR","EWR","CO",2697,69.28,18.87,2010
|
||||||
|
"LHR","EWR","VS",1399,65.62,23.15,2010
|
||||||
|
"LHR","IAD","BA",2011,62.11,21.71,2010
|
||||||
|
"LHR","IAD","UA",2094,74.67,14.11,2010
|
||||||
|
"LHR","IAD","VS",700,66.24,19.75,2010
|
||||||
|
"LCY","JFK","BA",1012,89.12,5.67,2010
|
||||||
|
"LHR","ORD","AA",2532,76.87,13.34,2011
|
||||||
|
"LHR","ORD","BA",1440,76.32,14.42,2011
|
||||||
|
"LHR","ORD","UA",2106,79.36,15.2,2011
|
||||||
|
"LHR","ORD","VS",302,82.72,8.66,2011
|
||||||
|
"LHR","LAX","AA",730,75.48,13.66,2011
|
||||||
|
"LHR","LAX","BA",1984,63.46,18.47,2011
|
||||||
|
"LHR","LAX","UA",698,83.52,11.7,2011
|
||||||
|
"LHR","LAX","VS",1397,79,11.2,2011
|
||||||
|
"LHR","JFK","AA",2917,75.8,14.77,2011
|
||||||
|
"LHR","JFK","BA",4944,73.56,14.65,2011
|
||||||
|
"LHR","JFK","DL",2144,80.91,13.05,2011
|
||||||
|
"LHR","JFK","KU",302,52.65,30.02,2011
|
||||||
|
"LHR","JFK","VS",2104,76.32,15.09,2011
|
||||||
|
"LHR","EWR","BA",2129,80.7,11.24,2011
|
||||||
|
"LHR","EWR","CO",3503,78.42,14.19,2011
|
||||||
|
"LHR","EWR","VS",1410,77.52,13.39,2011
|
||||||
|
"LHR","IAD","BA",2157,74.08,14.11,2011
|
||||||
|
"LHR","IAD","UA",2516,80.79,14,2011
|
||||||
|
"LHR","IAD","VS",711,82.42,9.73,2011
|
||||||
|
"LCY","JFK","BA",1031,92.05,4.12,2011
|
||||||
|
"LHR","ORD","AA",2523,72.07,20.03,2012
|
||||||
|
"LHR","ORD","BA",1445,70.03,17.73,2012
|
||||||
|
"LHR","ORD","UA",2102,76.08,15.35,2012
|
||||||
|
"LHR","ORD","VS",308,83.77,8.31,2012
|
||||||
|
"LHR","LAX","AA",714,75.35,12.03,2012
|
||||||
|
"LHR","LAX","BA",1874,63.48,18.34,2012
|
||||||
|
"LHR","LAX","UA",693,76.16,14.88,2012
|
||||||
|
"LHR","LAX","VS",1324,76.13,14.08,2012
|
||||||
|
"LHR","JFK","AA",3367,75.72,12.75,2012
|
||||||
|
"LHR","JFK","BA",5129,69.28,17.68,2012
|
||||||
|
"LHR","JFK","DL",2151,77.63,13.59,2012
|
||||||
|
"LHR","JFK","KU",307,52.12,32.02,2012
|
||||||
|
"LHR","JFK","VS",2227,81.9,11.32,2012
|
||||||
|
"LHR","EWR","BA",1920,73.02,15.88,2012
|
||||||
|
"LHR","EWR","CO",612,84.48,9.24,2012
|
||||||
|
"LHR","EWR","UA",2872,69.76,17.91,2012
|
||||||
|
"LHR","EWR","VS",1421,76.14,16.68,2012
|
||||||
|
"LHR","IAD","BA",2014,71.45,16.23,2012
|
||||||
|
"LHR","IAD","UA",2444,69.04,18.98,2012
|
||||||
|
"LHR","IAD","VS",703,87.06,7.71,2012
|
||||||
|
"LCY","JFK","BA",474,91.33,3.74,2012
|
||||||
|
"LCY","JFK","BA",537,89.37,4.71,2012
|
||||||
|
"LHR","ORD","AA",2513,74.69,15.29,2013
|
||||||
|
"LHR","ORD","BA",1440,65.35,20.84,2013
|
||||||
|
"LHR","ORD","UA",2110,78.39,14.13,2013
|
||||||
|
"LHR","ORD","VS",324,83.02,9.05,2013
|
||||||
|
"LHR","LAX","AA",728,76.48,12.56,2013
|
||||||
|
"LHR","LAX","BA",1864,59.5,27.43,2013
|
||||||
|
"LHR","LAX","UA",702,85.47,9.86,2013
|
||||||
|
"LHR","LAX","VS",1288,79,13.76,2013
|
||||||
|
"LHR","JFK","AA",2991,76.4,13.73,2013
|
||||||
|
"LHR","JFK","BA",5350,66.97,18.95,2013
|
||||||
|
"LHR","JFK","DL",2166,82.09,10.81,2013
|
||||||
|
"LHR","JFK","KU",306,62.42,21.53,2013
|
||||||
|
"LHR","JFK","VS",2825,81.91,10.53,2013
|
||||||
|
"LHR","EWR","BA",1838,71.44,16.72,2013
|
||||||
|
"LHR","EWR","UA",3494,80.59,12.36,2013
|
||||||
|
"LHR","EWR","VS",1419,81.66,11.7,2013
|
||||||
|
"LHR","IAD","BA",2012,71.02,18.67,2013
|
||||||
|
"LHR","IAD","UA",2496,80.39,12.82,2013
|
||||||
|
"LHR","IAD","VS",702,86.89,7.1,2013
|
||||||
|
"LCY","JFK","BA",945,90.64,4.58,2013
|
||||||
|
"LGW","LAX","DY",105,72.38,36.83,2014
|
||||||
|
"LGW","JFK","DY",156,51.92,27.79,2014
|
||||||
|
"LHR","ORD","AA",2476,73.5,22.09,2014
|
||||||
|
"LHR","ORD","BA",1444,65.28,18.7,2014
|
||||||
|
"LHR","ORD","UA",2110,77.56,13.68,2014
|
||||||
|
"LHR","ORD","VS",348,81.32,8.86,2014
|
||||||
|
"LHR","LAX","AA",728,76.79,18.56,2014
|
||||||
|
"LHR","LAX","BA",1450,56.84,23.1,2014
|
||||||
|
"LHR","LAX","DL",127,92.13,6.92,2014
|
||||||
|
"LHR","LAX","UA",698,85.1,8.53,2014
|
||||||
|
"LHR","LAX","VS",1217,80.99,12.58,2014
|
||||||
|
"LHR","JFK","AA",2287,78.18,14.74,2014
|
||||||
|
"LHR","JFK","BA",6018,71.29,16.59,2014
|
||||||
|
"LHR","JFK","DL",2123,82.13,11.19,2014
|
||||||
|
"LHR","JFK","KU",310,48.22,33.17,2014
|
||||||
|
"LHR","JFK","VS",2820,79.77,12.45,2014
|
||||||
|
"LHR","EWR","BA",1943,71.46,19.7,2014
|
||||||
|
"LHR","EWR","UA",3485,78.3,15.19,2014
|
||||||
|
"LHR","EWR","VS",1420,86.6,8.37,2014
|
||||||
|
"LHR","IAD","BA",1807,74.58,15.31,2014
|
||||||
|
"LHR","IAD","UA",2130,81.13,13.61,2014
|
||||||
|
"LHR","IAD","VS",695,89.06,6.9,2014
|
||||||
|
"LCY","JFK","BA",1003,91.23,4.43,2014
|
||||||
|
"LGW","LAX","DY",328,66.46,21.41,2015
|
||||||
|
"LGW","JFK","DY",543,64.09,31.78,2015
|
||||||
|
"LHR","ORD","AA",2062,72.09,27.59,2015
|
||||||
|
"LHR","ORD","BA",1442,71.4,16.24,2015
|
||||||
|
"LHR","ORD","UA",2099,83.94,10.37,2015
|
||||||
|
"LHR","ORD","VS",330,78.79,11.61,2015
|
||||||
|
"LHR","LAX","AA",1265,75.42,14.5,2015
|
||||||
|
"LHR","LAX","BA",1450,66.9,17.67,2015
|
||||||
|
"LHR","LAX","DL",540,79.44,10.97,2015
|
||||||
|
"LHR","LAX","UA",704,89.2,7.5,2015
|
||||||
|
"LHR","LAX","VS",1230,80.81,12.74,2015
|
||||||
|
"LHR","JFK","AA",2130,77.45,15.21,2015
|
||||||
|
"LHR","JFK","BA",5994,77.84,13.61,2015
|
||||||
|
"LHR","JFK","DL",2069,80.75,13.51,2015
|
||||||
|
"LHR","JFK","KU",310,59.22,32.67,2015
|
||||||
|
"LHR","JFK","VS",3363,76.35,13.82,2015
|
||||||
|
"LHR","EWR","BA",1459,76.47,14.03,2015
|
||||||
|
"LHR","EWR","DL",381,95.54,2.88,2015
|
||||||
|
"LHR","EWR","UA",3493,79.23,16.31,2015
|
||||||
|
"LHR","EWR","VS",871,81.75,10.7,2015
|
||||||
|
"LHR","IAD","BA",1421,76.89,12.03,2015
|
||||||
|
"LHR","IAD","UA",2126,81.74,14.02,2015
|
||||||
|
"LHR","IAD","VS",703,85.49,12.59,2015
|
||||||
|
"LCY","JFK","BA",937,92.74,4.52,2015
|
||||||
|
"LTN","EWR","DJT",316,81.53,18.18,2015
|
||||||
|
"LGW","LAX","DY",421,59.86,23.64,2016
|
||||||
|
"LGW","LAX","DI",38,65.79,14.39,2016
|
||||||
|
"LGW","JFK","BA",486,69.75,18.02,2016
|
||||||
|
"LGW","JFK","DY",689,65.31,28.18,2016
|
||||||
|
"LGW","JFK","DI",39,69.23,15.49,2016
|
||||||
|
"LHR","ORD","AA",2453,71.82,26.15,2016
|
||||||
|
"LHR","ORD","BA",1443,68.88,17.58,2016
|
||||||
|
"LHR","ORD","UA",2085,82.25,14.2,2016
|
||||||
|
"LHR","ORD","VS",333,89.79,5.07,2016
|
||||||
|
"LHR","LAX","AA",1411,68.25,19.46,2016
|
||||||
|
"LHR","LAX","BA",1452,54.34,24.89,2016
|
||||||
|
"LHR","LAX","UA",700,87,8.48,2016
|
||||||
|
"LHR","LAX","VS",1376,79.58,10.13,2016
|
||||||
|
"LHR","JFK","AA",2623,76.82,21.41,2016
|
||||||
|
"LHR","JFK","BA",5625,69.97,17.15,2016
|
||||||
|
"LHR","JFK","DL",2081,80.37,13.42,2016
|
||||||
|
"LHR","JFK","KU",253,19.76,71.96,2016
|
||||||
|
"LHR","JFK","VS",3489,79.67,11.44,2016
|
||||||
|
"LHR","EWR","AI",118,65.25,21.14,2016
|
||||||
|
"LHR","EWR","BA",1444,76.11,13.49,2016
|
||||||
|
"LHR","EWR","UA",3472,79.03,15.71,2016
|
||||||
|
"LHR","EWR","VS",722,82.27,9.48,2016
|
||||||
|
"LHR","IAD","BA",1415,71.59,17.44,2016
|
||||||
|
"LHR","IAD","UA",2134,82.05,13.24,2016
|
||||||
|
"LHR","IAD","VS",699,84.69,8.02,2016
|
||||||
|
"LCY","JFK","BA",921,90.01,5.26,2016
|
||||||
|
"LTN","EWR","DJT",333,87.05,8.44,2016
|
||||||
|
BIN
resources/public/data/london.png
Normal file
BIN
resources/public/data/london.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
BIN
resources/public/img/three-lines.png
Normal file
BIN
resources/public/img/three-lines.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 665 B |
88
resources/public/img/three-lines.svg
Normal file
88
resources/public/img/three-lines.svg
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="210mm"
|
||||||
|
height="297mm"
|
||||||
|
viewBox="0 0 744.09448819 1052.3622047"
|
||||||
|
id="svg2"
|
||||||
|
version="1.1"
|
||||||
|
inkscape:version="0.91 r13725"
|
||||||
|
sodipodi:docname="three-lines.svg">
|
||||||
|
<defs
|
||||||
|
id="defs4" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="0.7"
|
||||||
|
inkscape:cx="16.791691"
|
||||||
|
inkscape:cy="509.7116"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="996"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="28"
|
||||||
|
inkscape:window-maximized="1">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid4136" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<metadata
|
||||||
|
id="metadata7">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#060000;stroke-width:1.324;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.51906158"
|
||||||
|
id="rect4172"
|
||||||
|
width="580"
|
||||||
|
height="440"
|
||||||
|
x="40"
|
||||||
|
y="72.362206" />
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:#000000;fill-opacity:1;stroke:#060000;stroke-width:1.324;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.51906158"
|
||||||
|
id="rect4138"
|
||||||
|
width="540"
|
||||||
|
height="80"
|
||||||
|
x="60"
|
||||||
|
y="92.362206" />
|
||||||
|
<rect
|
||||||
|
y="252.36221"
|
||||||
|
x="60"
|
||||||
|
height="80"
|
||||||
|
width="540"
|
||||||
|
id="rect4149"
|
||||||
|
style="opacity:1;fill:#000000;fill-opacity:1;stroke:#060000;stroke-width:1.324;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.51906158" />
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:#000000;fill-opacity:1;stroke:#060000;stroke-width:1.324;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.51906158"
|
||||||
|
id="rect4151"
|
||||||
|
width="540"
|
||||||
|
height="80"
|
||||||
|
x="60"
|
||||||
|
y="412.36221" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
BIN
resources/public/img/three-lines.xcf
Normal file
BIN
resources/public/img/three-lines.xcf
Normal file
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 260 B |
BIN
resources/public/uploads/water.png
Normal file
BIN
resources/public/uploads/water.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -1,27 +1,28 @@
|
||||||
{% extends "templates/base.html" %}
|
{% extends "templates/base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="auth">
|
<div id="content" class="auth">
|
||||||
<form action="{{servlet-context}}/auth" method="POST">
|
<form action="{{servlet-context}}/auth" method="POST">
|
||||||
<input type="hidden" name="redirect-to" value="{{redirect-to}}"/>
|
{% csrf-field %}
|
||||||
{% if user %}
|
<input type="hidden" name="redirect-to" value="{{redirect-to}}"/>
|
||||||
<p class="widget">
|
{% if user %}
|
||||||
<label for="submit">To finish editing</label>
|
<p class="widget">
|
||||||
<input name="action" id="action" type="submit" class="action-dangerous" value="Logout!"/>
|
<label for="submit">{% i18n save-prompt %}</label>
|
||||||
</p>
|
<input name="action" id="action" type="submit" class="action-dangerous" value="{% i18n logout-label %}"/>
|
||||||
{% else %}
|
</p>
|
||||||
<p class="widget">
|
{% else %}
|
||||||
<label for="username">Your username</label>
|
<p class="widget">
|
||||||
<input name="username" id="username" type="text" required/>
|
<label for="username">{% i18n your-uname-prompt %}</label>
|
||||||
</p>
|
<input name="username" id="username" type="text" required/>
|
||||||
<p class="widget">
|
</p>
|
||||||
<label for="password">Your password</label>
|
<p class="widget">
|
||||||
<input name="password" id="password" type="password" required/>
|
<label for="password">{% i18n old-pass-prompt %}</label>
|
||||||
</p>
|
<input name="password" id="password" type="password" required/>
|
||||||
<p class="widget">
|
</p>
|
||||||
<label for="submit">To edit this wiki</label>
|
<p class="widget">
|
||||||
<input name="action" id="action" type="submit" class="action" value="Login!"/>
|
<label for="submit">{% i18n login-prompt %}</label>
|
||||||
</p>
|
<input name="action" id="action" type="submit" class="action" value="{% i18n login-label %}"/>
|
||||||
{% endif %}
|
</p>
|
||||||
</form>
|
{% endif %}
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>{{title}}</title>
|
<title>{% i18n site-title %}: {{title}}</title>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
<link href="{{servlet-context}}/content/stylesheet.css" media="screen and (min-device-width: 1025px)" rel="stylesheet" type="text/css" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<link href="{{servlet-context}}/css/print.css" media="print" rel="stylesheet" type="text/css" />
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
{% style "/content/stylesheet.css" %}
|
||||||
{% block extra-headers %}
|
{% block extra-headers %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
@ -12,24 +13,39 @@
|
||||||
<header class="wiki">
|
<header class="wiki">
|
||||||
<!-- navbar -->
|
<!-- navbar -->
|
||||||
<div id="nav">
|
<div id="nav">
|
||||||
{% if user %}
|
<img id="nav-icon" src="{{servlet-context}}/img/three-lines.png" alt="Menu"/>
|
||||||
<p class="user" id="user">You are logged in as {{user}} | <a href="passwd">change password</a></p>
|
<menu id="nav-menu">
|
||||||
{% endif %}
|
<li class="{{wiki-selected}}"><a href="{{servlet-context}}/">{% i18n home-link %}</a></li>
|
||||||
<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 %}
|
{% if admin %}
|
||||||
<li class="{{admin-selected}}"><a href="{{servlet-context}}/edit-users">Edit users</a></li>
|
<li class="{{edit-users-selected}}"><a href="{{servlet-context}}/edit-users">{% i18n edit-users-link %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if user %}
|
||||||
|
<li class="{{upload-selected}}"><a href="upload">{% i18n file-upload-title %}</a></li>
|
||||||
|
<li class="{{passwd-selected}}"><a href="passwd">{% i18n change-pass-link %}</a></li>
|
||||||
|
<li class="user" id="user">{% i18n logged-in-as}} {{user}}</li>
|
||||||
<li class="{{auth-selected}}"><a href="{{servlet-context}}/auth">
|
<li class="{{auth-selected}}"><a href="{{servlet-context}}/auth">
|
||||||
{% if user %}
|
{% i18n logout-link %}</a></li>
|
||||||
Log out
|
{% else %}
|
||||||
{% else %}
|
<li class="{{auth-selected}}"><a href="{{servlet-context}}/auth">
|
||||||
Log in
|
{% i18n login-link %}</a></li>
|
||||||
{% endif %}</a></li>
|
{% endif %}
|
||||||
</ul>
|
</menu>
|
||||||
|
<div id="phone-side-bar" class="wiki">
|
||||||
|
{{side-bar|safe}}
|
||||||
|
<div id="phone-credits">
|
||||||
|
<p>
|
||||||
|
<img height="16" width="16" alt="one wiki to rule them all"
|
||||||
|
src="img/smeagol.png"/>One Wiki to rule them all ||
|
||||||
|
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>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1>{{title}}</h1>
|
<h1>{% i18n site-title %}: {{title}}</h1>
|
||||||
{{header|safe}}
|
{{header|safe}}
|
||||||
{% if message %}
|
{% if message %}
|
||||||
<div id="message">
|
<div id="message">
|
||||||
|
|
@ -52,13 +68,10 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="cookies">
|
<div id="cookies">
|
||||||
<div id="more-about-cookies">
|
<div id="more-about-cookies">
|
||||||
This website stores session information as a 'cookie' on your browser. This helps us show you the content
|
{% i18n cookies-more %}
|
||||||
you want to see. This cookie does not identify you, and cannot be read by other websites. It is deleted by
|
|
||||||
your browser as soon as you leave this site. This website does not use any third party cookies, so your
|
|
||||||
visit here cannot be tracked by other websites.
|
|
||||||
</div>
|
</div>
|
||||||
<div id="about-cookies">
|
<div id="about-cookies">
|
||||||
About cookies
|
{% i18n cookies-about %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
|
|
@ -66,7 +79,7 @@
|
||||||
<div>
|
<div>
|
||||||
<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"/>One Wiki to rule them all ||
|
||||||
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="https://www.weft.scot/images/weft.logo.64.png"> Developed by <a href="http://www.weft.scot/">WEFT</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Built with <a href="http://www.luminusweb.net/">LuminusWeb</a> ||
|
Built with <a href="http://www.luminusweb.net/">LuminusWeb</a> ||
|
||||||
|
|
@ -78,4 +91,3 @@
|
||||||
<footer>
|
<footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,18 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="edit">
|
<div id="content" class="edit">
|
||||||
<form action="{{servlet-context}}/edit-css" method="POST">
|
<form action="{{servlet-context}}/edit-css" method="POST">
|
||||||
|
{% csrf-field %}
|
||||||
<input type="hidden" name="page" value="{{page}}"/>
|
<input type="hidden" name="page" value="{{page}}"/>
|
||||||
<textarea name="src" id="src" rows="25" cols="80">{{content}}</textarea>
|
<textarea name="src" id="src" rows="25" cols="80">{{content}}</textarea>
|
||||||
<p class="widget">
|
<p class="widget">
|
||||||
<label for="summary">What have you changed?</label>
|
<label for="summary">{% i18n what-changed-prompt %}</label>
|
||||||
<input name="summary" id="summary" type="text"
|
<input name="summary" id="summary" type="text"
|
||||||
value="{%if exists%}{%else%}New file {{title}}{%endif%}" required/>
|
value="{%if exists%}{%else%}New file {{title}}{%endif%}" required/>
|
||||||
</p>
|
</p>
|
||||||
<p class="widget">
|
<p class="widget">
|
||||||
<label for="submit">When you have finished editing</label>
|
<label for="submit">{% i18n save-prompt %}</label>
|
||||||
<input name="submit" id="submit" type="submit" class="action" value="Save!"/>
|
<input name="submit" id="submit" type="submit" class="action" value="{% i18n save-label %}"/>
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
|
||||||
var simplemde = new SimpleMDE();
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -3,29 +3,35 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="edit">
|
<div id="content" class="edit">
|
||||||
<form action="{{servlet-context}}/edit-user" method="POST">
|
<form action="{{servlet-context}}/edit-user" method="POST">
|
||||||
<p class="widget">
|
{% csrf-field %}
|
||||||
<label for="target">Username</label>
|
|
||||||
<input type="text" name="target" id="target" value="{{target}}" required/>
|
|
||||||
</p>
|
|
||||||
<p class="widget">
|
<p class="widget">
|
||||||
<label for="pass1">New password</label>
|
<label for="target">{% i18n username-prompt %}</label>
|
||||||
|
{% ifequal target "" %}
|
||||||
|
<input type="text" name="target" id="target" value="{{target}}" required/>
|
||||||
|
{% else %}
|
||||||
|
<span class="pseudo-input">{{target}}</span>
|
||||||
|
<input type="hidden" name="target" id="target" value="{{target}}" required/>
|
||||||
|
{% endifequal %}
|
||||||
|
</p>
|
||||||
|
<p class="widget">
|
||||||
|
<label for="pass1">{% i18n new-pass-prompt %}</label>
|
||||||
<input name="pass1" id="pass1" type="password"/>
|
<input name="pass1" id="pass1" type="password"/>
|
||||||
</p>
|
</p>
|
||||||
<p class="widget">
|
<p class="widget">
|
||||||
<label for="pass2">And again</label>
|
<label for="pass2">{% i18n rpt-pass-prompt %}</label>
|
||||||
<input name="pass2" id="pass2" type="password"/>
|
<input name="pass2" id="pass2" type="password"/>
|
||||||
</p>
|
</p>
|
||||||
<p class="widget">
|
<p class="widget">
|
||||||
<label for="email">Email address</label>
|
<label for="email">{% i18n email-prompt %}</label>
|
||||||
<input name="email" id="email" type="text" value="{{details.email}}" required/>
|
<input name="email" id="email" type="text" value="{{details.email}}" required/>
|
||||||
</p>
|
</p>
|
||||||
<p class="widget">
|
<p class="widget">
|
||||||
<label for="admin">Is administrator?</label>
|
<label for="admin">{% i18n is-admin-prompt %}</label>
|
||||||
<input name="admin" id="admin" type="checkbox" {% if details.admin %}checked{% endif %}/>
|
<input name="admin" id="admin" type="checkbox" {% if details.admin %}checked{% endif %}/>
|
||||||
</p>
|
</p>
|
||||||
<p class="widget">
|
<p class="widget">
|
||||||
<label for="submit">When you have finished editing</label>
|
<label for="submit">{% i18n save-prompt %}</label>
|
||||||
<input name="submit" id="submit" type="submit" class="action" value="Save!"/>
|
<input name="submit" id="submit" type="submit" class="action" value="{% i18n save-label}}"/>
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,18 @@
|
||||||
<div id="content">
|
<div id="content">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Edit</th><th>Delete</th>
|
<th/><th>{% i18n edit-col-hdr %}</th><th>{% i18n del-col-hdr %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="edit-user?target={{user}}">Edit {{user}}</a></td>
|
<td>{{user}}</td>
|
||||||
<td><a href="delete-user?target={{user}}">Delete {{user}}</a></td>
|
<td><a href="edit-user?target={{user}}">{% i18n edit-col-hdr %} {{user}}</a></td>
|
||||||
|
<td><a href="delete-user?target={{user}}">{% i18n del-col-hdr %} {{user}}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="edit-user">Add new user</a></td>
|
<td><a href="edit-user">{% i18n add-user-label %}</a></td>
|
||||||
|
<td></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,43 @@
|
||||||
{% extends "templates/base.html" %}
|
{% extends "templates/base.html" %}
|
||||||
{% block extra-headers %}
|
{% block extra-headers %}
|
||||||
<link rel="stylesheet" href="{{servlet-context}}/vendor/simplemde/dist/simplemde.min.css">
|
{% style "/vendor/simplemde/dist/simplemde.min.css" %}
|
||||||
<script src="{{servlet-context}}/vendor/simplemde/dist/simplemde.min.js"></script>
|
{% script "/vendor/simplemde/dist/simplemde.min.js" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="edit">
|
<div id="content" class="edit">
|
||||||
<form action="{{servlet-context}}/edit" method="POST">
|
<form action="{{servlet-context}}/edit" method="POST">
|
||||||
|
{% csrf-field %}
|
||||||
<input type="hidden" name="page" value="{{page}}"/>
|
<input type="hidden" name="page" value="{{page}}"/>
|
||||||
<textarea name="src" id="src" rows="25" cols="80">{{content}}</textarea>
|
<textarea name="src" id="src" rows="25" cols="80">{{content}}</textarea>
|
||||||
<p class="widget">
|
<p class="widget">
|
||||||
<label for="summary">What have you changed?</label>
|
<label for="summary">{% i18n what-changed-prompt %}</label>
|
||||||
<input name="summary" id="summary" type="text"
|
<input name="summary" id="summary" type="text"
|
||||||
value="{%if exists%}{%else%}New file {{title}}{%endif%}" required/>
|
value="{%if exists%}{%else%}New file {{title}}{%endif%}" required/>
|
||||||
</p>
|
</p>
|
||||||
<p class="widget">
|
<p class="widget">
|
||||||
<label for="submit">When you have finished editing</label>
|
<label for="submit">{% i18n save-prompt %}</label>
|
||||||
<input name="submit" id="submit" type="submit" class="action" value="Save!"/>
|
<input name="submit" id="submit" type="submit" class="action" value="{% i18n save-label %}"/>
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
var simplemde = new SimpleMDE();
|
var simplemde = new SimpleMDE({
|
||||||
|
autosave: {
|
||||||
|
enabled: true,
|
||||||
|
uniqueId: "Smeagol-{{page}}",
|
||||||
|
delay: 1000,
|
||||||
|
},
|
||||||
|
indentWithTabs: true,
|
||||||
|
insertTexts: {
|
||||||
|
horizontalRule: ["", "\n\n-----\n\n"],
|
||||||
|
image: [""],
|
||||||
|
link: ["[", "](http://)"],
|
||||||
|
table: ["", "\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n"],
|
||||||
|
},
|
||||||
|
showIcons: ["code"], //, "table"], - sadly, markdown-clj does not support tables
|
||||||
|
spellChecker: true,
|
||||||
|
status: ["autosave", "lines", "words", "cursor"]
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,29 @@
|
||||||
{% extends "templates/base.html" %}
|
{% extends "templates/base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="history">
|
<div id="content" class="history">
|
||||||
<table>
|
<table class="music-ruled">
|
||||||
<tr>
|
<tr>
|
||||||
<th>When</th><th>What</th><th>Version</th><th>Changes</th>
|
<th>{% i18n when-col-hdr %}</th>
|
||||||
</tr>
|
<th>{% i18n what-col-hdr %}</th>
|
||||||
<tr>
|
<th>{% i18n vers-col-hdr %}</th>
|
||||||
<td>Now</td><td></td>
|
<th>{% i18n change-col-hdr %}</th>
|
||||||
<td><a href="wiki?content={{page}}">[current]</a></td>
|
</tr>
|
||||||
<td>[no changes]</td>
|
{% for entry in history %}
|
||||||
<tr>
|
<tr>
|
||||||
{% for entry in history %}
|
<td>S
|
||||||
<tr>
|
{{entry.time}}
|
||||||
<td>S
|
</td>
|
||||||
{{entry.time}}
|
<td>
|
||||||
</td>
|
{{entry.message}}
|
||||||
<td>
|
</td>
|
||||||
{{entry.message}}
|
<td>
|
||||||
</td>
|
<a href="version?page={{page}}&version={{entry.id}}">Show version</a>
|
||||||
<td>
|
</td>
|
||||||
<a href="version?page={{page}}&version={{entry.id}}">Show version</a>
|
<td>
|
||||||
</td>
|
<a href="changes?page={{page}}&version={{entry.id}}">What's changed since?</a>
|
||||||
<td>
|
</td>
|
||||||
<a href="changes?page={{page}}&version={{entry.id}}">What's changed?</a>
|
</tr>
|
||||||
</td>
|
{% endfor %}
|
||||||
</tr>
|
</table>
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,22 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="auth">
|
<div id="content" class="auth">
|
||||||
<form action="{{servlet-context}}/passwd" method="POST">
|
<form action="{{servlet-context}}/passwd" method="POST">
|
||||||
|
{% csrf-field %}
|
||||||
<p class="widget">
|
<p class="widget">
|
||||||
<label for="oldpass">Your password</label>
|
<label for="oldpass">{% i18n old-pass-prompt %}</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="pass1">New password</label>
|
<label for="pass1">{% i18n new-pass-prompt %}</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="pass2">And again</label>
|
<label for="pass2">{% i18n rpt-pass-prompt %}</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">{% i18n change-pass-prompt %}</label>
|
||||||
<input name="action" id="action" type="submit" class="action" value="Change password!"/>
|
<input name="action" id="action" type="submit" class="action" value="{% i18n change-pass-link %}!"/>
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
38
resources/templates/upload.html
Normal file
38
resources/templates/upload.html
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
{% extends "templates/base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div id="content" class="auth">
|
||||||
|
{% if uploaded %}
|
||||||
|
{% if is-image %}
|
||||||
|
<p>
|
||||||
|
<img id="uploaded-image" alt="Uploaded image" src="content/uploads/{{uploaded}}"/>
|
||||||
|
|
||||||
|
{% i18n file-upload-link-text %}:
|
||||||
|
|
||||||
|
<code></code>
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p>
|
||||||
|
{% i18n file-upload-link-text %}:
|
||||||
|
|
||||||
|
<code>[Uploaded file](uploads/{{uploaded}})</code>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<form action="{{servlet-context}}/upload" enctype="multipart/form-data" method="POST">
|
||||||
|
{% csrf-field %}
|
||||||
|
<p class="widget">
|
||||||
|
<label for="upload">{% i18n file-upload-prompt %}</label>
|
||||||
|
<input name="upload" id="upload" type="file" required/>
|
||||||
|
</p>
|
||||||
|
<p class="widget">
|
||||||
|
<label for="summary">{% i18n file-summary-prompt %}</label>
|
||||||
|
<input name="summary" id="summary" type="text" required/>
|
||||||
|
</p>
|
||||||
|
<p class="widget">
|
||||||
|
<label for="submit">{% i18n save-prompt %}</label>
|
||||||
|
<input name="submit" id="submit" type="submit" class="action" value="{% i18n save-label %}"/>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -1,13 +1,35 @@
|
||||||
{% extends "templates/base.html" %}
|
{% extends "templates/base.html" %}
|
||||||
|
|
||||||
|
{% block extra-headers %}
|
||||||
|
{% style "vendor/mermaid/dist/mermaid.css" %}
|
||||||
|
<!-- there's at the time of writing (20170731) a problem with the dependencies of the Bower
|
||||||
|
package for vega-embed, so we're currently not installing either it or Vega locally.
|
||||||
|
TODO: fix -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/vega/3.0.0-rc2/vega.js"></script>
|
||||||
|
{% script "vendor/vega-lite/build/vega-lite.js" %}
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/vega-embed/3.0.0-beta.19/vega-embed.js"></script>
|
||||||
|
|
||||||
|
{% script "vendor/mermaid/dist/mermaid.js" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="wiki">
|
<div id="content" class="wiki">
|
||||||
{% if editable %}
|
{% 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}}">{% i18n edit-page-link %}</a></li>
|
||||||
<li><a href="history?page={{page}}">History</a></li>
|
<li><a href="history?page={{page}}">{% i18n history-link %}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{content|safe}}
|
{{content|safe}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//<![CDATA[
|
||||||
|
if (document.addEventListener) {
|
||||||
|
document.addEventListener("DOMContentLoaded", function(event) {
|
||||||
|
mermaid.initialize({startOnLoad:true});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//]]
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
(ns ^{:doc "Authentication functions."
|
(ns ^{:doc "Authentication functions."
|
||||||
:author "Simon Brooke"}
|
:author "Simon Brooke"}
|
||||||
smeagol.authenticate
|
smeagol.authenticate
|
||||||
(:require [taoensso.timbre :as timbre]
|
(:require [crypto.password.scrypt :as password]
|
||||||
|
[environ.core :refer [env]]
|
||||||
[noir.io :as io]
|
[noir.io :as io]
|
||||||
[crypto.password.scrypt :as password]))
|
[taoensso.timbre :as timbre]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;
|
;;;;
|
||||||
|
|
@ -34,7 +35,10 @@
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
;; the relative path to the password file.
|
;; the relative path to the password file.
|
||||||
(def password-file-path (str (io/resource-path) "../passwd"))
|
(def password-file-path
|
||||||
|
(or
|
||||||
|
(env :smeagol-passwd)
|
||||||
|
(str (clojure.java.io/resource "passwd"))))
|
||||||
|
|
||||||
|
|
||||||
(defn- get-users
|
(defn- get-users
|
||||||
|
|
@ -70,10 +74,14 @@
|
||||||
(let [user ((keyword username) (get-users))]
|
(let [user ((keyword username) (get-users))]
|
||||||
(:admin user))))
|
(:admin user))))
|
||||||
|
|
||||||
|
|
||||||
(defn evaluate-password
|
(defn evaluate-password
|
||||||
"Evaluate whether this proposed password is suitable for use."
|
"Evaluate whether this proposed password is suitable for use; return `true` is so, a keyword if not."
|
||||||
([pass1 pass2]
|
([pass1 pass2]
|
||||||
(and pass1 (>= (count pass1) 8) (.equals pass1 pass2)))
|
(cond
|
||||||
|
(< (count pass1) 8) :chpass-too-short
|
||||||
|
(.equals pass1 pass2) true
|
||||||
|
true :chpass-bad-match))
|
||||||
([password]
|
([password]
|
||||||
(evaluate-password password password)))
|
(evaluate-password password password)))
|
||||||
|
|
||||||
|
|
@ -120,39 +128,44 @@
|
||||||
"Return the map of features of this user, if any."
|
"Return the map of features of this user, if any."
|
||||||
[username]
|
[username]
|
||||||
(if
|
(if
|
||||||
(and username (> (count (str username)) 0))
|
(and username (pos? (count (str username))))
|
||||||
((keyword username) (get-users))))
|
((keyword username) (get-users))))
|
||||||
|
|
||||||
|
|
||||||
(defn add-user
|
(defn add-user
|
||||||
"Add a user to the passwd file with this username, initial password and email address and admin flag."
|
"Add a user to the passwd file with this `username`, initial password `newpass`,
|
||||||
|
`email` address and `admin` flag; *or*, modify an existing user. Return true
|
||||||
|
if user is successfully stored, false otherwise."
|
||||||
[username newpass email admin]
|
[username newpass email admin]
|
||||||
(let [users (get-users)
|
(timbre/info "Trying to add user " username)
|
||||||
user ((keyword username) users)
|
(cond
|
||||||
password (if
|
(not (string? username)) (throw (Exception. "Username must be a string."))
|
||||||
(and newpass (evaluate-password newpass))
|
(zero? (count username)) (throw (Exception. "Username cannot be zero length"))
|
||||||
(password/encrypt newpass))
|
true (let [users (get-users)
|
||||||
details {:email email
|
user ((keyword username) users)
|
||||||
:admin (if
|
password (if
|
||||||
(and (string? admin) (> (count admin) 0))
|
(and newpass (evaluate-password newpass))
|
||||||
true
|
(password/encrypt newpass))
|
||||||
false)}
|
details {:email email
|
||||||
;; if we have a valid password we want to include it in the details to update.
|
:admin (if
|
||||||
full-details (if password
|
(and (string? admin) (pos? (count admin)))
|
||||||
(merge details {:password password})
|
true
|
||||||
details)]
|
false)}
|
||||||
(try
|
;; if we have a valid password we want to include it in the details to update.
|
||||||
(locking password-file-path
|
full-details (if password
|
||||||
(spit password-file-path
|
(assoc details :password password)
|
||||||
(merge users
|
details)]
|
||||||
{(keyword username) (merge user full-details)}))
|
(try
|
||||||
(timbre/info (str "Successfully added user " username))
|
(locking password-file-path
|
||||||
true)
|
(spit password-file-path
|
||||||
(catch Exception any
|
(assoc users (keyword username) (merge user full-details)))
|
||||||
(timbre/error
|
(timbre/info "Successfully added user " username)
|
||||||
(format "Adding user %s failed: %s (%s)"
|
true)
|
||||||
username (.getName (.getClass any)) (.getMessage any)))
|
(catch Exception any
|
||||||
false))))
|
(timbre/error
|
||||||
|
(format "Adding user %s failed: %s (%s)"
|
||||||
|
username (.getName (.getClass any)) (.getMessage any)))
|
||||||
|
false)))))
|
||||||
|
|
||||||
|
|
||||||
(defn delete-user
|
(defn delete-user
|
||||||
|
|
|
||||||
54
src/smeagol/configuration.clj
Normal file
54
src/smeagol/configuration.clj
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
(ns ^{:doc "Read and make available configuration."
|
||||||
|
:author "Simon Brooke"}
|
||||||
|
smeagol.configuration
|
||||||
|
(:require [environ.core :refer [env]]
|
||||||
|
[noir.io :as io]
|
||||||
|
[taoensso.timbre :as timbre]))
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;;;;
|
||||||
|
;;;; 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) 2017 Simon Brooke
|
||||||
|
;;;;
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;;;;
|
||||||
|
;;;; Right, doing the data visualisation thing is tricky. Doing it in the
|
||||||
|
;;;; pipeline doesn't work, because the md-to-html-string filter messes up
|
||||||
|
;;;; both YAML and JSON notation. So we need to extract the visualisation
|
||||||
|
;;;; fragments from the Markdown text and replace them with tokens we will
|
||||||
|
;;;; recognise afterwards, perform md-to-html-string, and then replace our
|
||||||
|
;;;; tokens with the transformed visualisation specification.
|
||||||
|
;;;;
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
|
||||||
|
(def config-file-path
|
||||||
|
"The relative path to the config file."
|
||||||
|
(or
|
||||||
|
(env :smeagol-config)
|
||||||
|
(str (io/resource-path) "../config.edn")))
|
||||||
|
|
||||||
|
|
||||||
|
(def config
|
||||||
|
"The actual configuration, as a map."
|
||||||
|
(try
|
||||||
|
(read-string (slurp config-file-path))
|
||||||
|
(catch Exception any
|
||||||
|
(timbre/error "Could not load configuration" any)
|
||||||
|
{})))
|
||||||
|
|
@ -41,22 +41,12 @@
|
||||||
(starts-with? line "-") (str "<p><del>" (subs line 1) "</del></p>")
|
(starts-with? line "-") (str "<p><del>" (subs line 1) "</del></p>")
|
||||||
(starts-with? line "@@") "</div><div class='change'>"
|
(starts-with? line "@@") "</div><div class='change'>"
|
||||||
(starts-with? line "\\") (str "<p class='warn'>" (subs line 1) "</p>")
|
(starts-with? line "\\") (str "<p class='warn'>" (subs line 1) "</p>")
|
||||||
:true (str "<p>" line "</p>")))
|
true (str "<p>" line "</p>")))
|
||||||
|
|
||||||
|
|
||||||
(defn diff2html
|
(defn diff2html
|
||||||
"Convert this string, assumed to be in diff format, to HTML."
|
"Convert this string, assumed to be in diff format, to HTML."
|
||||||
[^String diff-text]
|
[^String diff-text]
|
||||||
(apply str
|
(clojure.string/join (flatten (list "<div class='change'>" (join "\n" (remove nil? (map mung-line (drop 5 (split-lines diff-text))))) "</div>"))))
|
||||||
(flatten
|
|
||||||
(list "<div class='change'>"
|
|
||||||
(join "\n"
|
|
||||||
(remove nil?
|
|
||||||
(map mung-line
|
|
||||||
;; The first five lines are boilerplate, and
|
|
||||||
;; uninteresting for now
|
|
||||||
(drop 5
|
|
||||||
(split-lines diff-text)))))
|
|
||||||
"</div>"))))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
195
src/smeagol/formatting.clj
Normal file
195
src/smeagol/formatting.clj
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
(ns ^{:doc "Format Semagol's enhanced markdown format."
|
||||||
|
:author "Simon Brooke"}
|
||||||
|
smeagol.formatting
|
||||||
|
(:require [clojure.data.json :as json]
|
||||||
|
[clojure.string :as cs]
|
||||||
|
[cemerick.url :refer (url url-encode url-decode)]
|
||||||
|
[clj-yaml.core :as yaml]
|
||||||
|
[markdown.core :as md]
|
||||||
|
[smeagol.configuration :refer [config]]))
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;;;;
|
||||||
|
;;;; 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) 2017 Simon Brooke
|
||||||
|
;;;;
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;;;;
|
||||||
|
;;;; Right, doing the data visualisation thing is tricky. Doing it in the
|
||||||
|
;;;; pipeline doesn't work, because the md-to-html-string filter messes up
|
||||||
|
;;;; both YAML and JSON notation. So we need to extract the visualisation
|
||||||
|
;;;; fragments from the Markdown text and replace them with tokens we will
|
||||||
|
;;;; recognise afterwards, perform md-to-html-string, and then replace our
|
||||||
|
;;;; tokens with the transformed visualisation specification.
|
||||||
|
;;;;
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
;; Error to show if text to be rendered is nil.
|
||||||
|
(def no-text-error "No text: does the file exist?")
|
||||||
|
|
||||||
|
|
||||||
|
(defn local-links
|
||||||
|
"Rewrite text in `html-src` surrounded by double square brackets as a local link into this wiki."
|
||||||
|
[^String html-src]
|
||||||
|
(if 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)))
|
||||||
|
no-text-error))
|
||||||
|
|
||||||
|
|
||||||
|
(defn yaml->json
|
||||||
|
"Rewrite this string, assumed to be in YAML format, as JSON."
|
||||||
|
[^String yaml-src]
|
||||||
|
(json/write-str (yaml/parse-string yaml-src)))
|
||||||
|
|
||||||
|
|
||||||
|
(declare process-text)
|
||||||
|
|
||||||
|
|
||||||
|
(defn process-vega
|
||||||
|
"Process this `vega-src` string, assumed to be in YAML format, into a specification
|
||||||
|
of a Vega chart, and add the plumbing to render it."
|
||||||
|
[^String vega-src ^Integer index]
|
||||||
|
(str
|
||||||
|
"<div class='data-visualisation' id='vis" index "'></div>\n"
|
||||||
|
"<script>\n//<![CDATA[\nvar vl"
|
||||||
|
index
|
||||||
|
" = "
|
||||||
|
(yaml->json (str "$schema: https://vega.github.io/schema/vega-lite/v2.json\n" vega-src))
|
||||||
|
";\nvega.embed('#vis"
|
||||||
|
index
|
||||||
|
"', vl"
|
||||||
|
index
|
||||||
|
");\n//]]\n</script>"))
|
||||||
|
|
||||||
|
|
||||||
|
(defn process-mermaid
|
||||||
|
"Lightly mung this `graph-spec`, assumed to be a mermaid specification."
|
||||||
|
[^String graph-spec ^Integer index]
|
||||||
|
(str "<div class=\"mermaid data-visualisation\">\n"
|
||||||
|
graph-spec
|
||||||
|
"\n</div>"))
|
||||||
|
|
||||||
|
|
||||||
|
(defn process-backticks
|
||||||
|
"Effectively, escape the backticks surrounding this `text`, by protecting them
|
||||||
|
from the `md->html` filter."
|
||||||
|
[^String text ^Integer index]
|
||||||
|
(str "<pre class=\"backticks\">```" (.trim text) "\n```</pre>"))
|
||||||
|
|
||||||
|
|
||||||
|
(defn get-first-token
|
||||||
|
"Return the first space-separated token of this `string`."
|
||||||
|
[^String string]
|
||||||
|
(if string (first (cs/split string #"[^a-zA-Z0-9]+"))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn- process-markdown-fragment
|
||||||
|
"Within the context of `process-text`, process a fragment believed to be markdown.
|
||||||
|
|
||||||
|
As with `process-text`, this function returns a map with two top-level keys:
|
||||||
|
`:inclusions`, a map of constructed keywords to inclusion specifications,
|
||||||
|
and `:text`, an HTML text string with the keywords present where the
|
||||||
|
corresponding inclusion should be inserted."
|
||||||
|
[index result fragment fragments processed]
|
||||||
|
(process-text
|
||||||
|
(inc index)
|
||||||
|
result
|
||||||
|
fragments
|
||||||
|
(cons fragment processed)))
|
||||||
|
|
||||||
|
|
||||||
|
(defn- apply-formatter
|
||||||
|
"Within the context of `process-text`, process a fragment for which an explicit
|
||||||
|
§formatter has been identified.
|
||||||
|
|
||||||
|
As with `process-text`, this function returns a map with two top-level keys:
|
||||||
|
`:inclusions`, a map of constructed keywords to inclusion specifications,
|
||||||
|
and `:text`, an HTML text string with the keywords present where the
|
||||||
|
corresponding inclusion should be inserted."
|
||||||
|
[index result fragments processed fragment token formatter]
|
||||||
|
(let
|
||||||
|
[kw (keyword (str "inclusion-" index))]
|
||||||
|
(process-text
|
||||||
|
(inc index)
|
||||||
|
(assoc-in result [:inclusions kw] (apply formatter (list (subs fragment (count token)) index)))
|
||||||
|
(rest fragments)
|
||||||
|
(cons kw processed))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn process-text
|
||||||
|
"Process this `text`, assumed to be markdown potentially containing both local links
|
||||||
|
and YAML visualisation specifications, and return a map comprising JSON visualisation
|
||||||
|
specification, and HTML text with markers for where those should be reinserted.
|
||||||
|
|
||||||
|
The map has two top-level keys: `:inclusions`, a map of constructed keywords to
|
||||||
|
inclusion specifications, and `:text`, an HTML text string with the keywords
|
||||||
|
present where the corresponding inclusion should be inserted."
|
||||||
|
([^String text]
|
||||||
|
(process-text 0 {:inclusions {}} (cs/split (or text "") #"```") '()))
|
||||||
|
([index result fragments processed]
|
||||||
|
(let [fragment (first fragments)
|
||||||
|
;; if I didn't find a formatter for a back-tick marked fragment,
|
||||||
|
;; I need to put the backticks back in.
|
||||||
|
remarked (if (odd? index) (str "```" fragment "\n```") fragment)
|
||||||
|
first-token (get-first-token fragment)
|
||||||
|
formatter (eval ((:formatters config) first-token))]
|
||||||
|
(cond
|
||||||
|
(empty? fragments)
|
||||||
|
(assoc result :text
|
||||||
|
(local-links
|
||||||
|
(md/md-to-html-string
|
||||||
|
(cs/join "\n\n" (reverse processed))
|
||||||
|
:heading-anchors true)))
|
||||||
|
formatter
|
||||||
|
(apply-formatter index result fragments processed fragment first-token formatter)
|
||||||
|
true
|
||||||
|
(process-markdown-fragment index result remarked (rest fragments) processed)))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn reintegrate-inclusions
|
||||||
|
"Given a map of the form produced by `process-text`, return a string of HTML text
|
||||||
|
with the inclusions (if any) reintegrated."
|
||||||
|
([processed-text]
|
||||||
|
(reintegrate-inclusions (:inclusions processed-text) (:text processed-text)))
|
||||||
|
([inclusions text]
|
||||||
|
(let [ks (keys inclusions)]
|
||||||
|
(if (empty? (keys inclusions))
|
||||||
|
text
|
||||||
|
(let [kw (first ks)]
|
||||||
|
(reintegrate-inclusions
|
||||||
|
(dissoc inclusions kw)
|
||||||
|
(cs/replace
|
||||||
|
text
|
||||||
|
(str kw)
|
||||||
|
(cs/replace (kw inclusions) "\\/" "/"))))))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn md->html
|
||||||
|
"Take this markdown source, and return HTML."
|
||||||
|
[md-src]
|
||||||
|
(reintegrate-inclusions (process-text md-src)))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,22 +1,21 @@
|
||||||
(ns ^{:doc "Set up, configure, and clean up after the wiki server."
|
(ns ^{:doc "Set up, configure, and clean up after the wiki server."
|
||||||
:author "Simon Brooke"}
|
:author "Simon Brooke"}
|
||||||
smeagol.handler
|
smeagol.handler
|
||||||
(:require [compojure.core :refer [defroutes]]
|
(:require [clojure.java.io :as cjio]
|
||||||
|
[compojure.core :refer [defroutes]]
|
||||||
|
[compojure.route :as route]
|
||||||
|
[cronj.core :as cronj]
|
||||||
|
[environ.core :refer [env]]
|
||||||
|
[noir.response :refer [redirect]]
|
||||||
|
[noir.session :as session]
|
||||||
|
[noir.util.middleware :refer [app-handler]]
|
||||||
|
[ring.middleware.defaults :refer [site-defaults]]
|
||||||
|
[selmer.parser :as parser]
|
||||||
[smeagol.routes.wiki :refer [wiki-routes]]
|
[smeagol.routes.wiki :refer [wiki-routes]]
|
||||||
[smeagol.middleware :refer [load-middleware]]
|
[smeagol.middleware :refer [load-middleware]]
|
||||||
[smeagol.session-manager :as session-manager]
|
[smeagol.session-manager :as session-manager]
|
||||||
[noir.response :refer [redirect]]
|
|
||||||
[noir.util.middleware :refer [app-handler]]
|
|
||||||
[noir.util.route :refer [restricted]]
|
|
||||||
[noir.session :as session]
|
|
||||||
[ring.middleware.defaults :refer [site-defaults]]
|
|
||||||
[compojure.route :as route]
|
|
||||||
[taoensso.timbre :as timbre]
|
[taoensso.timbre :as timbre]
|
||||||
[taoensso.timbre.appenders.core :as appenders]
|
[taoensso.timbre.appenders.3rd-party.rotor :as rotor]))
|
||||||
[taoensso.timbre.appenders.3rd-party.rotor :as rotor]
|
|
||||||
[selmer.parser :as parser]
|
|
||||||
[environ.core :refer [env]]
|
|
||||||
[cronj.core :as cronj]))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;
|
;;;;
|
||||||
|
|
@ -48,23 +47,6 @@
|
||||||
(route/resources "/")
|
(route/resources "/")
|
||||||
(route/not-found "Not Found"))
|
(route/not-found "Not Found"))
|
||||||
|
|
||||||
(defn init
|
|
||||||
"init will be called once when
|
|
||||||
app is deployed as a servlet on
|
|
||||||
an app server such as Tomcat
|
|
||||||
put any initialization code here"
|
|
||||||
[]
|
|
||||||
(timbre/merge-config!
|
|
||||||
{:appenders
|
|
||||||
{:rotor (rotor/rotor-appender
|
|
||||||
{:path "smeagol.log"
|
|
||||||
:max-size (* 512 1024)
|
|
||||||
:backlog 10})}})
|
|
||||||
(if (env :dev) (parser/cache-off!))
|
|
||||||
;;start the expired session cleanup job
|
|
||||||
(cronj/start! session-manager/cleanup-job)
|
|
||||||
(timbre/info "\n-=[ smeagol started successfully"
|
|
||||||
(when (env :dev) "using the development profile") "]=-"))
|
|
||||||
|
|
||||||
(defn destroy
|
(defn destroy
|
||||||
"destroy will be called when your application
|
"destroy will be called when your application
|
||||||
|
|
@ -74,28 +56,53 @@
|
||||||
(cronj/shutdown! session-manager/cleanup-job)
|
(cronj/shutdown! session-manager/cleanup-job)
|
||||||
(timbre/info "shutdown complete!"))
|
(timbre/info "shutdown complete!"))
|
||||||
|
|
||||||
|
|
||||||
|
(defn init
|
||||||
|
"init will be called once when
|
||||||
|
app is deployed as a servlet on
|
||||||
|
an app server such as Tomcat
|
||||||
|
put any initialization code here"
|
||||||
|
[]
|
||||||
|
(try
|
||||||
|
(timbre/merge-config!
|
||||||
|
{:appenders
|
||||||
|
{:rotor (rotor/rotor-appender
|
||||||
|
{:path "smeagol.log"
|
||||||
|
:max-size (* 512 1024)
|
||||||
|
:backlog 10})}})
|
||||||
|
(cronj/start! session-manager/cleanup-job)
|
||||||
|
(if (env :dev) (parser/cache-off!))
|
||||||
|
;;start the expired session cleanup job
|
||||||
|
(timbre/info "\n-=[ smeagol started successfully"
|
||||||
|
(when (env :dev) "using the development profile") "]=-")
|
||||||
|
(catch Exception any
|
||||||
|
(timbre/error "Failure during startup" any)
|
||||||
|
(destroy))))
|
||||||
|
|
||||||
;; timeout sessions after 30 minutes
|
;; timeout sessions after 30 minutes
|
||||||
(def session-defaults
|
(def session-defaults
|
||||||
{:timeout (* 60 30)
|
{:timeout (* 60 30)
|
||||||
:timeout-response (redirect "/")})
|
:timeout-response (redirect "/")})
|
||||||
|
|
||||||
(defn- mk-defaults
|
|
||||||
"set to true to enable XSS protection"
|
(defn- make-defaults
|
||||||
[xss-protection?]
|
"set to true to enable XSS protection"
|
||||||
(-> site-defaults
|
[xss-protection?]
|
||||||
(update-in [:session] merge session-defaults)
|
(-> site-defaults
|
||||||
(assoc-in [:security :anti-forgery] xss-protection?)))
|
(update-in [:session] merge session-defaults)
|
||||||
|
(assoc-in [:security :anti-forgery] xss-protection?)))
|
||||||
|
|
||||||
|
|
||||||
(def app (app-handler
|
(def app (app-handler
|
||||||
;; add your application routes here
|
;; add your application routes here
|
||||||
[wiki-routes base-routes]
|
[wiki-routes base-routes]
|
||||||
;; add custom middleware here
|
;; add custom middleware here
|
||||||
:middleware (load-middleware)
|
:middleware (load-middleware)
|
||||||
:ring-defaults (mk-defaults false)
|
:ring-defaults (make-defaults true)
|
||||||
;; add access rules here
|
;; add access rules here
|
||||||
:access-rules [{:redirect "/auth"
|
:access-rules [{:redirect "/auth"
|
||||||
:rule user-access}]
|
:rule user-access}]
|
||||||
;; serialize/deserialize the following data formats
|
;; serialize/deserialize the following data formats
|
||||||
;; available formats:
|
;; available formats:
|
||||||
;; :json :json-kw :yaml :yaml-kw :edn :yaml-in-html
|
;; :json :json-kw :yaml :yaml-kw :edn :yaml-in-html
|
||||||
:formats [:json-kw :edn :transit-json]))
|
:formats [:json-kw :edn :transit-json]))
|
||||||
|
|
|
||||||
|
|
@ -41,18 +41,30 @@
|
||||||
[^String log-entry ^String file-path]
|
[^String log-entry ^String file-path]
|
||||||
(timbre/info (format "searching '%s' for '%s'" log-entry file-path))
|
(timbre/info (format "searching '%s' for '%s'" log-entry file-path))
|
||||||
(cond
|
(cond
|
||||||
(not
|
(seq (filter (fn* [p1__341301#] (= (first p1__341301#) file-path)) (:changed_files log-entry)))
|
||||||
(empty?
|
|
||||||
(filter
|
|
||||||
#(= (first %) file-path)
|
|
||||||
(:changed_files log-entry))))
|
|
||||||
log-entry))
|
log-entry))
|
||||||
|
|
||||||
|
|
||||||
(defn find-history [^String git-directory-path ^String file-path]
|
(defn load-or-init-repo
|
||||||
|
"There's a bootstrapping problem: when Smeagol first starts the repository
|
||||||
|
hasn't been initialised. Try to open the repository at this `git-directory-path`;
|
||||||
|
if an exception is thrown, try to init a repository at this `git-directory-path`,
|
||||||
|
and then open it and populate it."
|
||||||
|
[^String git-directory-path]
|
||||||
|
(try
|
||||||
|
(git/load-repo git-directory-path)
|
||||||
|
(catch java.io.FileNotFoundException fnf
|
||||||
|
(git/git-init git-directory-path)
|
||||||
|
(let [repo (git/load-repo git-directory-path)]
|
||||||
|
(git/git-add-and-commit repo "Initial commit")
|
||||||
|
repo))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn find-history
|
||||||
"Return the log entries in the repository at this `git-directory-path`
|
"Return the log entries in the repository at this `git-directory-path`
|
||||||
which refer to changes to the file at this `file-path`."
|
which refer to changes to the file at this `file-path`."
|
||||||
(let [repository (git/load-repo git-directory-path)]
|
[^String git-directory-path ^String file-path]
|
||||||
|
(let [repository (load-or-init-repo git-directory-path)]
|
||||||
(filter
|
(filter
|
||||||
#(entry-contains % file-path)
|
#(entry-contains % file-path)
|
||||||
(map #(q/commit-info repository %)
|
(map #(q/commit-info repository %)
|
||||||
|
|
@ -72,39 +84,40 @@
|
||||||
(try
|
(try
|
||||||
(.reset result reader (.getId tree))
|
(.reset result reader (.getId tree))
|
||||||
(finally
|
(finally
|
||||||
(.release reader)
|
(.close reader)
|
||||||
(.dispose walk)))
|
(.dispose walk)))
|
||||||
result))
|
result))
|
||||||
|
|
||||||
|
|
||||||
(defn diff
|
(defn diff
|
||||||
"Find the diff in the file at `file-path` within the repository at
|
"Find the diff in the file at `file-path` within the repository at
|
||||||
`git-directory-path` between versions `older` and `newer` or between the specified
|
`git-directory-path` between versions `older` and `newer` or between the specified
|
||||||
`version` and the current version of the file. Returns the diff as a string.
|
`version` and the current version of the file. Returns the diff as a string.
|
||||||
|
|
||||||
Based on JGit Cookbook ShowFileDiff."
|
Based on JGit Cookbook ShowFileDiff."
|
||||||
([^String git-directory-path ^String file-path ^String version]
|
([^String git-directory-path ^String file-path ^String version]
|
||||||
(diff git-directory-path file-path version
|
(diff git-directory-path file-path version
|
||||||
(:id (first (find-history git-directory-path file-path)))))
|
(:id (first (find-history git-directory-path file-path)))))
|
||||||
([^String git-directory-path ^String file-path ^String older ^String newer]
|
([^String git-directory-path ^String file-path ^String older ^String newer]
|
||||||
(let [git-r (git/load-repo git-directory-path)
|
(let [git-r (load-or-init-repo git-directory-path)
|
||||||
old-parse (prepare-tree-parser git-r older)
|
old-parse (prepare-tree-parser git-r older)
|
||||||
new-parse (prepare-tree-parser git-r newer)
|
new-parse (prepare-tree-parser git-r newer)
|
||||||
out (java.io.ByteArrayOutputStream.)]
|
out (java.io.ByteArrayOutputStream.)]
|
||||||
(map
|
(doall
|
||||||
#(let [formatter (DiffFormatter. out)]
|
(map
|
||||||
(.setRepository formatter (.getRepository git-r))
|
#(let [formatter (DiffFormatter. out)]
|
||||||
(.format formatter %)
|
(.setRepository formatter (.getRepository git-r))
|
||||||
%)
|
(.format formatter %)
|
||||||
(.call
|
%)
|
||||||
(.setOutputStream
|
(.call
|
||||||
(.setPathFilter
|
(.setOutputStream
|
||||||
(.setNewTree
|
(.setPathFilter
|
||||||
(.setOldTree (.diff git-r) old-parse)
|
(.setNewTree
|
||||||
new-parse)
|
(.setOldTree (.diff git-r) old-parse)
|
||||||
(PathFilter/create file-path))
|
new-parse)
|
||||||
out)))
|
(PathFilter/create file-path))
|
||||||
(.toString out))))
|
out))))
|
||||||
|
(str out))))
|
||||||
|
|
||||||
|
|
||||||
(defn fetch-version
|
(defn fetch-version
|
||||||
|
|
@ -113,7 +126,7 @@
|
||||||
|
|
||||||
Based on JGit Cookbook ReadFileFromCommit."
|
Based on JGit Cookbook ReadFileFromCommit."
|
||||||
[^String git-directory-path ^String file-path ^String version]
|
[^String git-directory-path ^String file-path ^String version]
|
||||||
(let [git-r (git/load-repo git-directory-path)
|
(let [git-r (load-or-init-repo git-directory-path)
|
||||||
repo (.getRepository git-r)
|
repo (.getRepository git-r)
|
||||||
walk (i/new-rev-walk git-r)
|
walk (i/new-rev-walk git-r)
|
||||||
commit (i/bound-commit git-r walk (ObjectId/fromString version))
|
commit (i/bound-commit git-r walk (ObjectId/fromString version))
|
||||||
|
|
@ -127,4 +140,4 @@
|
||||||
(throw (IllegalStateException.
|
(throw (IllegalStateException.
|
||||||
(str "Did not find expected file '" file-path "'"))))
|
(str "Did not find expected file '" file-path "'"))))
|
||||||
(.copyTo (.open repo (.getObjectId tw 0)) out)
|
(.copyTo (.open repo (.getObjectId tw 0)) out)
|
||||||
(.toString out)))
|
(str out)))
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,18 @@
|
||||||
(ns ^{:doc "Render a page as HTML."
|
(ns ^{:doc "Render a page as HTML."
|
||||||
:author "Simon Brooke"}
|
:author "Simon Brooke"}
|
||||||
smeagol.layout
|
smeagol.layout
|
||||||
(:require [selmer.parser :as parser]
|
(:require [clojure.java.io :as cjio]
|
||||||
[clojure.string :as s]
|
[clojure.string :as s]
|
||||||
[ring.util.response :refer [content-type response]]
|
|
||||||
[compojure.response :refer [Renderable]]
|
[compojure.response :refer [Renderable]]
|
||||||
[environ.core :refer [env]]))
|
[environ.core :refer [env]]
|
||||||
|
[hiccup.core :refer [html]]
|
||||||
|
[ring.util.anti-forgery :refer [anti-forgery-field]]
|
||||||
|
[ring.util.response :refer [content-type response]]
|
||||||
|
[selmer.parser :as parser]
|
||||||
|
[smeagol.configuration :refer [config]]
|
||||||
|
[smeagol.sanity :refer :all]
|
||||||
|
[smeagol.util :as util]
|
||||||
|
[taoensso.timbre :as timbre]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;
|
;;;;
|
||||||
|
|
@ -33,26 +40,45 @@
|
||||||
|
|
||||||
(def template-path "templates/")
|
(def template-path "templates/")
|
||||||
|
|
||||||
|
(parser/add-tag! :csrf-field (fn [_ _] (anti-forgery-field)))
|
||||||
|
|
||||||
|
;; Attempt to do internationalisation more neatly
|
||||||
|
;; This tag takes two arguments, the first is a key, the (optional) second is a
|
||||||
|
;; default. The key is looked up in the i18n
|
||||||
|
(parser/add-tag! :i18n
|
||||||
|
(fn [args context-map]
|
||||||
|
(let [messages (:i18n context-map)
|
||||||
|
default (or (second args) (first args))]
|
||||||
|
(if (map? messages) (or (messages (keyword (first args))) default) default))))
|
||||||
|
|
||||||
|
|
||||||
(deftype RenderableTemplate [template params]
|
(deftype RenderableTemplate [template params]
|
||||||
Renderable
|
Renderable
|
||||||
(render [this request]
|
(render [this request]
|
||||||
(content-type
|
(try
|
||||||
(->> (assoc params
|
(content-type
|
||||||
(keyword (s/replace template #".html" "-selected")) "active"
|
(->> (assoc params
|
||||||
:dev (env :dev)
|
(keyword (s/replace template #".html" "-selected")) "active"
|
||||||
:servlet-context
|
:i18n (util/get-messages request)
|
||||||
(if-let [context (:servlet-context request)]
|
:dev (env :dev)
|
||||||
;; If we're not inside a serlvet environment (for
|
:servlet-context
|
||||||
;; example when using mock requests), then
|
(if-let [context (:servlet-context request)]
|
||||||
;; .getContextPath might not exist
|
;; If we're not inside a serlvet environment (for
|
||||||
(try (.getContextPath context)
|
;; example when using mock requests), then
|
||||||
|
;; .getContextPath might not exist
|
||||||
|
(try (.getContextPath context)
|
||||||
(catch IllegalArgumentException _ context))))
|
(catch IllegalArgumentException _ context))))
|
||||||
(parser/render-file (str template-path template))
|
(parser/render-file (str template-path template))
|
||||||
response)
|
response)
|
||||||
"text/html; charset=utf-8")))
|
"text/html; charset=utf-8")
|
||||||
|
(catch Exception any
|
||||||
|
(show-sanity-check-error any)))))
|
||||||
|
|
||||||
|
|
||||||
(defn render [template & [params]]
|
(defn render
|
||||||
(RenderableTemplate. template params))
|
[template & [params]]
|
||||||
|
(try
|
||||||
|
(RenderableTemplate. template params)
|
||||||
|
(catch Exception any
|
||||||
|
(show-sanity-check-error any))))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@
|
||||||
:author "Simon Brooke"}
|
:author "Simon Brooke"}
|
||||||
smeagol.middleware
|
smeagol.middleware
|
||||||
(:require [taoensso.timbre :as timbre]
|
(:require [taoensso.timbre :as timbre]
|
||||||
[selmer.parser :as parser]
|
|
||||||
[environ.core :refer [env]]
|
[environ.core :refer [env]]
|
||||||
[selmer.middleware :refer [wrap-error-page]]
|
[selmer.middleware :refer [wrap-error-page]]
|
||||||
[prone.middleware :refer [wrap-exceptions]]
|
[prone.middleware :refer [wrap-exceptions]]
|
||||||
|
[ring.middleware.anti-forgery :refer [wrap-anti-forgery]]
|
||||||
[noir-exception.core :refer [wrap-internal-error]]))
|
[noir-exception.core :refer [wrap-internal-error]]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
user (session/get :user)]
|
user (session/get :user)]
|
||||||
(layout/render "edit-users.html"
|
(layout/render "edit-users.html"
|
||||||
(merge (util/standard-params request)
|
(merge (util/standard-params request)
|
||||||
{:title "Select user to edit"
|
{:title (:edit-users-title (util/get-messages request))
|
||||||
:users (auth/list-users)}))))
|
:users (auth/list-users)}))))
|
||||||
|
|
||||||
(defn delete-user
|
(defn delete-user
|
||||||
|
|
@ -48,11 +48,11 @@
|
||||||
(let [params (keywordize-keys (:params request))
|
(let [params (keywordize-keys (:params request))
|
||||||
target (:target params)
|
target (:target params)
|
||||||
deleted (auth/delete-user target)
|
deleted (auth/delete-user target)
|
||||||
message (if deleted (str "Successfully deleted user " target))
|
message (if deleted (str (:del-user-success (util/get-messages request)) " " target "."))
|
||||||
error (if (not deleted) (str "Could not delete user " target))]
|
error (if (not deleted) (str (:del-user-fail (util/get-messages request)) " " target "."))]
|
||||||
(layout/render "edit-users.html"
|
(layout/render "edit-users.html"
|
||||||
(merge (util/standard-params request)
|
(merge (util/standard-params request)
|
||||||
{:title "Select user to edit"
|
{:title (:edit-users-title (util/get-messages request))
|
||||||
:message message
|
:message message
|
||||||
:error error
|
:error error
|
||||||
:users (auth/list-users)}))))
|
:users (auth/list-users)}))))
|
||||||
|
|
@ -61,24 +61,47 @@
|
||||||
(defn edit-user
|
(defn edit-user
|
||||||
"Put an individual user's details on screen for editing."
|
"Put an individual user's details on screen for editing."
|
||||||
[request]
|
[request]
|
||||||
(let [params (keywordize-keys (:params request))
|
(let [params (keywordize-keys (:params request))]
|
||||||
target (:target params)
|
(try
|
||||||
pass1 (:pass1 params)
|
(let [target (or (:target params) "")
|
||||||
password (if (and pass1 (auth/evaluate-password pass1 (:pass2 params))) pass1)
|
pass1 (:pass1 params)
|
||||||
stored (if (:email params)
|
pass2 (:pass2 params)
|
||||||
(auth/add-user target password (:email params) (:admin params)))
|
check-pass (auth/evaluate-password pass1 pass2)
|
||||||
message (if stored (str "User " target " was stored successfully."))
|
password (if (and pass1 (true? check-pass)) pass1)
|
||||||
error (if (and (:email params) (not stored))
|
stored (if
|
||||||
(str "User " target " was not stored."))
|
(and
|
||||||
details (auth/fetch-user-details target)]
|
(:email params)
|
||||||
(if message
|
(or
|
||||||
(timbre/info message))
|
(nil? pass1)
|
||||||
(if error
|
(zero? (count pass1))
|
||||||
(timbre/warn error))
|
(true? check-pass)))
|
||||||
(layout/render "edit-user.html"
|
(auth/add-user target password (:email params) (:admin params)))
|
||||||
(merge (util/standard-params request)
|
message (if stored (str (:save-user-success (util/get-messages request)) " " target "."))
|
||||||
{:title (str "Edit user " target)
|
error (if
|
||||||
:message message
|
(and (:email params) (not stored))
|
||||||
:error error
|
(str
|
||||||
:target target
|
(:save-user-fail (util/get-messages request))
|
||||||
:details details}))))
|
" " target ". "
|
||||||
|
(if (keyword? check-pass) (check-pass (util/get-messages request)))))
|
||||||
|
page (if stored "edit-users.html" "edit-user.html")
|
||||||
|
details (auth/fetch-user-details target)]
|
||||||
|
(if message
|
||||||
|
(timbre/info message))
|
||||||
|
(if error
|
||||||
|
(timbre/warn error))
|
||||||
|
(layout/render page
|
||||||
|
(merge (util/standard-params request)
|
||||||
|
{:title (str (:edit-title-prefix (util/get-messages request)) " " target)
|
||||||
|
:message message
|
||||||
|
:error error
|
||||||
|
:target target
|
||||||
|
:details details
|
||||||
|
:users (auth/list-users)})))
|
||||||
|
(catch Exception any
|
||||||
|
(timbre/error (.getMessage any))
|
||||||
|
(layout/render "edit-user.html"
|
||||||
|
(merge (util/standard-params request)
|
||||||
|
{:title (str (:edit-title-prefix (util/get-messages request)) " " (:target params))
|
||||||
|
:error (.getMessage any)
|
||||||
|
:target (:target params)
|
||||||
|
:details {:email (:email params) :admin (:admin params)}}))))))
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,26 @@
|
||||||
(ns ^{:doc "Render all the main pages of a very simple Wiki engine."
|
(ns ^{:doc "Render all the main pages of a very simple Wiki engine."
|
||||||
:author "Simon Brooke"}
|
:author "Simon Brooke"}
|
||||||
smeagol.routes.wiki
|
smeagol.routes.wiki
|
||||||
(:require [clojure.walk :refer :all]
|
(:require [cemerick.url :refer (url url-encode url-decode)]
|
||||||
[clojure.java.io :as cjio]
|
|
||||||
[cemerick.url :refer (url url-encode url-decode)]
|
|
||||||
[compojure.core :refer :all]
|
|
||||||
[clj-jgit.porcelain :as git]
|
[clj-jgit.porcelain :as git]
|
||||||
[markdown.core :as md]
|
[clojure.java.io :as cjio]
|
||||||
|
[clojure.string :as cs]
|
||||||
|
[clojure.walk :refer :all]
|
||||||
|
[compojure.core :refer :all]
|
||||||
[noir.io :as io]
|
[noir.io :as io]
|
||||||
[noir.response :as response]
|
[noir.response :as response]
|
||||||
[noir.util.route :as route]
|
[noir.util.route :as route]
|
||||||
[noir.session :as session]
|
[noir.session :as session]
|
||||||
[taoensso.timbre :as timbre]
|
|
||||||
[smeagol.authenticate :as auth]
|
[smeagol.authenticate :as auth]
|
||||||
[smeagol.diff2html :as d2h]
|
[smeagol.diff2html :as d2h]
|
||||||
[smeagol.layout :as layout]
|
[smeagol.formatting :refer [md->html]]
|
||||||
[smeagol.util :as util]
|
|
||||||
[smeagol.history :as hist]
|
[smeagol.history :as hist]
|
||||||
[smeagol.routes.admin :as admin]))
|
[smeagol.layout :as layout]
|
||||||
|
[smeagol.routes.admin :as admin]
|
||||||
|
[smeagol.sanity :refer [show-sanity-check-error]]
|
||||||
|
[smeagol.util :as util]
|
||||||
|
[smeagol.uploads :as ul]
|
||||||
|
[taoensso.timbre :as timbre]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;
|
;;;;
|
||||||
|
|
@ -42,31 +45,29 @@
|
||||||
;;;;
|
;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
|
||||||
(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"
|
||||||
[]
|
[]
|
||||||
(let [path (str (io/resource-path) "/content/")
|
(hist/load-or-init-repo util/content-dir))
|
||||||
repo (cjio/as-file (str path ".git"))]
|
|
||||||
(if (.exists repo) (git/load-repo repo)
|
|
||||||
(git/git-init path))))
|
|
||||||
|
|
||||||
|
|
||||||
(defn process-source
|
(defn process-source
|
||||||
"Process `source-text` and save it to the specified `file-path`, committing it
|
"Process `source-text` and save it to the specified `file-path`, committing it
|
||||||
to Git and finally redirecting to wiki-page."
|
to Git and finally redirecting to wiki-page."
|
||||||
[params suffix]
|
[params suffix request]
|
||||||
(let [source-text (:src params)
|
(let [source-text (:src params)
|
||||||
page (:page params)
|
page (:page params)
|
||||||
file-name (str page suffix)
|
file-name (str page suffix)
|
||||||
file-path (str (io/resource-path) "/content/" file-name)
|
file-path (cjio/file util/content-dir file-name)
|
||||||
exists? (.exists (clojure.java.io/as-file file-path))
|
exists? (.exists (cjio/as-file file-path))
|
||||||
git-repo (get-git-repo)
|
git-repo (get-git-repo)
|
||||||
user (session/get :user)
|
user (session/get :user)
|
||||||
email (auth/get-email user)
|
email (auth/get-email user)
|
||||||
summary (format "%s: %s" user (or (:summary params) "no summary"))]
|
summary (format "%s: %s" user (or (:summary params) "no summary"))]
|
||||||
(timbre/info (format "Saving %s's changes ('%s') to %s" user summary page))
|
(timbre/info (format "Saving %s's changes ('%s') to %s in file '%s'" user summary page file-path))
|
||||||
(spit file-path source-text)
|
(spit file-path source-text)
|
||||||
(if (not exists?) (git/git-add git-repo file-name))
|
(git/git-add git-repo file-name)
|
||||||
(git/git-commit git-repo summary {:name user :email email})
|
(git/git-commit git-repo summary {:name user :email email})
|
||||||
(response/redirect
|
(response/redirect
|
||||||
(str
|
(str
|
||||||
|
|
@ -74,59 +75,66 @@
|
||||||
(if
|
(if
|
||||||
(= suffix ".md")
|
(= suffix ".md")
|
||||||
(url-encode page)
|
(url-encode page)
|
||||||
"Introduction")))))
|
(util/get-message :default-page-title request))))))
|
||||||
|
|
||||||
|
|
||||||
(defn edit-page
|
(defn edit-page
|
||||||
"Render a page in a text-area for editing. This could have been done in the same function as wiki-page,
|
"Render a page in a text-area for editing. This could have been done in the same function as wiki-page,
|
||||||
and that would have been neat, but I couldn't see how to establish security if that were done."
|
and that would have been neat, but I couldn't see how to establish security if that were done."
|
||||||
([request]
|
([request]
|
||||||
(edit-page request "Introduction" ".md" "edit.html" "/content/_edit-side-bar.md"))
|
(edit-page request (util/get-message :default-page-title request) ".md" "edit.html" "_edit-side-bar.md"))
|
||||||
([request default suffix template side-bar]
|
([request default suffix template side-bar]
|
||||||
(let [params (keywordize-keys (:params request))
|
(or
|
||||||
src-text (:src params)
|
(show-sanity-check-error)
|
||||||
page (or (:page params) default)
|
(let [params (keywordize-keys (:params request))
|
||||||
file-path (str (io/resource-path) "content/" page suffix)
|
src-text (:src params)
|
||||||
exists? (.exists (cjio/as-file file-path))
|
page (or (:page params) default)
|
||||||
user (session/get :user)]
|
file-name (str page suffix)
|
||||||
(if (not exists?)
|
file-path (cjio/file util/content-dir file-name)
|
||||||
(timbre/info (format "File '%s' not found; creating a new file" file-path))
|
exists? (.exists (cjio/as-file file-path))
|
||||||
(timbre/info (format "Opening '%s' for editing" file-path)))
|
user (session/get :user)]
|
||||||
(cond src-text (process-source params suffix)
|
(if-not
|
||||||
true
|
exists?
|
||||||
(layout/render template
|
(timbre/info
|
||||||
(merge (util/standard-params request)
|
(format "File '%s' not found; creating a new file" file-path))
|
||||||
{:title (str "Edit " page)
|
(timbre/info (format "Opening '%s' for editing" file-path)))
|
||||||
:page page
|
(cond src-text (process-source params suffix request)
|
||||||
:side-bar (util/local-links (util/md->html side-bar))
|
true
|
||||||
:content (if exists? (io/slurp-resource (str "/content/" page suffix)) "")
|
(layout/render template
|
||||||
:exists exists?}))))))
|
(merge (util/standard-params request)
|
||||||
|
{:title (str (util/get-message :edit-title-prefix request) " " page)
|
||||||
|
:page page
|
||||||
|
:side-bar (md->html (slurp (cjio/file util/content-dir side-bar)))
|
||||||
|
:content (if exists? (slurp file-path) "")
|
||||||
|
:exists exists?})))))))
|
||||||
|
|
||||||
|
|
||||||
(defn edit-css-page
|
(defn edit-css-page
|
||||||
"Render a stylesheet in a text-area for editing.."
|
"Render a stylesheet in a text-area for editing.."
|
||||||
[request]
|
[request]
|
||||||
(edit-page request "stylesheet" ".css" "edit-css.html" "/content/_edit-side-bar.md"))
|
(edit-page request "stylesheet" ".css" "edit-css.html" "_edit-side-bar.md"))
|
||||||
|
|
||||||
|
|
||||||
(defn wiki-page
|
(defn wiki-page
|
||||||
"Render the markdown page specified in this `request`, if any. If none found, redirect to edit-page"
|
"Render the markdown page specified in this `request`, if any. If none found, redirect to edit-page"
|
||||||
[request]
|
[request]
|
||||||
(let [params (keywordize-keys (:params request))
|
(or
|
||||||
page (or (:page params) "Introduction")
|
(show-sanity-check-error)
|
||||||
file-name (str "/content/" page ".md")
|
(let [params (keywordize-keys (:params request))
|
||||||
file-path (str (io/resource-path) file-name)
|
page (or (:page params) (util/get-message :default-page-title "Introduction" request))
|
||||||
exists? (.exists (clojure.java.io/as-file file-path))]
|
file-name (str page ".md")
|
||||||
(cond exists?
|
file-path (cjio/file util/content-dir file-name)
|
||||||
(do
|
exists? (.exists (clojure.java.io/as-file file-path))]
|
||||||
(timbre/info (format "Showing page '%s'" page))
|
(cond exists?
|
||||||
(layout/render "wiki.html"
|
(do
|
||||||
(merge (util/standard-params request)
|
(timbre/info (format "Showing page '%s' from file '%s'" page file-path))
|
||||||
{:title page
|
(layout/render "wiki.html"
|
||||||
:page page
|
(merge (util/standard-params request)
|
||||||
:content (util/local-links (util/md->html file-name))
|
{:title page
|
||||||
:editable true})))
|
:page page
|
||||||
true (response/redirect (str "/edit?page=" page)))))
|
:content (md->html (slurp file-path))
|
||||||
|
:editable true})))
|
||||||
|
true (response/redirect (str "/edit?page=" page))))))
|
||||||
|
|
||||||
|
|
||||||
(defn history-page
|
(defn history-page
|
||||||
|
|
@ -134,76 +142,115 @@
|
||||||
if any. If none, error?"
|
if any. If none, error?"
|
||||||
[request]
|
[request]
|
||||||
(let [params (keywordize-keys (:params request))
|
(let [params (keywordize-keys (:params request))
|
||||||
page (url-decode (or (:page params) "Introduction"))
|
page (url-decode (or (:page params) (util/get-message :default-page-title request)))
|
||||||
file-name (str page ".md")
|
file-name (str page ".md")
|
||||||
repo-path (str (io/resource-path) "/content/")]
|
repo-path util/content-dir]
|
||||||
|
(timbre/info (format "Showing history of page '%s'" page))
|
||||||
(layout/render "history.html"
|
(layout/render "history.html"
|
||||||
(merge (util/standard-params request)
|
(merge (util/standard-params request)
|
||||||
{:title (str "History of " page)
|
{:title (str "History of " page)
|
||||||
:page page
|
:page page
|
||||||
:history (hist/find-history repo-path file-name)}))))
|
:history (hist/find-history repo-path file-name)}))))
|
||||||
|
|
||||||
|
(defn upload-page
|
||||||
|
"Render a form to allow the upload of a file."
|
||||||
|
[request]
|
||||||
|
(let [params (keywordize-keys (:params request))
|
||||||
|
data-path (str (io/resource-path) "/content/uploads/")
|
||||||
|
git-repo (get-git-repo)
|
||||||
|
upload (:upload params)
|
||||||
|
uploaded (if upload (ul/store-upload params data-path))
|
||||||
|
user (session/get :user)
|
||||||
|
summary (format "%s: %s" user (or (:summary params) "no summary"))]
|
||||||
|
(if
|
||||||
|
uploaded
|
||||||
|
(do
|
||||||
|
(git/git-add git-repo uploaded)
|
||||||
|
(git/git-commit git-repo summary {:name user :email (auth/get-email user)})))
|
||||||
|
(layout/render "upload.html"
|
||||||
|
(merge (util/standard-params request)
|
||||||
|
{:title (util/get-message :file-upload-title request)
|
||||||
|
:uploaded uploaded
|
||||||
|
:is-image (and
|
||||||
|
uploaded
|
||||||
|
(or
|
||||||
|
(cs/ends-with? uploaded ".gif")
|
||||||
|
(cs/ends-with? uploaded ".jpg")
|
||||||
|
(cs/ends-with? uploaded ".jpeg")
|
||||||
|
(cs/ends-with? uploaded ".png")
|
||||||
|
(cs/ends-with? uploaded ".GIF")
|
||||||
|
(cs/ends-with? uploaded ".JPG")
|
||||||
|
(cs/ends-with? uploaded ".PNG")))}))))
|
||||||
|
|
||||||
|
|
||||||
(defn version-page
|
(defn version-page
|
||||||
"Render a specific historical version of a page"
|
"Render a specific historical version of a page"
|
||||||
[request]
|
[request]
|
||||||
(let [params (keywordize-keys (:params request))
|
(let [params (keywordize-keys (:params request))
|
||||||
page (url-decode (or (:page params) "Introduction"))
|
page (url-decode (or (:page params) (util/get-message :default-page-title request)))
|
||||||
version (:version params)
|
version (:version params)
|
||||||
file-name (str page ".md")
|
file-name (str page ".md")
|
||||||
repo-path (str (io/resource-path) "/content/")]
|
content (hist/fetch-version util/content-dir file-name version)]
|
||||||
|
(timbre/info (format "Showing version '%s' of page '%s'" version page))
|
||||||
(layout/render "wiki.html"
|
(layout/render "wiki.html"
|
||||||
(merge (util/standard-params request)
|
(merge (util/standard-params request)
|
||||||
{:title (str "Version " version " of " page)
|
{:title (str (util/get-message :vers-col-hdr request) " " version " " (util/get-message :of request) " " page)
|
||||||
:page page
|
:page page
|
||||||
:content (util/local-links
|
:content (md->html content)}))))
|
||||||
(md/md-to-html-string
|
|
||||||
(hist/fetch-version
|
|
||||||
repo-path file-name version)))}))))
|
|
||||||
|
|
||||||
|
|
||||||
(defn diff-page
|
(defn diff-page
|
||||||
"Render a diff between two versions of a page"
|
"Render a diff between two versions of a page"
|
||||||
[request]
|
[request]
|
||||||
(let [params (keywordize-keys (:params request))
|
(let [params (keywordize-keys (:params request))
|
||||||
page (url-decode (or (:page params) "Introduction"))
|
page (url-decode (or (:page params) (util/get-message :default-page-title request)))
|
||||||
version (:version params)
|
version (:version params)
|
||||||
file-name (str page ".md")
|
file-name (str page ".md")]
|
||||||
repo-path (str (io/resource-path) "/content/")]
|
(timbre/info (format "Showing diff between version '%s' of page '%s' and current" version page))
|
||||||
(layout/render "wiki.html"
|
(layout/render "wiki.html"
|
||||||
(merge (util/standard-params request)
|
(merge (util/standard-params request)
|
||||||
{:title (str "Changes since version " version " of " page)
|
{:title
|
||||||
|
(str
|
||||||
|
(util/get-message :diff-title-prefix request)
|
||||||
|
" "
|
||||||
|
version
|
||||||
|
" "
|
||||||
|
(util/get-message :of request)
|
||||||
|
" "
|
||||||
|
page)
|
||||||
:page page
|
:page page
|
||||||
:content (d2h/diff2html (hist/diff repo-path file-name version))}))))
|
:content (d2h/diff2html
|
||||||
|
(hist/diff util/content-dir file-name version))}))))
|
||||||
|
|
||||||
|
|
||||||
(defn auth-page
|
(defn auth-page
|
||||||
"Render the auth page"
|
"Render the auth page"
|
||||||
[request]
|
[request]
|
||||||
(let [params (keywordize-keys (:form-params request))
|
(or
|
||||||
username (:username params)
|
(show-sanity-check-error)
|
||||||
password (:password params)
|
(let [params (keywordize-keys (:form-params request))
|
||||||
action (:action params)
|
username (:username params)
|
||||||
user (session/get :user)
|
password (:password params)
|
||||||
redirect-to (or (:redirect-to params) "/wiki")]
|
action (:action params)
|
||||||
(cond
|
user (session/get :user)
|
||||||
(= action "Logout!")
|
redirect-to (or (:redirect-to params) "/wiki")]
|
||||||
(do
|
(cond
|
||||||
(timbre/info (str "User " user " logging out"))
|
(= action (util/get-message :logout-label request))
|
||||||
(session/remove! :user)
|
(do
|
||||||
(response/redirect redirect-to))
|
(timbre/info (str "User " user " logging out"))
|
||||||
(and username password (auth/authenticate username password))
|
(session/remove! :user)
|
||||||
(do
|
(response/redirect redirect-to))
|
||||||
(session/put! :user username)
|
(and username password (auth/authenticate username password))
|
||||||
(response/redirect redirect-to))
|
(do
|
||||||
true
|
(session/put! :user username)
|
||||||
(layout/render "auth.html"
|
(response/redirect redirect-to))
|
||||||
(merge (util/standard-params request)
|
true
|
||||||
{:title (if user (str "Logout " user) "Log in")
|
(layout/render "auth.html"
|
||||||
:redirect-to ((:headers request) "referer")
|
(merge (util/standard-params request)
|
||||||
:side-bar (util/local-links (util/md->html "/content/_side-bar.md"))
|
{:title (if user
|
||||||
:header (util/local-links (util/md->html "/content/_header.md"))
|
(str (util/get-message :logout-link request) " " user)
|
||||||
:user user})))))
|
(util/get-message :login-link request))
|
||||||
|
:redirect-to ((:headers request) "referer")}))))))
|
||||||
|
|
||||||
|
|
||||||
(defn passwd-page
|
(defn passwd-page
|
||||||
|
|
@ -214,19 +261,19 @@
|
||||||
pass1 (:pass1 params)
|
pass1 (:pass1 params)
|
||||||
pass2 (:pass2 params)
|
pass2 (:pass2 params)
|
||||||
user (session/get :user)
|
user (session/get :user)
|
||||||
message (cond
|
check-pass (auth/evaluate-password pass1 pass2)
|
||||||
(nil? oldpass) nil
|
changed? (and
|
||||||
(and (auth/evaluate-password pass1 pass2) (auth/change-pass user oldpass pass2))
|
(true? check-pass)
|
||||||
"Your password was changed"
|
(auth/change-pass user oldpass pass2))]
|
||||||
(< (count pass1) 8) "You proposed password wasn't long enough: 8 characters required"
|
|
||||||
(not (= pass1 pass2)) "Your proposed passwords don't match"
|
|
||||||
true "Your password was not changed")] ;; but I don't know why...
|
|
||||||
(layout/render "passwd.html"
|
(layout/render "passwd.html"
|
||||||
(merge (util/standard-params request)
|
(merge (util/standard-params request)
|
||||||
{:title (str "Change passord for " user)
|
{:title (str (util/get-message :chpass-title-prefix request) " " user)
|
||||||
:side-bar (util/local-links (util/md->html "/content/_side-bar.md"))
|
:message (if changed? (util/get-message :chpass-success request))
|
||||||
:header (util/local-links (util/md->html "/content/_header.md"))
|
:error (cond
|
||||||
:message message}))))
|
(nil? oldpass) nil
|
||||||
|
changed? nil
|
||||||
|
(keyword? check-pass) (util/get-message check-pass request)
|
||||||
|
true (util/get-message :chpass-fail request))}))))
|
||||||
|
|
||||||
|
|
||||||
(defroutes wiki-routes
|
(defroutes wiki-routes
|
||||||
|
|
@ -247,4 +294,5 @@
|
||||||
(POST "/auth" request (auth-page request))
|
(POST "/auth" request (auth-page request))
|
||||||
(GET "/passwd" request (passwd-page request))
|
(GET "/passwd" request (passwd-page request))
|
||||||
(POST "/passwd" request (passwd-page request))
|
(POST "/passwd" request (passwd-page request))
|
||||||
)
|
(GET "/upload" request (route/restricted (upload-page request)))
|
||||||
|
(POST "/upload" request (route/restricted (upload-page request))))
|
||||||
|
|
|
||||||
408
src/smeagol/sanity.clj
Normal file
408
src/smeagol/sanity.clj
Normal file
|
|
@ -0,0 +1,408 @@
|
||||||
|
(ns ^{:doc "Functions related to sanity checks and error reporting in conditions where the environment may not be sane."
|
||||||
|
:author "Simon Brooke"}
|
||||||
|
smeagol.sanity
|
||||||
|
(:import (java.util Locale))
|
||||||
|
(:require [clojure.java.io :as cjio]
|
||||||
|
[clojure.string :as string]
|
||||||
|
[hiccup.core :refer [html]]
|
||||||
|
[scot.weft.i18n.core :as i18n]
|
||||||
|
[smeagol.authenticate :refer [password-file-path]]
|
||||||
|
[smeagol.configuration :refer [config-file-path config]]
|
||||||
|
[smeagol.util :as util]
|
||||||
|
[taoensso.timbre :as timbre]))
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;;;;
|
||||||
|
;;;; 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) 2014 Simon Brooke
|
||||||
|
;;;;
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
|
||||||
|
;; The general idea behind the 'check-' functions in this file is that, if the
|
||||||
|
;; check passes, they return true; if it fails, they return a map of problems found.
|
||||||
|
;; the map comprises keys bound to 'explanation' lists of keywords and strings. If
|
||||||
|
;; internationalisation is available, the keywords will then be translated into
|
||||||
|
;; localised strings for presentation to the user; but if it isn't available,
|
||||||
|
;; the keywords need to be human readable. Sanity checking ought to work even
|
||||||
|
;; when the installation is quite badly broken.
|
||||||
|
|
||||||
|
(defn check-exists
|
||||||
|
"Check this `path` exists. If so, return `true`; if not, return a map
|
||||||
|
containing this `problem-key` bound to a list explaining the problem."
|
||||||
|
[path problem-key]
|
||||||
|
(if-not
|
||||||
|
(.exists (cjio/as-file path))
|
||||||
|
{problem-key (list :file-or-directory path :does-not-exist)}
|
||||||
|
true))
|
||||||
|
|
||||||
|
|
||||||
|
(defn check-is-dir
|
||||||
|
"Check this `path` is a directory. If so, return `true`; if not, return a map
|
||||||
|
containing this `problem-key` bound to a list explaining the problem."
|
||||||
|
[path problem-key]
|
||||||
|
(if-not
|
||||||
|
(.isDirectory (cjio/as-file path))
|
||||||
|
{problem-key (list :file-or-directory path :is-not-directory)}
|
||||||
|
true))
|
||||||
|
|
||||||
|
|
||||||
|
(defn check-can-write
|
||||||
|
"Check this `path` is writable. If so, return `true`; if not, return a map
|
||||||
|
containing this `problem-key` bound to a list explaining the problem."
|
||||||
|
[path problem-key]
|
||||||
|
(if-not
|
||||||
|
(.canWrite (cjio/as-file path))
|
||||||
|
{problem-key (list :file-or-directory path :is-not-writable)}
|
||||||
|
true))
|
||||||
|
|
||||||
|
|
||||||
|
(defn check-can-read
|
||||||
|
"Check this `path` is readable. If so, return `true`; if not, return a map
|
||||||
|
containing this `problem-key` bound to a list explaining the problem."
|
||||||
|
[path problem-key]
|
||||||
|
(if-not
|
||||||
|
(.canRead (cjio/as-file path))
|
||||||
|
{problem-key (list :file-or-directory path :is-not-readable)}
|
||||||
|
true))
|
||||||
|
|
||||||
|
|
||||||
|
(defn check-with-protection
|
||||||
|
"Apply this `check` to this `path` and `problem-key`; if no exception is thrown, return
|
||||||
|
the result. If an exception is thrown, return a map comprising a problem-key bound to
|
||||||
|
an explanation which includes the exception."
|
||||||
|
[check problem-key & args]
|
||||||
|
(try
|
||||||
|
(apply check args)
|
||||||
|
(catch Exception ex
|
||||||
|
{problem-key (list problem-key args ex)})))
|
||||||
|
|
||||||
|
|
||||||
|
(defn compound-check-results
|
||||||
|
[& results]
|
||||||
|
(let [problems (remove true? results)]
|
||||||
|
(if (empty? problems) true (apply merge problems))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn check-can-read-and-write
|
||||||
|
"Check this `path` is both readable and writable. If so, return `true`;
|
||||||
|
if not, return a map containing this `problem-key` bound to a list explaining
|
||||||
|
the problem."
|
||||||
|
[path problem-key]
|
||||||
|
(compound-check-results
|
||||||
|
(check-with-protection check-exists :file-or-directory path (keyword (str (name problem-key) "-exists")))
|
||||||
|
(check-with-protection check-can-read :file-or-directory path (keyword (str (name problem-key) "-can-read")))
|
||||||
|
(check-with-protection check-can-write :file-or-directory path (keyword (str (name problem-key) "-can-write")))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn check-content-dir
|
||||||
|
"Check that `path` exists and is populated as a valid content directory. Return true
|
||||||
|
if so, else a map of all problems found. If `path` is not supplied, default to the
|
||||||
|
configured content directory."
|
||||||
|
([path]
|
||||||
|
(compound-check-results
|
||||||
|
(check-with-protection check-exists :file-or-directory path :content-dir-exists)
|
||||||
|
(check-with-protection check-is-dir :file-or-directory path :content-dir-is-dir)
|
||||||
|
(check-can-read-and-write path :content-dir)
|
||||||
|
(apply compound-check-results
|
||||||
|
(map
|
||||||
|
#(check-can-read-and-write
|
||||||
|
(cjio/file path (str "_" % ".md"))
|
||||||
|
%)
|
||||||
|
["side-bar" "edit-side-bar" "header" ]))))
|
||||||
|
([]
|
||||||
|
(check-content-dir util/content-dir)))
|
||||||
|
|
||||||
|
|
||||||
|
(defn check-password-member-field
|
||||||
|
"Check that this `member` map, expected to be an entry from the passwd
|
||||||
|
file whose key was `user-key`, has this `field` and if not return a
|
||||||
|
problem explanation with this `problem-key`."
|
||||||
|
[member field user-key problem-key]
|
||||||
|
(if
|
||||||
|
(and (map? member) (member field))
|
||||||
|
true
|
||||||
|
{problem-key (list :user-lacks-field user-key field)}))
|
||||||
|
|
||||||
|
|
||||||
|
(defn check-password-member
|
||||||
|
"Check that this `member` map, expected to be an entry from the passwd
|
||||||
|
file whose key was `user-key`, has all the required fields and if not
|
||||||
|
return a problem explanation with this `problem-key`."
|
||||||
|
[member user-key problem-key]
|
||||||
|
(apply
|
||||||
|
compound-check-results
|
||||||
|
(map
|
||||||
|
#(check-password-member-field
|
||||||
|
member
|
||||||
|
%
|
||||||
|
user-key
|
||||||
|
(keyword
|
||||||
|
(string/join
|
||||||
|
"-"
|
||||||
|
(list
|
||||||
|
(name problem-key)
|
||||||
|
(name user-key)
|
||||||
|
(name %)))))
|
||||||
|
[:email :password])))
|
||||||
|
|
||||||
|
|
||||||
|
(defn check-password-members
|
||||||
|
"Check that all entries in this `passwd-content` have the required fields."
|
||||||
|
[passwd-content]
|
||||||
|
(apply
|
||||||
|
compound-check-results
|
||||||
|
(map
|
||||||
|
#(check-password-member (passwd-content %) % :missing-field)
|
||||||
|
(keys passwd-content))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn check-at-least-one-admin
|
||||||
|
"Check that there is at least one user in this `passwd-content` who has
|
||||||
|
`:admin` set to `true`."
|
||||||
|
[passwd-content]
|
||||||
|
(if
|
||||||
|
(empty?
|
||||||
|
(remove
|
||||||
|
nil?
|
||||||
|
(map
|
||||||
|
#(:admin (passwd-content %))
|
||||||
|
(keys passwd-content))))
|
||||||
|
{:no-admin-users '(:no-admin-users)}
|
||||||
|
true))
|
||||||
|
|
||||||
|
|
||||||
|
(defn check-password-file
|
||||||
|
"Check that the file at this `path` is a valid passwd file."
|
||||||
|
[path]
|
||||||
|
(let [content (read-string (slurp path))]
|
||||||
|
(compound-check-results
|
||||||
|
(check-can-read-and-write path :password-file)
|
||||||
|
(check-password-members content)
|
||||||
|
(check-at-least-one-admin content))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn check-config
|
||||||
|
"Check that the file at this `path` is a valid configuration file"
|
||||||
|
[path]
|
||||||
|
(let [content (try
|
||||||
|
(read-string (slurp path))
|
||||||
|
(catch Exception any {}))]
|
||||||
|
(compound-check-results
|
||||||
|
(check-with-protection check-exists :file-or-directory path :config-exists)
|
||||||
|
(check-with-protection check-can-read :file-or-directory path :config-can-read)
|
||||||
|
(if-not
|
||||||
|
(:site-title content)
|
||||||
|
{:site-title-not-configured :site-title-not-configured}
|
||||||
|
true)
|
||||||
|
(if-not
|
||||||
|
(:default-locale content)
|
||||||
|
{:default-locale-not-configured :default-locale-not-configured}
|
||||||
|
true))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn check-everything
|
||||||
|
([content-dir config-path passwd-path]
|
||||||
|
(compound-check-results
|
||||||
|
(check-content-dir content-dir)
|
||||||
|
(check-config config-path)
|
||||||
|
(check-password-file passwd-path)))
|
||||||
|
([]
|
||||||
|
(check-everything util/content-dir config-file-path password-file-path)))
|
||||||
|
|
||||||
|
|
||||||
|
(defn- get-causes
|
||||||
|
"Get the causes of this `error`, if it is an Exception."
|
||||||
|
[error]
|
||||||
|
(if
|
||||||
|
(instance? Exception error)
|
||||||
|
(cons error (get-causes (.getCause error)))
|
||||||
|
'()))
|
||||||
|
|
||||||
|
|
||||||
|
;; ExplanationPart is a protocol for ensuring that everything which may form part of a
|
||||||
|
;; problem explanation can be formatted into hiccup, so that it can be converted by
|
||||||
|
;; hiccup into HTML. The reason for using Hiccup rather than Selmer is that in
|
||||||
|
;; sanity check I don't want to be dependent on the existance of templates.
|
||||||
|
;; (Also, I personally like Hiccup better, although I know it's too geeky for most
|
||||||
|
;; people)
|
||||||
|
(defprotocol ExplanationPart
|
||||||
|
"things which may be parts of explanations need mechanisms for reducing
|
||||||
|
themselves to natural language where possible"
|
||||||
|
(as-hiccup [this dictionary] "Return `this` as a hiccup-formatted structure."))
|
||||||
|
|
||||||
|
(extend-protocol ExplanationPart
|
||||||
|
nil
|
||||||
|
(as-hiccup [this dictionary] "")
|
||||||
|
|
||||||
|
clojure.lang.Keyword
|
||||||
|
(as-hiccup [this dictionary] (str (or (this dictionary)(string/replace (name this) "-" " ")) " "))
|
||||||
|
|
||||||
|
clojure.lang.PersistentList
|
||||||
|
(as-hiccup [this dictionary] (apply vector (cons :div (map #(as-hiccup % dictionary) this))))
|
||||||
|
|
||||||
|
clojure.lang.PersistentVector
|
||||||
|
(as-hiccup [this dictionary] (apply vector (cons :div (map #(as-hiccup % dictionary) this))))
|
||||||
|
|
||||||
|
clojure.lang.PersistentArrayMap
|
||||||
|
(as-hiccup [this dictionary]
|
||||||
|
(apply
|
||||||
|
vector
|
||||||
|
(cons
|
||||||
|
:dl
|
||||||
|
(map
|
||||||
|
#(list [:dt (as-hiccup % dictionary)]
|
||||||
|
[:dd (as-hiccup (this %) dictionary)])
|
||||||
|
(keys this)))))
|
||||||
|
|
||||||
|
clojure.lang.PersistentHashMap
|
||||||
|
(as-hiccup [this dictionary]
|
||||||
|
(apply
|
||||||
|
vector
|
||||||
|
(cons
|
||||||
|
:dl
|
||||||
|
(map
|
||||||
|
#(list [:dt (as-hiccup % dictionary)]
|
||||||
|
[:dd (as-hiccup (this %) dictionary)])
|
||||||
|
(keys this)))))
|
||||||
|
|
||||||
|
java.lang.String
|
||||||
|
(as-hiccup [this dictionary] (str this " "))
|
||||||
|
|
||||||
|
java.lang.StackTraceElement
|
||||||
|
(as-hiccup [this dictionary]
|
||||||
|
[:li this])
|
||||||
|
|
||||||
|
java.lang.Exception
|
||||||
|
(as-hiccup [this dictionary]
|
||||||
|
;; OK, this is the interesting one
|
||||||
|
(apply
|
||||||
|
vector
|
||||||
|
(cons
|
||||||
|
:div
|
||||||
|
(cons
|
||||||
|
{:class "sanity-exception"}
|
||||||
|
(map
|
||||||
|
(fn [x]
|
||||||
|
[:div
|
||||||
|
{:class "sanity-cause"}
|
||||||
|
(.getMessage x)
|
||||||
|
[:div {:class "sanity-stacktrace"}
|
||||||
|
(apply
|
||||||
|
vector
|
||||||
|
(cons
|
||||||
|
:ol
|
||||||
|
(map
|
||||||
|
as-hiccup
|
||||||
|
(.getStackTrace x)
|
||||||
|
dictionary)))]])
|
||||||
|
(get-causes this))))))
|
||||||
|
java.lang.Object
|
||||||
|
(as-hiccup [this dictionary] (str this " ")))
|
||||||
|
|
||||||
|
|
||||||
|
(defn sanity-check-report
|
||||||
|
[problems]
|
||||||
|
(let [locale (Locale/getDefault)
|
||||||
|
locale-specifier (str (.getLanguage locale) "-" (.getCountry locale))
|
||||||
|
messages (try
|
||||||
|
(i18n/get-messages locale-specifier "i18n" "en-GB")
|
||||||
|
(catch Exception any {}))]
|
||||||
|
(html
|
||||||
|
[:html
|
||||||
|
[:head
|
||||||
|
[:title (as-hiccup :smeagol-not-initialised messages)]
|
||||||
|
[:link {:href "/content/stylesheet.css" :rel "stylesheet"}]]
|
||||||
|
[:body
|
||||||
|
[:header
|
||||||
|
[:div {:id "nav"} " "]
|
||||||
|
[:h1 (as-hiccup :smeagol-not-initialised messages)]
|
||||||
|
[:p " "]]
|
||||||
|
[:div {:id "error" :class "error"}
|
||||||
|
[:div {:class "error"}
|
||||||
|
(as-hiccup [(count (keys problems)) :problems-found] messages)]]
|
||||||
|
[:div {:id "main-container" :class "sanity-check-report"}
|
||||||
|
[:p (as-hiccup :smeagol-misconfiguration messages)]
|
||||||
|
(as-hiccup problems messages)
|
||||||
|
[:p (as-hiccup :see-documentation messages)
|
||||||
|
[:a
|
||||||
|
{:href
|
||||||
|
"https://github.com/journeyman-cc/smeagol/blob/master/resources/public/content/Deploying%20Smeagol.md"}
|
||||||
|
(as-hiccup :here messages)]]]
|
||||||
|
[:footer
|
||||||
|
[:div {:id "credits"}
|
||||||
|
[:div
|
||||||
|
[:img {:height "16" :width "16" :alt "one wiki to rule them all" :src "img/smeagol.png"}]
|
||||||
|
" One Wiki to rule them all || Smeagol wiki engine || "
|
||||||
|
[: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"]]]]]])))
|
||||||
|
|
||||||
|
|
||||||
|
(defn- raw-sanity-check-installation
|
||||||
|
"Actually do the sanity check."
|
||||||
|
[]
|
||||||
|
(timbre/info "Running sanity check")
|
||||||
|
(let [result (check-everything)]
|
||||||
|
(if
|
||||||
|
(map? result)
|
||||||
|
(do
|
||||||
|
(timbre/warn "Sanity check completed; " (count (keys result)) " problem(s) found")
|
||||||
|
(sanity-check-report result))
|
||||||
|
(do
|
||||||
|
(timbre/info "Sanity check completed; no problem(s) found")
|
||||||
|
nil))))
|
||||||
|
|
||||||
|
|
||||||
|
;; We memoise the sanity check so that although it is called for every wiki
|
||||||
|
;; page, it is only actually evaluated once.
|
||||||
|
(def sanity-check-installation (memoize raw-sanity-check-installation))
|
||||||
|
|
||||||
|
|
||||||
|
(defn show-sanity-check-error
|
||||||
|
"Generate an error page in a way which should work even when everything else is broken.
|
||||||
|
If no argument is passed, run the sanity check and if it fails return page contents;
|
||||||
|
if `error` is passed, just return page content describing the error."
|
||||||
|
([error]
|
||||||
|
(html
|
||||||
|
[:html
|
||||||
|
[:head
|
||||||
|
[:title "Smeagol is not initialised correctly"]
|
||||||
|
[:link {:href "/content/stylesheet.css" :rel "stylesheet"}]]
|
||||||
|
[:body
|
||||||
|
[:header
|
||||||
|
[:h1 "Smeagol is not initialised correctly"]]
|
||||||
|
[:div {:id "error"}
|
||||||
|
[:p {:class "error"} (.getMessage error)]]
|
||||||
|
[:p "There was a problem launching Smeagol probably because of misconfiguration:"]
|
||||||
|
(apply
|
||||||
|
vector
|
||||||
|
(cons :ol
|
||||||
|
(map #(vector :li (.getMessage %))
|
||||||
|
(get-causes error))))
|
||||||
|
[:p :see-documentation
|
||||||
|
[:a {:href "https://github.com/journeyman-cc/smeagol/blob/develop/resources/public/content/Deploying%20Smeagol.md"} "here"]]]]))
|
||||||
|
([]
|
||||||
|
(try
|
||||||
|
(sanity-check-installation)
|
||||||
|
(catch Exception any (show-sanity-check-error any)))))
|
||||||
|
|
||||||
64
src/smeagol/uploads.clj
Normal file
64
src/smeagol/uploads.clj
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
(ns ^{:doc "Handle file uploads."
|
||||||
|
:author "Simon Brooke"}
|
||||||
|
smeagol.uploads
|
||||||
|
(:import [java.io File])
|
||||||
|
(:require [clojure.string :as cs]
|
||||||
|
[noir.io :as io]
|
||||||
|
[taoensso.timbre :as timbre]))
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;;;;
|
||||||
|
;;;; 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) 2017 Simon Brooke
|
||||||
|
;;;;
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
;; No longer used as uploaded files now go into Git.
|
||||||
|
;; (defn avoid-name-collisions
|
||||||
|
;; "Find a filename within this `path`, based on this `file-name`, that does not
|
||||||
|
;; reference an existing file. It is assumed that `path` ends with a path separator.
|
||||||
|
;; Returns a filename hwich does not currently reference a file within the path."
|
||||||
|
;; [path file-name]
|
||||||
|
;; (if (.exists (File. (str path file-name)))
|
||||||
|
;; (let [parts (cs/split file-name #"\.")
|
||||||
|
;; prefix (cs/join "." (butlast parts))
|
||||||
|
;; suffix (last parts)]
|
||||||
|
;; (first
|
||||||
|
;; (filter #(not (.exists (File. (str path %))))
|
||||||
|
;; (map #(str prefix "." % "." suffix) (range)))))
|
||||||
|
;; file-name))
|
||||||
|
|
||||||
|
|
||||||
|
(defn store-upload
|
||||||
|
"Store an upload both to the file system and to the database.
|
||||||
|
The issue with storing an upload is moving it into place.
|
||||||
|
If `params` are passed as a map, it is expected that this is a map from
|
||||||
|
an HTTP POST operation of a form with type `multipart/form-data`."
|
||||||
|
[params path]
|
||||||
|
(let [upload (:upload params)
|
||||||
|
tmp-file (:tempfile upload)
|
||||||
|
filename (:filename upload)]
|
||||||
|
(timbre/info
|
||||||
|
(str "Storing upload file: " upload))
|
||||||
|
(if tmp-file
|
||||||
|
(do
|
||||||
|
(.renameTo tmp-file
|
||||||
|
(File. (str path filename)))
|
||||||
|
filename)
|
||||||
|
(throw (Exception. "No file found?")))))
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
(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 [clojure.string :as cs]
|
(:require [clojure.java.io :as cjio]
|
||||||
[cemerick.url :refer (url url-encode url-decode)]
|
[environ.core :refer [env]]
|
||||||
[noir.io :as io]
|
[noir.io :as io]
|
||||||
[noir.session :as session]
|
[noir.session :as session]
|
||||||
[markdown.core :as md]
|
[scot.weft.i18n.core :as i18n]
|
||||||
[smeagol.authenticate :as auth]))
|
[smeagol.authenticate :as auth]
|
||||||
|
[smeagol.configuration :refer [config]]
|
||||||
|
[smeagol.formatting :refer [md->html]]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;
|
;;;;
|
||||||
|
|
@ -31,23 +33,11 @@
|
||||||
;;;;
|
;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(defn md->html
|
|
||||||
"reads a markdown file from public/md and returns an HTML string"
|
|
||||||
[filename]
|
|
||||||
(md/md-to-html-string (io/slurp-resource filename)))
|
|
||||||
|
|
||||||
|
(def content-dir
|
||||||
(defn local-links
|
(or
|
||||||
"Rewrite text in `html-src` surrounded by double square brackets as a local link into this wiki."
|
(env :smeagol-content-dir)
|
||||||
[^String html-src]
|
(cjio/file (io/resource-path) "content")))
|
||||||
(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
|
(defn standard-params
|
||||||
|
|
@ -56,7 +46,35 @@
|
||||||
(let [user (session/get :user)]
|
(let [user (session/get :user)]
|
||||||
{:user user
|
{:user user
|
||||||
:admin (auth/get-admin user)
|
:admin (auth/get-admin user)
|
||||||
:side-bar (local-links (md->html "/content/_side-bar.md"))
|
:side-bar (md->html (slurp (cjio/file content-dir "_side-bar.md")))
|
||||||
:header (local-links (md->html "/content/_header.md"))
|
:header (md->html (slurp (cjio/file content-dir "_header.md")))
|
||||||
:version (System/getProperty "smeagol.version")}))
|
:version (System/getProperty "smeagol.version")}))
|
||||||
|
|
||||||
|
|
||||||
|
(defn- raw-get-messages
|
||||||
|
"Return the most acceptable messages collection we have given the
|
||||||
|
`Accept-Language` header in this `request`."
|
||||||
|
[request]
|
||||||
|
(merge
|
||||||
|
(i18n/get-messages
|
||||||
|
((:headers request) "accept-language")
|
||||||
|
"i18n"
|
||||||
|
"en-GB")
|
||||||
|
config))
|
||||||
|
|
||||||
|
|
||||||
|
(def get-messages (memoize raw-get-messages))
|
||||||
|
|
||||||
|
|
||||||
|
(defn get-message
|
||||||
|
"Return the message with this `message-key` from this `request`.
|
||||||
|
if not found, return this `default`, if provided; else return the
|
||||||
|
`message-key`."
|
||||||
|
([message-key request]
|
||||||
|
(get-message message-key message-key request))
|
||||||
|
([message-key default request]
|
||||||
|
(let [messages (get-messages request)]
|
||||||
|
(if
|
||||||
|
(map? messages)
|
||||||
|
(or (messages message-key) default)
|
||||||
|
default))))
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
(ns smeagol.test.diff2html
|
(ns smeagol.test.diff2html
|
||||||
(:use clojure.test
|
(:require [clojure.test :refer :all]
|
||||||
ring.mock.request
|
[smeagol.diff2html :refer :all]))
|
||||||
smeagol.diff2html))
|
|
||||||
|
|
||||||
(deftest test-mung-line
|
(deftest test-mung-line
|
||||||
(testing "conversion of individual lines"
|
(testing "conversion of individual lines"
|
||||||
|
|
|
||||||
12
test/smeagol/test/formatting.clj
Normal file
12
test/smeagol/test/formatting.clj
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
(ns smeagol.test.formatting
|
||||||
|
(:require [clojure.test :refer :all]
|
||||||
|
[smeagol.formatting :refer [local-links no-text-error]]))
|
||||||
|
|
||||||
|
(deftest test-local-links
|
||||||
|
(testing "Rewriting of local links"
|
||||||
|
(is (= (local-links nil) no-text-error) "Should NOT fail with a no pointer exception!")
|
||||||
|
(is (= (local-links "") "") "Empty string should pass through unchanged.")
|
||||||
|
(is (= (local-links "[[froboz]]") "<a href='wiki?page=froboz'>froboz</a>") "Local link should be rewritten.")
|
||||||
|
(let [text (str "# This is a heading"
|
||||||
|
"[This is a foreign link](http://to.somewhere)")]
|
||||||
|
(is (= (local-links text) text) "Foreign links should be unchanged"))))
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
(ns smeagol.test.handler
|
(ns smeagol.test.handler
|
||||||
(:use clojure.test
|
(:require [clojure.test :refer :all]
|
||||||
ring.mock.request
|
[ring.mock.request :refer :all]
|
||||||
smeagol.handler))
|
[smeagol.handler :refer :all]))
|
||||||
|
|
||||||
(deftest test-app
|
(deftest test-app
|
||||||
(testing "main route"
|
(testing "main route"
|
||||||
(let [response (app (request :get "/"))]
|
(let [response (app (request :get "/" {:accept-language "en-GB"}))]
|
||||||
(is (= 200 (:status response)))))
|
(is (= 200 (:status response)))))
|
||||||
|
|
||||||
(testing "not-found route"
|
(testing "not-found route"
|
||||||
(let [response (app (request :get "/invalid"))]
|
(let [response (app (request :get "/invalid" {:accept-language "en-GB"}))]
|
||||||
(is (= 404 (:status response))))))
|
(is (= 404 (:status response))))))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue