diff --git a/resources/i18n/en-GB.edn b/resources/i18n/en-GB.edn
index 6f4d8b2..442f097 100644
--- a/resources/i18n/en-GB.edn
+++ b/resources/i18n/en-GB.edn
@@ -131,6 +131,8 @@
"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
+ :sortable "You can sort this table by selecting column headers"
+ ;; used for sortable tables
: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
diff --git a/resources/public/content/stylesheet.css b/resources/public/content/stylesheet.css
index 81a661c..62690e0 100644
--- a/resources/public/content/stylesheet.css
+++ b/resources/public/content/stylesheet.css
@@ -24,6 +24,16 @@
## html elements generally in alphabetic order
*/
+a {
+ color: darkgray;
+ font-weight: bold;
+}
+
+a:hover {
+ color: darkgray;
+ background:rgba(200,200,200,0.8);
+}
+
body {
margin: 0;
padding: 0;
@@ -52,14 +62,15 @@ dd {
/* footer of the page - not-editable, provided by Smeagol */
footer {
- border-top: thin solid gray;
+ border-top: thin solid silver;
+ color: gray;
+ background:rgba(200,200,200,0.8);
clear: both;
font-size: smaller;
text-align: center;
- color: gray;
- background: rgba(224,224,224,0.95);
width: 100%;
margin: 0;
+ min-height: 4px;
padding: 0.25em 0;
bottom:0;
position:fixed;
@@ -71,9 +82,14 @@ footer {
}
footer div {
+ display: none;
padding: 0.1em;
}
+footer:hover div {
+ display: block;
+}
+
form {
border: thin solid silver;
}
@@ -83,8 +99,6 @@ header {
margin-top: 0;
width:100%;
max-width: 100%;
- background-color: gray;
- color: white;
}
header h1 {
@@ -93,7 +107,6 @@ header h1 {
header a {
font-weight: bold;
- color: white;
}
header a:hover {
@@ -131,12 +144,12 @@ ins {
label {
width: 20%;
min-width: 20em;
- border-right: thin solid gray;
+ border-right: thin solid silver;
display: inline-block;
}
table {
- border: 2px solid black;
+ border: thin solid silver;
border-collapse: collapse;
}
@@ -148,7 +161,7 @@ th, td {
text-align: left;
vertical-align: top;
padding: 0.15em 1.5em;
- border: 1px solid gray;
+ border: 1px solid silver;
}
th {
@@ -166,6 +179,7 @@ th {
/* left bar for all pages in the Wiki - editable, provided by users. Within main-container */
#side-bar {
+ display: none;
width: 17%;
height: 100%;
float: left;
@@ -173,10 +187,10 @@ th {
/* cookies information box, fixed, in right margin, just above footer */
#cookies {
- width: 30%;
+ width: 20%;
float: right;
position: fixed;
- bottom: 3.5em;
+ bottom: 8px;
right: 0;
z-index: 175;
background: transparent;
@@ -190,8 +204,8 @@ th {
text-align: right;
padding: 0.25em 2em;
border-radius: 0.25em;
- color: white;
- background:rgba(40,40,40,0.8);
+ color: gray;
+ background:rgba(200,200,200,0.8);
}
/* more-about-cookies box, normally hidden */
@@ -199,9 +213,9 @@ th {
display: none;
padding: 0.5em 2em;
border-radius: 0.5em;
- color: white;
- background:rgba(40,40,40,0.8);
- border-bottom: thin solid white;
+ color: gray;
+ background:rgba(200,200,200,0.8);
+ border-bottom: thin solid gray;
}
/* but magically appears on mouseover */
@@ -242,8 +256,8 @@ th {
right: 0;
padding: 0.25em 2em;
border-radius: 0.25em;
- color: white;
- background:rgba(40,40,40,0.8);
+ color: gray;
+ background:rgba(200,200,200,0.8);
font-size: 66%;
}
@@ -254,7 +268,11 @@ th {
.minor-controls a {
float: right;
padding: 0.25em 2em;
- color: white;
+ color: gray;
+}
+
+.minor-controls a:hover {
+ color: darkgray;
}
.pseudo-input {
@@ -303,8 +321,7 @@ th {
/* content of the current page in the Wiki - editable, provided by users. Within main-container */
#content {
border: thin solid silver;
- width: 80%;
- float: right;
+ width: 100%;
padding-bottom: 5em;
}
@@ -312,16 +329,29 @@ th {
display: none;
}
+ #header {
+ font-size: smaller;
+ }
+
/* top-of-page navigation, not editable, provided by Smeagol */
#nav{
margin: 0;
padding: 0;
top: 0;
- width: 100%;
+ min-height: 4px;
_position: absolute;
_top: expression(document.documentElement.scrollTop);
z-index: 149;
- background:rgba(40,40,40,0.8);
+ color: gray;
+ background:rgba(200,200,200,0.8);
+ }
+
+ #nav #nav-menu {
+ display: none;
+ }
+
+ #nav:hover #nav-menu {
+ display: block;
}
/* only needed for fly-out menu effect on tablet and phone stylesheets */
@@ -341,14 +371,14 @@ th {
}
#nav menu li a {
- color: white;
+ color: gray;
text-decoration: none;
font-weight: bold;
padding: 0.1em 0.75em;
margin: 0;
}
- #nav menu li.active a { background: gray;}
+ #nav menu li.active a { background: gray; color: white;}
li.nav-item a:hover { background: rgb( 240, 240, 240) }
li.nav-item a:active { background: gray; color: white; }
@@ -379,17 +409,15 @@ th {
padding: 0;
position: fixed;
z-index: 149;
- color: silver;
- background:rgba(40,40,40,0.9);
+ color: black;
+ background:rgba(200,200,200,0.9);
}
#nav a {
- color: white;
- text-decoration: none;
font-weight: bold;
}
- #nav:hover #nav-menu {
+ #nav:hover #nav-menu, #nav:hover #phone-side-bar {
display: block;
list-style-type: none;
width: 100%;
@@ -455,18 +483,21 @@ th {
display: none;
}
+ #header {
+ display: none;
+ }
+
#nav{
margin: 0;
padding: 0;
position: fixed;
z-index: 149;
- color: silver;
- background:rgba(40,40,40,0.9);
+ color: black;
+ background:rgba(200,200,200,0.9);
}
#nav a {
- color: white;
- text-decoration: none;
+ color: black;
font-weight: bold;
}
@@ -491,6 +522,8 @@ th {
}
#nav menu li a {
+ color: black;
+ font-weight: bold;
}
#nav ul li.active a { background: silver;}
diff --git a/resources/templates/edit-users.html b/resources/templates/edit-users.html
index 2621576..06d0d2e 100644
--- a/resources/templates/edit-users.html
+++ b/resources/templates/edit-users.html
@@ -1,10 +1,18 @@
{% extends "templates/base.html" %}
+{% block extra-headers %}
+ {% script "/vendor/node_modules/tablesort/dist/tablesort.min.js" %}
+{% endblock %}
{% block content %}
-
-
- | {% i18n edit-col-hdr %} | {% i18n del-col-hdr %} |
+
+ {% i18n sortable %}
+
+
+
{% endblock %}
diff --git a/resources/templates/edit.html b/resources/templates/edit.html
index 36426dc..44ee54a 100644
--- a/resources/templates/edit.html
+++ b/resources/templates/edit.html
@@ -1,11 +1,11 @@
{% extends "templates/base.html" %}
{% block extra-headers %}
- {% ifequal js-from ":cloudflare" %}
+ {% ifequal js-from ":cdnjs" %}
{% else %}
- {% style "/vendor/simplemde/dist/simplemde.min.css" %}
- {% script "/vendor/simplemde/dist/simplemde.min.js" %}
+ {% style "vendor/simplemde/dist/simplemde.min.css" %}
+ {% script "vendor/simplemde/dist/simplemde.min.js" %}
{% endifequal %}
{% endblock %}
diff --git a/resources/templates/list-uploads.html b/resources/templates/list-uploads.html
index e759dac..4289485 100644
--- a/resources/templates/list-uploads.html
+++ b/resources/templates/list-uploads.html
@@ -1,4 +1,10 @@
{% extends "templates/base.html" %}
+{% block extra-headers %}
+ {% script "/vendor/node_modules/tablesort/dist/tablesort.min.js" %}
+ {% script "/vendor/node_modules/tablesort/dist/sorts/tablesort.number.min.js" %}
+ {% script "/vendor/node_modules/tablesort/dist/sorts/tablesort.date.min.js" %}
+ {% script "/vendor/node_modules/tablesort/dist/sorts/tablesort.monthname.min.js" %}
+{% endblock %}
{% block content %}
@@ -9,12 +15,15 @@
-
-
+
+ {% i18n sortable %}
+
+
+
Name |
Uploaded |
Type this |
- To get this |
+ To get this |
{% for entry in files %}
@@ -26,9 +35,11 @@
{% if entry.is-image %} {% else %} link {% endif %}
|
-
{% endfor %}
+
{% endblock %}
diff --git a/src/smeagol/routes/wiki.clj b/src/smeagol/routes/wiki.clj
index c87dbef..104b6e0 100644
--- a/src/smeagol/routes/wiki.clj
+++ b/src/smeagol/routes/wiki.clj
@@ -161,7 +161,8 @@
(log/info (format "Showing history of page '%s'" page))
(layout/render "history.html"
(merge (util/standard-params request)
- {:title (util/get-message :history-title-prefix request)
+ {:title (str (util/get-message :history-title-prefix request)
+ " " page)
:page page
:history (hist/find-history repo-path file-name)}))))
@@ -175,7 +176,7 @@
If `template` is supplied, use that as the formatting template as specified for
java.time.Formatter. Assumes system default timezone. Returns a string."
([^Long unix-time]
- (format-instant unix-time "EEEE, dd MMMM YYYY"))
+ (format-instant unix-time "dd MMMM YYYY"))
([^Long unix-time ^String template]
(jt/format
(java-time/formatter template)
@@ -188,33 +189,26 @@
[request]
(let
[params (keywordize-keys (:params request))
- cl (count (io/resource-path))
files
- (map
- #(zipmap
- [:base-name :is-image :modified :name :resource]
- [(fs/base-name %)
- (if
- (and (fs/extension %)
- (image-extns (cs/lower-case (fs/extension %))))
- true false)
- (if
- (fs/mod-time %)
- (format-instant (fs/mod-time %)))
- (fs/name %)
- (try
- (subs (str (fs/absolute %)) cl)
- (catch StringIndexOutOfBoundsException x
- (log/error "Could not resolve relative path for" %
- ";\n resource-path is:" (io/resource-path)
- ";\n absolute path is:" (fs/absolute %)
- ";\n data-path is:" util/upload-dir
- ";\n content path is:" (:content-dir config))
- %))])
- (remove
- #(or (cs/starts-with? (fs/name %) ".")
- (fs/directory? %))
- (file-seq (clojure.java.io/file util/upload-dir))))]
+ (sort-by
+ (juxt :name (fn [x] (- 0 (count (:resource x)))))
+ (map
+ #(zipmap
+ [:base-name :is-image :modified :name :resource]
+ [(fs/base-name %)
+ (if
+ (and (fs/extension %)
+ (image-extns (cs/lower-case (fs/extension %))))
+ true false)
+ (if
+ (fs/mod-time %)
+ (format-instant (fs/mod-time %)))
+ (fs/name %)
+ (util/local-url %)])
+ (remove
+ #(or (cs/starts-with? (fs/name %) ".")
+ (fs/directory? %))
+ (file-seq (clojure.java.io/file util/upload-dir)))))]
(log/info (with-out-str (pprint files)))
(layout/render
"list-uploads.html"
@@ -236,6 +230,7 @@
files)
}))))
+
;;;; end of list-uploads section ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn upload-page
diff --git a/src/smeagol/util.clj b/src/smeagol/util.clj
index 1ef44df..d33ee32 100644
--- a/src/smeagol/util.clj
+++ b/src/smeagol/util.clj
@@ -2,6 +2,7 @@
:author "Simon Brooke"}
smeagol.util
(:require [clojure.java.io :as cjio]
+ [clojure.string :as cs]
[environ.core :refer [env]]
[me.raynes.fs :as fs]
[noir.io :as io]
@@ -10,7 +11,7 @@
[smeagol.authenticate :as auth]
[smeagol.configuration :refer [config]]
[smeagol.formatting :refer [md->html]]
- [taoensso.timbre :as timbre]))
+ [taoensso.timbre :as log]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
@@ -49,6 +50,51 @@
(def upload-dir
(str (cjio/file content-dir "uploads")))
+(def local-url-base
+ (let [a (str (fs/absolute content-dir))]
+ (subs a 0 (- (count a) (count "content")))))
+
+(defn not-servable-reason
+ "As a string, the reason this `file-path` cannot safely be served, or `nil`
+ if it is safe to serve. This reason may be logged, but should *not* be
+ shown to remote users, as it would allow file system probing."
+ [file-path]
+ (let [path (fs/absolute file-path)]
+ (cond
+ (cs/includes? file-path "..")
+ (cs/join " " file-path
+ "Attempts to ascend the file hierarchy are disallowed.")
+ (not (cs/starts-with? path local-url-base))
+ (cs/join " " [path "is not servable"])
+ (not (fs/exists? path))
+ (cs/join " " [path "does not exist"])
+ (not (fs/readable? path))
+ (cs/join " " [path "is not readable"]))))
+
+(defn local-url?
+ "True if this `file-path` can be served as a local URL, else false."
+ [file-path]
+ (empty? (not-servable-reason file-path)))
+
+(defn local-url
+ "Return a local URL for this `file-path`, or a deliberate 404 if none
+ can be safely served."
+ [file-path]
+ (try
+ (let [path (fs/absolute file-path)
+ problem (not-servable-reason path)]
+ (if
+ (empty? problem)
+ (subs (str path) (count local-url-base))
+ (do
+ (log/error
+ "In `smeagol.util/local-url `" file-path "` is not a servable resource.")
+ (str "404-not-found?path=" file-path))))
+ (catch Exception any
+ (log/error
+ "In `smeagol.util/local-url `" file-path "` is not a servable resource:" any)
+ (str "404-not-found?path=" file-path))))
+
(defn standard-params
"Return a map of standard parameters to pass to the template renderer."
[request]
@@ -69,7 +115,7 @@
messages (try
(i18n/get-messages specifier "i18n" "en-GB")
(catch Exception any
- (timbre/error
+ (log/error
any
(str
"Failed to parse accept-language header '"