Browse Source

clojure notehub removed; let's start from scratch

master
Christian Müller 11 years ago
parent
commit
6df0d7990d
  1. 13
      .gitignore
  2. 1
      Procfile
  3. 26
      project.clj
  4. 10
      scripts/expose.sh
  5. 9
      scripts/run.sh
  6. 126
      src/notehub/api.clj
  7. 260
      src/notehub/css.clj
  8. 169
      src/notehub/handler.clj
  9. 147
      src/notehub/storage.clj
  10. 124
      src/notehub/views.clj
  11. 1
      system.properties
  12. 214
      test/notehub/test/api.clj
  13. 129
      test/notehub/test/handler.clj
  14. 70
      test/notehub/test/storage.clj

13
.gitignore vendored

@ -1,14 +1 @@
Makefile
PrivateMakefile
dump.rdb dump.rdb
resources/public/cljs
pom.xml
*jar
/lib/
/classes/
.lein-*
.crossover-cljs
target/
.nrepl-port
.idea/
NoteHub.iml

1
Procfile

@ -1 +0,0 @@
web: lein with-profile production ring server-headless $PORT

26
project.clj

@ -1,26 +0,0 @@
(defproject NoteHub "2.0.0"
:description "A free and anonymous hosting for markdown pages."
:dependencies [[org.clojure/clojure "1.6.0"]
[org.clojure/core.cache "0.6.4"]
[hiccup "1.0.5"]
[zeus "0.1.0"]
[garden "1.2.5"]
[org.pegdown/pegdown "1.4.2"]
[iokv "0.1.1"]
[cheshire "5.3.1"]
[ring "1.3.1"]
[com.taoensso/carmine "2.7.0" :exclusions [org.clojure/clojure]]
[compojure "1.2.0"]]
:main notehub.handler
:min-lein-version "2.0.0"
:plugins [[lein-ring "0.8.12"]]
:ring {:handler notehub.handler/app}
:profiles {:uberjar {:aot :all}
:production {:ring {:auto-reload? false
:auto-refresh? false}}
:dev {:ring {:auto-reload? true
:auto-refresh? true}
:dependencies
[[javax.servlet/servlet-api "2.5"]
[ring-mock "0.1.5"]]}}
:jvm-opts ["-Dfile.encoding=utf-8"])

10
scripts/expose.sh

@ -1,10 +0,0 @@
#!/bin/bash
unset DEVMODE
if ! pgrep "redis-server" > /dev/null; then
redis-server &
fi
lein uberjar
java -jar target/NoteHub-2.0.0-standalone.jar

9
scripts/run.sh

@ -1,9 +0,0 @@
#!/bin/bash
export DEVMODE=1
if ! pgrep "redis-server" > /dev/null; then
redis-server &
fi
lein ring server-headless 8080

126
src/notehub/api.clj

@ -1,126 +0,0 @@
(ns notehub.api
(:import
[java.util Calendar])
(:use
[iokv.core]
[ring.util.codec :only [url-encode]]
[clojure.string :rename {replace sreplace}
:only [replace blank? trim lower-case split-lines split]])
(:require
[ring.util.codec]
[hiccup.util :as util]
[notehub.storage :as storage]))
(def version "1.4")
(defn log
"Logs args to the server stdout"
[string & args]
(apply printf (str "%s:" string) (str (storage/get-current-date) ":LOG") args)
(println))
(defn url
"Creates a local url from the given substrings"
[& args]
(apply str (interpose "/" (cons "" (map url-encode args)))))
; Concatenates all fields to a string
(defn build-key
"Returns a storage-key for the given note coordinates"
[year month day title]
(apply str (interpose "/" [year month day title])))
(defn derive-title [md-text]
(sreplace (first (split-lines md-text))
#"(#+|_|\*+|<.*?>)" ""))
(defn get-date
"Returns today's date"
[]
(map #(+ (second %) (.get (Calendar/getInstance) (first %)))
{Calendar/YEAR 0, Calendar/MONTH 1, Calendar/DAY_OF_MONTH 0}))
(defn- create-response
([success] {:success success})
([success message & params]
(assoc (create-response success) :message (apply format message params))))
(defn- get-path [host-url token & [description]]
(if (= :url description)
(str host-url "/" token)
(let [[year month day title] (split token #"/")]
(if description
(str host-url "/" (storage/create-short-url token {:year year :month month :day day :title title}))
(str host-url (url year month day title))))))
(defn version-manager [f params]
(if-let [req-version (:version params)]
(let [req-version (Double/parseDouble req-version)
version (Double/parseDouble version)]
(if (< req-version version)
{:status (create-response false "Deprecated API version")}
(f params)))
{:status (create-response false "API version expected")}))
(defn get-note [{:keys [noteID hostURL]}]
(if (storage/note-exists? noteID)
(let [note (storage/get-note noteID)]
(storage/increment-note-view noteID)
{:note note
:title (derive-title note)
:longURL (get-path hostURL noteID)
:shortURL (get-path hostURL noteID :id)
:statistics (storage/get-note-statistics noteID)
:status (create-response true)
:publisher (storage/get-publisher noteID)})
{:status (create-response false "noteID '%s' unknown" noteID)}))
(defn propose-title [note]
(let [raw-title (filter #(or (= \- %) (Character/isLetterOrDigit %))
(-> note derive-title trim (sreplace " " "-") lower-case))
max-length (get-setting :max-title-length #(Integer/parseInt %) 80)]
(apply str (take max-length raw-title))))
(defn post-note
[{:keys [note pid signature password hostURL] :as params}]
;(log "post-note: %s" {:pid pid :signature signature :password password :note note})
(let [errors (filter identity
[(when-not (storage/valid-publisher? pid) "pid invalid")
(when-not (= signature (storage/sign pid (storage/get-psk pid) note))
"signature invalid")
(when (blank? note) "note is empty")])]
(if (empty? errors)
(let [[year month day] (map str (get-date))
params (select-keys params [:text-size :header-size :text-font :header-font :theme])
proposed-title (propose-title note)
title (first (drop-while #(storage/note-exists? (build-key year month day %))
(cons proposed-title
(map #(str proposed-title "-" (+ 2 %)) (range)))))
noteID (build-key year month day title)
new-params (assoc params :year year :month month :day day :title title)
short-url (get-path hostURL (storage/create-short-url noteID new-params) :url)
long-url (get-path hostURL noteID)]
(do
(storage/add-note noteID note pid password)
{:noteID noteID
:longURL (if (empty? params) long-url (str (util/url long-url params)))
:shortURL short-url
:status (create-response true)}))
{:status (create-response false (first errors))})))
(defn update-note [{:keys [noteID note pid signature password hostURL]}]
;(log "update-note: %s" {:pid pid :noteID noteID :signature signature :password password :note note})
(let [errors (filter identity
[(when-not (storage/valid-publisher? pid) "pid invalid")
(when-not (= signature (storage/sign pid (storage/get-psk pid) noteID note password))
"signature invalid")
(when (blank? note) "note is empty")
(when-not (storage/valid-password? noteID password) "password invalid")])]
(if (empty? errors)
(do
(storage/edit-note noteID note)
{:longURL (get-path hostURL noteID)
:shortURL (get-path hostURL noteID :id)
:status (create-response true)})
{:status (create-response false (first errors))})))

260
src/notehub/css.clj

@ -1,260 +0,0 @@
(ns notehub.css
(:require [garden.core :refer [css]]
[garden.stylesheet :refer [at-media]]
[garden.units :as u :refer [px pt em]]))
(def themes
{
"dark"
{
:background {
:normal "#333",
:halftone "#444"
},
:foreground {
:normal "#ccc",
:halftone "#bbb"
},
:link {
:fresh "#6b8",
:visited "#496",
:hover "#7c9"
}
},
"solarized-light"
{
:background {
:normal "#fdf6e3",
:halftone "#eee8d5"
},
:foreground {
:normal "#657b83",
:halftone "#839496"
},
:link {
:fresh "#b58900",
:visited "#cb4b16",
:hover "#dc322f"
}
},
"solarized-dark"
{
:background {
:normal "#073642",
:halftone "#002b36"
},
:foreground {
:normal "#93a1a1",
:halftone "#839191"
},
:link {
:fresh "#cb4b16",
:visited "#b58900",
:hover "#dc322f"
}
},
"default"
{
:background {
:normal "#fff",
:halftone "#efefef"
},
:foreground {
:normal "#333",
:halftone "#888"
},
:link {
:fresh "#097",
:visited "#054",
:hover "#0a8"
}
}
}
)
(defn generate [params]
(let [theme (themes (params "theme" "default"))
; VARIABLES
background (get-in theme [:background :normal])
foreground (get-in theme [:foreground :normal])
background-halftone (get-in theme [:background :halftone])
foreground-halftone (get-in theme [:foreground :halftone])
link-fresh (get-in theme [:link :fresh])
link-visited (get-in theme [:link :visited])
link-hover (get-in theme [:link :hover])
width (px 800)
header-font (or (params "header-font") "Noticia Text")
text-font (or (params "text-font") "Georgia")
header-size-factor (Float/parseFloat (or (params "header-size") "1"))
text-size-factor (Float/parseFloat (or (params "text-size") "1"))
; MIXINS
helvetica {
:font-weight 300
:font-family "'Helvetica Neue','Helvetica','Arial','Lucida Grande','sans-serif'"
}
central-element {
:margin-left "auto"
:margin-right "auto"
}
thin-border {
:border (print-str "1px solid" foreground)
}]
(css
[:.ui-border { :border-radius (px 3) } thin-border]
[:a {
:color link-fresh
:text-decoration "none"
:border-bottom "1px dotted"
}]
[:a:hover { :color link-hover }]
[:a:visited { :color link-visited }]
[:#draft {
:margin-bottom (em 3)
}]
[:.button {
:cursor "pointer"
}]
[:.ui-elem {
:border-radius (px 3)
:padding (em 0.3)
:opacity 0.8
:font-size (em 1)
:background background
}
helvetica thin-border]
[:.landing-button, :textarea, :fieldset { :border "none" }]
[:.landing-button {
:box-shadow "0 2px 5px #aaa"
:text-decoration "none"
:font-size (em 1.5)
:background "#0a2"
:border-radius (px 10)
:padding (px 10)
}
helvetica]
[:.landing-button:hover { :background "#0b2" }]
[:.helvetica helvetica]
[:#footer {
:width "100%"
:font-size (em 0.8)
:padding-bottom (em 1)
:text-align "center"
}
helvetica]
(at-media {:screen true :max-width (px 767)} [:#footer {:font-size (em 0.4)}])
["#footer a" { :border "none" }]
[:html, :body {
:background background
:color foreground
:margin 0
:padding 0
}]
[:#hero {
:padding-top (em 5)
:padding-bottom (em 5)
:text-align "center"
}]
[:h1, :h2, :h3, :h4, :h5, :h6 {
:font-weight "bold"
:font-family (str header-font ",'Noticia Text','PT Serif','Georgia'")
}]
[:h1 { :font-size (em (* 1.8 header-size-factor)) }]
[:h2 { :font-size (em (* 1.6 header-size-factor)) }]
[:h3 { :font-size (em (* 1.4 header-size-factor)) }]
[:h4 { :font-size (em (* 1.2 header-size-factor)) }]
[:h5 { :font-size (em (* 1.1 header-size-factor)) }]
[:h6 { :font-size (em (* 1 header-size-factor)) }]
["#hero h1" { :font-size (em 2.5) }]
["#hero h2" { :margin (em 2) } helvetica ]
[:article {
:font-family (str text-font ", 'Georgia'")
:margin-top (em 5)
:text-align "justify"
:flex 1
:-webkit-flex 1
}
central-element]
(at-media {:screen true :min-width (px 1024)} [:article {:width width}])
(at-media {:screen true :max-width (px 1023)} [:article {:width "90%"}])
[:.central-element central-element]
(at-media {:screen true :min-width (px 1024)} [:.central-element {:width width}])
(at-media {:screen true :max-width (px 1023)} [:.central-element {:width "90%"}])
["article img" { :max-width "100%" }]
["article p" {
:font-size (em (* 1.2 text-size-factor))
:line-height "140%"
}]
["article > h1:first-child" {
:text-align "center"
:font-size (em (* 2 header-size-factor))
:margin (em 2)
}]
[:.centered { :text-align "center" }]
[:.bottom-space { :margin-bottom (em 7) }]
[:code, :pre {
:font-family "monospace"
:background background-halftone
:font-size (em (* 1.2 text-size-factor))
}]
[:pre {
:border-radius (px 3)
:padding (em 0.5)
:border (str "1px dotted" foreground-halftone)
}]
["*:focus" { :outline "0px none transparent" }]
(at-media {:screen true :min-width (px 1024)} [:textarea {:width width}])
[:textarea {
:border-radius (px 5)
:font-family "Courier"
:font-size (em 1)
:height (px 500)
}]
[:.hidden { :display "none" }]
[:#dashed-line {
:border-bottom (str "1px dashed" foreground-halftone)
:margin-top (em 3)
:margin-bottom (em 3)
}]
[:table {
:width "100%"
:border-collapse "collapse"
}
helvetica]
[:th {
:padding (em 0.3)
:line-height (em 2.5)
:background-color background-halftone
}]
[:td {
:border-top (str "1px dotted" foreground-halftone)
:padding (em 0.3)
:line-height (em 2.5)
}]
[:.middot { :padding (em 0.5) }]
[:body { :display "-webkit-flex" }]
[:body {
:display "flex"
:min-height "100vh"
:flex-direction "column"
:-webkit-flex-direction "column"
}]
)))

169
src/notehub/handler.clj

@ -1,169 +0,0 @@
(ns notehub.handler
(:use
compojure.core
iokv.core
notehub.views
[clojure.string :rename {replace sreplace} :only [replace]])
(:require
[ring.adapter.jetty :as jetty]
[clojure.core.cache :as cache]
[hiccup.util :as util]
[compojure.handler :as handler]
[compojure.route :as route]
[notehub.api :as api]
[notehub.storage :as storage]
[cheshire.core :refer :all])
(:gen-class))
(defn current-timestamp []
(quot (System/currentTimeMillis) 100000000))
; note page cache
(def page-cache (atom (cache/lru-cache-factory {})))
; TODO: make sure the status is really set to the response!!!!
(defn- response
"Sets a custom message for each needed HTTP status.
The message to be assigned is extracted with a dynamically generated key"
[code]
{:status code
:body (let [message (get-message (keyword (str "status-" code)))]
(layout :no-js {} message
[:article [:h1 message]]))})
(defn redirect [url]
{:status 302
:headers {"Location" (str url)}
:body ""})
(defn return-content-type [ctype content]
{:headers {"Content-Type" ctype}
:body content})
(defroutes api-routes
(GET "/note" {params :params}
(generate-string (api/version-manager api/get-note params)))
(POST "/note" {params :params}
(generate-string (api/version-manager api/post-note params)))
(PUT "/note" {params :params}
(generate-string (api/version-manager api/update-note params))))
(defroutes app-routes
(GET "/api" [] (layout :no-js {} (get-message :api-title)
[:article (md-to-html (slurp "API.md"))]))
(context "/api" []
#(ring.util.response/content-type (api-routes %) "application/json"))
(GET "/" [] landing-page)
(POST "/propose-title" {body :body}
(let [note (slurp body)]
(return-content-type
"text/plain; charset=utf-8"
(api/propose-title note))))
(GET "/:year/:month/:day/:title/export" [year month day title]
(when-let [md-text (:note (api/get-note {:noteID (api/build-key year month day title)}))]
(return-content-type "text/plain; charset=utf-8" md-text)))
(GET "/:year/:month/:day/:title/stats" [year month day title]
(let [note-id (api/build-key year month day title)]
(statistics-page (api/derive-title (storage/get-note note-id))
(storage/get-note-statistics note-id)
(storage/get-publisher note-id))))
(GET "/:year/:month/:day/:title/edit" [year month day title]
(let [note-id (api/build-key year month day title)]
(note-update-page
note-id
(:note (api/get-note {:noteID note-id})))))
(GET "/new" [] (new-note-page
(str
(current-timestamp)
(storage/sign (rand-int Integer/MAX_VALUE)))))
(GET "/:year/:month/:day/:title" [year month day title :as params]
(let [params (assoc (:query-params params)
:year year :month month :day day :title title)
note-id (api/build-key year month day title)
short-url (storage/create-short-url note-id params)]
(when (storage/note-exists? note-id)
(if (cache/has? @page-cache short-url)
(do
(swap! page-cache cache/hit short-url)
(storage/increment-note-view note-id))
(swap! page-cache cache/miss short-url
(note-page (api/get-note {:noteID note-id})
(api/url short-url)
params)))
(cache/lookup @page-cache short-url))))
(GET "/:short-url" [short-url]
(when-let [params (storage/resolve-url short-url)]
(let [{:keys [year month day title]} params
rest-params (dissoc params :year :month :day :title)
core-url (api/url year month day title)
long-url (if (empty? rest-params) core-url (util/url core-url rest-params))]
(redirect long-url))))
(POST "/post-note" [session note signature password]
(if (and session
(.startsWith session
(str (current-timestamp)))
(= signature (storage/sign session note)))
(let [pid "NoteHub"
psk (storage/get-psk pid)
params {:note note
:pid pid
:signature (storage/sign pid psk note)
:password password}]
(if (storage/valid-publisher? pid)
(let [resp (api/post-note params)]
(if (get-in resp [:status :success])
(redirect (:longURL resp))
(response 400)))
(response 500)))
(response 400)))
(POST "/update-note" [noteID note password]
(let [pid "NoteHub"
psk (storage/get-psk pid)
params {:noteID noteID :note note :password password :pid pid}]
(if (storage/valid-publisher? pid)
(let [resp (api/update-note (assoc params
:signature
(storage/sign pid psk noteID note password)))]
(if (get-in resp [:status :success])
(do
(doseq [url (storage/get-short-urls noteID)]
(swap! page-cache cache/evict url))
(redirect (:longURL resp)))
(response 403)))
(response 500))))
(route/resources "/")
(route/not-found (response 404)))
(def app
(let [handler (handler/site app-routes)]
(fn [request]
(let [{:keys [server-name server-port]} request
hostURL (str "https://" server-name
(when (not= 80 server-port) (str ":" server-port)))
request (assoc-in request [:params :hostURL] hostURL)]
(if (get-setting :dev-mode)
(handler request)
(try (handler request)
(catch Exception e
(do
;TODO (log e)
(response 500)))))))))
(defn -main [& [port]]
(jetty/run-jetty #'app
{:port (if port (Integer/parseInt port) 8080)}))

147
src/notehub/storage.clj

@ -1,147 +0,0 @@
(ns notehub.storage
(:use [iokv.core]
[zeus.core]
[clojure.string :only (blank? replace) :rename {replace sreplace}])
(:require [taoensso.carmine :as car :refer (wcar)]))
(def conn {:pool {} :spec {:uri (get-setting :db-url)}})
(let [md5Instance (java.security.MessageDigest/getInstance "MD5")]
(defn sign
"Returns the MD5 hash for the concatenation of all passed parameters"
[& args]
(let [input (sreplace (apply str args) #"\r+" "")]
(do (.reset md5Instance)
(.update md5Instance (.getBytes input))
(apply str
(map #(let [c (Integer/toHexString (bit-and 0xff %))]
(if (= 1 (count c)) (str "0" c) c))
(.digest md5Instance)))))))
(defmacro redis [cmd & body]
`(car/wcar conn
(~(symbol "car" (name cmd))
~@body)))
(defn get-current-date []
(.getTime (java.util.Date.)))
(defn valid-publisher? [pid]
(= 1 (redis :hexists :publisher-key pid)))
(defn register-publisher [pid]
"Returns nil if given PID exists or a PSK otherwise"
(when (not (valid-publisher? pid))
(let [psk (sign (str (rand-int Integer/MAX_VALUE) pid))]
(redis :hset :publisher-key pid psk)
psk)))
(when (and (get-setting :dev-mode)
(not (valid-publisher? "NoteHub"))
(register-publisher "NoteHub")))
(defn revoke-publisher [pid]
(redis :hdel :publisher-key pid))
(defn get-psk [pid]
(redis :hget :publisher-key pid))
(defn edit-note [noteID text]
(redis :hset :edited noteID (get-current-date))
(redis :hset :note noteID (zip text)))
(defn add-note
([noteID text pid] (add-note noteID text pid nil))
([noteID text pid passwd]
(redis :hset :note noteID (zip text))
(redis :hset :published noteID (get-current-date))
(redis :hset :publisher noteID pid)
(when (not (blank? passwd))
(redis :hset :password noteID passwd))))
(defn valid-password? [noteID passwd]
(let [stored (redis :hget :password noteID)]
(and (not= 0 stored) (= stored passwd))))
(defn get-note-views [noteID]
(redis :hget :views noteID))
(defn get-publisher [noteID]
(redis :hget :publisher noteID))
(defn get-note-statistics [noteID]
{:views (get-note-views noteID)
:published (redis :hget :published noteID)
:edited (redis :hget :edited noteID)})
(defn note-exists? [noteID]
(= 1 (redis :hexists :note noteID)))
(defn get-note [noteID]
(when (note-exists? noteID)
(unzip (redis :hget :note noteID))))
(defn increment-note-view [noteID]
(redis :hincrby :views noteID 1))
(defn short-url-exists? [url]
(= 1 (redis :hexists :short-url url)))
(defn resolve-url [url]
(let [value (redis :hget :short-url url)]
(when value ; TODO: necessary?
(read-string value))))
(defn delete-short-url [url]
(when-let [params (redis :hget :short-url url)]
(redis :hdel :short-url params)
(redis :hdel :short-url url)))
(defn get-short-urls [noteID]
(redis :smembers (str noteID :urls)))
(defn delete-note [noteID]
(doseq [kw [:password :views :note :published :edited :publisher]]
(redis :hdel kw noteID))
(doseq [url (get-short-urls noteID)]
(delete-short-url url))
(redis :del (str noteID :urls)))
(defn create-short-url
"Creates a short url for the given request metadata or extracts
one if it was already created"
[noteID params]
(let [key (str (into (sorted-map) (clojure.walk/keywordize-keys params)))]
(if (short-url-exists? key)
(redis :hget :short-url key)
(let [hash-stream (partition 5 (repeatedly #(rand-int 36)))
hash-to-string (fn [hash]
(apply str
; map first 10 numbers to digits
; and the rest to chars
(map #(char (+ (if (< 9 %) 87 48) %)) hash)))
url (first
(remove short-url-exists?
(map hash-to-string hash-stream)))]
; we create two mappings: key params -> short url and back,
; s.t. we can later easily check whether a short url already exists
(redis :hset :short-url url key)
(redis :hset :short-url key url)
; we save all short urls of a note for removal later
(redis :sadd (str noteID :urls) url)
url))))
(defn gc [password dry]
(println (get-setting :admin-pw))
(when (= password (get-setting :admin-pw))
(let [N 30
timestamp (- (get-current-date) (* N 24 60 60 1000))
all-notes (map first (partition 2 (redis :hgetall :note)))
old-notes (filter #(< (Long/parseLong (redis :hget :published %)) timestamp) all-notes)
unpopular-notes (filter #(< (try (Long/parseLong (redis :hget :views %))
(catch Exception a 0)) N) old-notes)]
(println "timestamp:" (str (java.util.Date. timestamp)))
(doseq [note-id unpopular-notes]
(do (println (if dry "to be deleted" "deleting") note-id)
(when-not dry (delete-note note-id))))
(println (count unpopular-notes) "deleted"))))

124
src/notehub/views.clj

@ -1,124 +0,0 @@
(ns notehub.views
(:use
iokv.core
[clojure.string :rename {replace sreplace} :only [replace]]
[hiccup.form]
[hiccup.core]
[hiccup.element]
[hiccup.util :only [escape-html]]
[hiccup.page :only [include-js html5]])
(:require [notehub.css :as css])
(:import (org.pegdown PegDownProcessor Extensions)))
(def get-message (get-map "messages"))
(def md-processor
(PegDownProcessor. (int (bit-and-not Extensions/ALL Extensions/HARDWRAPS))))
(defn md-to-html [md-text]
(.markdownToHtml md-processor md-text))
; Creates the main html layout
(defn layout
[js? style title & content]
(html5
[:head
[:title (print-str (get-message :name) "&mdash;" title)]
[:meta {:charset "UTF-8"}]
[:meta {:name "viewport" :content "width=device-width, initial-scale=1.0"}]
[:link {:rel "stylesheet" :type "text/css"
:href
(format "https://fonts.googleapis.com/css?family=PT+Serif:700|Noticia+Text:700%s&subset=latin,cyrillic"
(reduce (fn [acc e]
(if-let [font (style e)]
(str acc "|" (sreplace font #" " "+"))
acc)) "" ["text-font" "header-font"]))}]
[:style (css/generate style)]
(if (= :js js?)
(html
(include-js "//cdnjs.cloudflare.com/ajax/libs/marked/0.3.2/marked.min.js")
(include-js "//cdnjs.cloudflare.com/ajax/libs/blueimp-md5/1.0.1/js/md5.min.js")
(include-js "/js/publishing.js")
[:body {:onload "onLoad()"} content])
[:body content])]))
(defn- sanitize
"Breakes all usages of <script> & <iframe>"
[input]
(sreplace input #"(</?(iframe|script).*?>|javascript:)" ""))
; input form for the markdown text with a preview area
(defn- input-form [form-url command fields content passwd-msg]
(let [css-class (when (= :publish command) :hidden)]
(layout :js {} (get-message :new-page)
[:article#preview {:style "flex: none; -webkit-flex: none"} ""]
[:div#dashed-line {:class css-class}]
[:div.central-element.helvetica {:style "margin-bottom: 3em"}
(form-to {:autocomplete :off} [:post form-url]
(hidden-field :action command)
(hidden-field :password)
fields
(text-area :note content)
[:fieldset#input-elems {:class css-class}
(text-field {:class "ui-elem"
:placeholder (get-message passwd-msg)}
:plain-password) "&nbsp"
(submit-button {:class "button ui-elem"
:id :publish-button} (get-message command))
[:br]
[:br]
[:div#proposed-title {:style "color: #aaa; font-size: 0.8em"}]])])))
(def landing-page
(layout :no-js {} (get-message :page-title)
[:div#hero
[:h1 (get-message :name)]
[:h2 (get-message :title)]
[:br]
[:a.landing-button {:href "/new" :style "color: white"} (get-message :new-page)]]
[:div#dashed-line]
[:article.helvetica.bottom-space
{:style "font-size: 1em"}
(md-to-html (slurp "LANDING.md"))]
[:div#footer (md-to-html (get-message :footer))]))
(defn statistics-page [note-title stats publisher]
(let [page-title (get-message :statistics)
info (assoc stats :publisher publisher)]
(layout :no-js {} page-title
[:div.central-element.helvetica {:style "width: 80%"}
[:h2 note-title]
[:h3.helvetica page-title]
[:table#stats
(map
#(when-let [v (% info)]
[:tr
[:td (str (get-message %) ":")]
[:td (if (or (= % :published) (= % :edited))
(str (java.util.Date. (Long/parseLong v))) v)]])
[:published :edited :publisher :views])]])))
(defn note-update-page [note-id note]
(input-form "/update-note"
:update
(html (hidden-field :noteID note-id))
note
:enter-passwd))
(defn new-note-page [session]
(input-form "/post-note" :publish
(html (hidden-field :session session)
(hidden-field {:id :signature} :signature))
(get-message :loading) :set-passwd))
(defn note-page [note short-url css-params]
(layout :no-js css-params (:title note)
[:article.bottom-space (md-to-html (sanitize (:note note)))]
(let [urls {:short-url short-url
:notehub "/"}
links (map #(link-to
(urls % (str (:longURL note) "/" (name %)))
(get-message %))
[:notehub :stats :edit :export :short-url])]
[:div#footer (interpose [:span.middot "&middot;"] links)])))

1
system.properties

@ -1 +0,0 @@
java.runtime.version=1.8

214
test/notehub/test/api.clj

@ -1,214 +0,0 @@
(ns notehub.test.api
(:require
[cheshire.core :refer :all]
[notehub.storage :as storage])
(:use [notehub.api]
[notehub.handler]
[clojure.test]))
(def note "hello world!\nThis is a _test_ note!")
(def pid "somePlugin")
(def pid2 "somePlugin2")
(def note-title (let [[y m d] (get-date)]
(apply str (interpose "/" [y m d "hello-world"]))))
(def note-url (str (apply str "/" (interpose "/" (get-date))) "/hello-world"))
(defn substring? [a b] (not (= nil (re-matches (re-pattern (str "(?s).*" a ".*")) b))))
(defmacro isnt [arg] `(is (not ~arg)))
(defn send-request
([resource] (send-request resource {}))
([resource params]
(let [[method url] (if (vector? resource) resource [:get resource])]
(app-routes {:request-method method :uri url :params params}))))
(defn has-status [input status]
(= status (:status input)))
(defn register-publisher-fixture [f]
(def psk (storage/register-publisher pid))
(f)
(storage/revoke-publisher pid)
(storage/revoke-publisher pid2)
(storage/delete-note note-title))
(use-fixtures :each register-publisher-fixture)
(deftest api
(testing "API"
(testing "publisher registration"
(is (storage/valid-publisher? pid))
(isnt (storage/valid-publisher? pid2))
(let [psk2 (storage/register-publisher pid2)]
(is (= psk2 (storage/get-psk pid2)))
(is (storage/valid-publisher? pid2))
(is (storage/revoke-publisher pid2))
(isnt (storage/valid-publisher? "any_PID"))
(isnt (storage/valid-publisher? pid2))))
(testing "note publishing & retrieval"
(isnt (:success (:status (get-note {:noteID "some note id"}))))
(is (= "note is empty" (:message (:status (post-note {:note "" :pid pid :signature (storage/sign pid psk "")})))))
(let [post-response (post-note {:note note :pid pid :signature (storage/sign pid psk note)})
get-response (get-note post-response)]
(is (:success (:status post-response)))
(is (:success (:status get-response)))
(is (= note (:note get-response)))
(is (= (:longURL post-response) (:longURL get-response) note-url))
(is (= (:shortURL post-response) (:shortURL get-response)))
(is (storage/note-exists? (:noteID post-response)))
(let [su (last (clojure.string/split (:shortURL get-response) #"/"))]
(is (= su (storage/create-short-url (:noteID post-response) (storage/resolve-url su)))))
(let [resp (send-request (:shortURL get-response))
resp (send-request ((:headers resp) "Location"))]
(is (substring? "hello world"(:body resp))))
(is (= (:publisher get-response) pid))
(is (= (:title get-response) (derive-title note)))
(is (= "1" (get-in get-response [:statistics :views])))
(isnt (get-in get-response [:statistics :edited]))
(is (= "noteID 'randomString' unknown"
(get-in
(parse-string
(:body (send-request "/api/note" {:version "1.4" :noteID "randomString"})))
["status" "message"])))
(is (= "3" (get-in (get-note post-response) [:statistics :views])))))
(testing "creation with wrong signature"
(let [response (post-note {:note note :pid pid :signature (storage/sign pid2 psk note)})]
(isnt (:success (:status response)))
(is (= "signature invalid" (:message (:status response)))))
(let [response (post-note {:note note :pid pid :signature (storage/sign pid2 psk "any note")})]
(isnt (:success (:status response)))
(is (= "signature invalid" (:message (:status response)))))
(isnt (:success (:status (post-note {:note note :pid pid :signature (storage/sign pid "random_psk" note)}))))
(is (:success (:status (post-note {:note note :pid pid :signature (storage/sign pid psk note)}))))
(let [randomPID "randomPID"
psk2 (storage/register-publisher randomPID)
_ (storage/revoke-publisher randomPID)
response (post-note {:note note :pid randomPID :signature (storage/sign randomPID psk2 note)})]
(isnt (:success (:status response)))
(is (= (:message (:status response)) "pid invalid"))))
(testing "note update"
(let [post-response (post-note {:note note :pid pid :signature (storage/sign pid psk note) :password "passwd"})
note-id (:noteID post-response)
new-note "a new note!"]
(is (:success (:status post-response)))
(is (:success (:status (get-note {:noteID note-id}))))
(is (= note (:note (get-note {:noteID note-id}))))
(let [update-response (update-note {:noteID note-id :note new-note :pid pid
:signature (storage/sign pid psk new-note) :password "passwd"})]
(isnt (:success (:status update-response)))
(is (= "signature invalid" (:message (:status update-response)))))
(is (= note (:note (get-note {:noteID note-id}))))
(let [update-response (update-note {:noteID note-id :note new-note :pid pid
:signature (storage/sign pid psk note-id new-note "passwd")
:password "passwd"})]
(is (= { :success true } (:status update-response)))
(isnt (= nil (get-in (get-note {:noteID note-id}) [:statistics :edited])))
(is (= new-note (:note (get-note {:noteID note-id})))))
(let [update-response (update-note {:noteID note-id :note "aaa" :pid pid
:signature (storage/sign pid psk note-id "aaa" "pass")
:password "pass"})]
(isnt (:success (:status update-response)))
(is (= "password invalid" (:message (:status update-response)))))
(is (= new-note (:note (get-note {:noteID note-id}))))
(is (= new-note (:note (get-note {:noteID note-id}))))))))
(deftest api-note-creation
(testing "Note creation"
(let [response (send-request [:post "/api/note"]
{:note note
:pid pid
:signature (storage/sign pid psk note)
:version "1.4"})
body (parse-string (:body response))
noteID (body "noteID")]
(is (has-status response 200))
(is (get-in body ["status" "success"]))
(is (= note ((parse-string
(:body (send-request [:get "/api/note"] {:version "1.4" :noteID noteID}))) "note")))
(is (= "Deprecated API version" (get-in (parse-string
(:body (send-request [:get "/api/note"] {:version "1.3" :noteID noteID}))) ["status" "message"])))
(is (= "API version expected" (get-in (parse-string
(:body (send-request [:get "/api/note"] {:noteID noteID}))) ["status" "message"])))
(is (= note ((parse-string
(:body (send-request [:get "/api/note"] {:version "1.4" :noteID noteID}))) "note")))
(isnt (= note ((parse-string
(:body (send-request [:get "/api/note"] {:version "1.3" :noteID noteID}))) "note")))
(is (do
(storage/delete-note noteID)
(not (storage/note-exists? noteID)))))))
(deftest api-note-creation-with-params
(testing "Note creation with params"
(let [response (send-request [:post "/api/note"]
{:note note
:pid pid
:signature (storage/sign pid psk note)
:version "1.4"
:theme "dark"
:text-size 1.1
:text-font "Helvetica"})
body (parse-string (:body response))
noteID (body "noteID")]
(let [url ((:headers
(send-request
(str "/"
(last (clojure.string/split
((parse-string (:body response)) "shortURL") #"/"))))) "Location")]
(= url ((parse-string (:body response)) "longURL"))
(is (substring? "theme=dark" url))
(is (substring? "text-size=1.1" url))
(is (substring? "text-font=Helvetica" url)))
(is (do
(storage/delete-note noteID)
(not (storage/note-exists? noteID)))))))
(deftest api-note-update
(let [response (send-request [:post "/api/note"]
{:note note
:pid pid
:signature (storage/sign pid psk note)
:version "1.4"
:password "qwerty"})
body (parse-string (:body response))
noteID (body "noteID")]
(testing "Note update"
(is (has-status response 200))
(is (get-in body ["status" "success"]))
(is (storage/note-exists? noteID))
(is (substring? "_test_ note"
((parse-string
(:body (send-request [:get "/api/note"] {:version "1.4" :noteID noteID}))) "note")))
(let [response (send-request [:put "/api/note"]
{:noteID noteID
:note "WRONG pass"
:pid pid
:signature (storage/sign pid psk noteID "WRONG pass" "qwerty1")
:password "qwerty1"
:version "1.4"})
body (parse-string (:body response))]
(is (has-status response 200))
(isnt (get-in body ["status" "success"]))
(is (= "password invalid" (get-in body ["status" "message"])))
(isnt (get-in body ["statistics" "edited"]))
(is (substring? "_test_ note"
((parse-string
(:body (send-request [:get "/api/note"] {:version "1.4" :noteID noteID}))) "note"))))
(is (get-in (parse-string
(:body (send-request [:put "/api/note"]
{:noteID noteID
:note "UPDATED CONTENT"
:pid pid
:signature (storage/sign pid psk noteID "UPDATED CONTENT" "qwerty")
:password "qwerty"
:version "1.4"}))) ["status" "success"]))
(isnt (= nil (((parse-string
(:body (send-request [:get "/api/note"] {:version "1.4" :noteID noteID})))
"statistics") "edited")))
(let [resp (send-request [:get "/api/note"] {:version "1.4" :noteID noteID})]
(is (substring? "UPDATED CONTENT"
((parse-string
(:body resp)) "note"))))
(is (do
(storage/delete-note noteID)
(not (storage/note-exists? noteID)))))))

129
test/notehub/test/handler.clj

@ -1,129 +0,0 @@
(ns notehub.test.handler
(:use clojure.test
[notehub.api :only [get-date url]]
notehub.storage
ring.mock.request
notehub.test.api
notehub.handler))
(defn build-key [[y m d] t] (notehub.api/build-key y m d t))
(def date [2012 6 3])
(def test-title "some-title")
(def test-note "# This is a test note.\nHello _world_. Motörhead, тест.")
(def session-key (str (quot (System/currentTimeMillis) 100000000) "somemd5hash"))
(defn create-testnote-fixture [f]
(add-note (build-key date test-title) test-note "testPID")
(f)
(delete-note (build-key date test-title)))
(use-fixtures :each create-testnote-fixture)
(is (= (url 2010 05 06 "test-title" "export") "/2010/5/6/test-title/export"))
(deftest testing-fixture
(testing "Was a not created?"
(is (= (get-note (build-key date test-title)) test-note))
(is (note-exists? (build-key date test-title)))))
(deftest export-test
(testing "Markdown export"
(is (= (:body (send-request (url 2012 6 3 "some-title" "export"))) test-note))))
(deftest note-creation
(let [date (get-date)
title "this-is-a-test-note"
[year month day] date]
(testing "Note creation"
(let [resp (send-request
[:post "/post-note"]
{:session session-key
:note test-note
:signature (sign session-key test-note)})]
(is (has-status resp 302))
(is (note-exists? (build-key date title)))
(is (substring? "Hello <em>world</em>"
((send-request (url year month day title)) :body)))
(is (do
(delete-note (build-key date title))
(not (note-exists? (build-key date title)))))))))
(deftest note-creation-utf
(let [date (get-date)
title "радуга"
note "# Радуга\nкаждый охотник желает знать, где сидят фазаны."
[year month day] date]
(testing "Note creation with UTF8 symbols"
(is (has-status
(send-request
[:post "/post-note"]
{:session session-key
:note note
:signature (sign session-key note)}) 302))
(is (note-exists? (build-key date title)))
(is (substring? "знать" ((send-request (url year month day title)) :body)))
(is (do
(delete-note (build-key date title))
(not (note-exists? (build-key date title))))))))
(deftest note-update
(let [date (get-date)
title "this-is-a-test-note"
[year month day] date
hash (sign session-key test-note)]
(testing "Note update"
(is (has-status
(send-request
[:post "/post-note"]
{:session session-key
:note test-note
:password "qwerty"
:signature hash}) 302))
(is (note-exists? (build-key date title)))
(is (substring? "test note" ((send-request (url year month day title)) :body)))
(is (has-status
(send-request
[:post "/update-note"]
{:noteID (build-key [year month day] title)
:note "WRONG pass"
:password "qwerty1" }) 403))
(is (substring? "test note" ((send-request (url year month day title)) :body)))
(is (has-status
(send-request
[:post "/update-note"]
{:noteID (build-key [year month day] title)
:note "UPDATED CONTENT 123"
:password "qwerty" }) 302))
(is (substring? "UPDATED CONTENT" ((send-request (url year month day title)) :body)))
(is (do
(delete-note (build-key date title))
(not (note-exists? (build-key date title))))))))
(deftest requests
(testing "HTTP Status"
(testing "of a wrong access"
(is (has-status (send-request "/wrong-page") 404))
(is (has-status (send-request (url 2012 6 3 "lol" "stat")) 404))
(is (has-status (send-request (url 2012 6 3 "lol" "export")) 404))
(is (has-status (send-request (url 2012 6 3 "lol")) 404))
(is (has-status (send-request (url 2012 6 4 "wrong-title")) 404)))
(testing "of corrupt note-post"
(is (has-status (send-request [:post "/post-note"]) 400)))
(testing "valid accesses"
;(is (has-status (send-request "/new") 200) "accessing /new")
(is (has-status (send-request "/api") 200) "accessing API")
(is (has-status (send-request (url 2012 6 3 "some-title")) 200) "accessing test note")
(is (has-status (send-request (url 2012 6 3 "some-title" "export")) 200) "accessing test note's export")
(is (has-status (send-request (url 2012 6 3 "some-title" "stats")) 200) "accessing test note's stats")
(is (has-status (send-request "/") 200) "accessing landing page"))))
(deftest test-app
(testing "main route"
(let [response (app (request :get "/"))]
(is (= (:status response) 200))
(is (substring? "Free and Hassle-free" (:body response)))))
(testing "not-found route"
(let [response (app (request :get "/invalid"))]
(is (= (:status response) 404)))))

70
test/notehub/test/storage.clj

@ -1,70 +0,0 @@
(ns notehub.test.storage
(:use [notehub.storage]
[clojure.test])
(:require [taoensso.carmine :as car :refer (wcar)]))
(defn build-key [[y m d] t] (notehub.api/build-key y m d t))
(def date [2012 06 03])
(def test-title "Some title.")
(def test-note "This is a test note.")
(def metadata {:year 2012,
:month 6,
:day 23,
:title test-title,
:theme "dark",
:header-font "Anton"})
(def test-short-url "")
(deftest signature
(is (= "07e1c0d9533b5168e18a99f4540448af" (sign "wwq123456"))))
(deftest storage
(testing "Storage"
(testing "of short-url mechanism"
(let [fakeID (build-key date test-title)
url (create-short-url fakeID metadata)
url2 (create-short-url fakeID metadata)]
(is (= 1 (redis :scard (str fakeID :urls))))
(def test-short-url (create-short-url fakeID (assoc metadata :a :b)))
(is (= 2 (redis :scard (str fakeID :urls))))
(is (short-url-exists? url))
(is (= url url2))
(is (= metadata (resolve-url url)))
(delete-short-url url)
(is (not (short-url-exists? url))))))
(testing "of correct note creation"
(is (= (let [note-id (build-key date test-title)]
(add-note note-id test-note "testPID")
(is (= 2 (redis :scard (str note-id :urls))))
(create-short-url note-id metadata)
(is (= 3 (redis :scard (str note-id :urls))))
(increment-note-view note-id)
(get-note note-id))
test-note))
(is (= "1" (get-note-views (build-key date test-title))))
(is (= (do
(increment-note-view (build-key date test-title))
(get-note-views (build-key date test-title)))
"2")))
(testing "of note update"
(is (= (do
(add-note (build-key date test-title) test-note "testPID" "12345qwert")
(get-note (build-key date test-title)))
test-note))
(is (valid-password? (build-key date test-title) "12345qwert"))
(is (= (do
(edit-note (build-key date test-title) "update")
(get-note (build-key date test-title)))
"update")))
(testing "of the note access"
(is (not= (get-note (build-key date test-title)) "any text")))
(testing "of note existence"
(is (note-exists? (build-key date test-title)))
(is (short-url-exists? test-short-url))
(is (= 3 (redis :scard (str (build-key date test-title) :urls))))
(delete-note (build-key date test-title))
(is (not (short-url-exists? test-short-url)))
(is (not (note-exists? (build-key date test-title))))
(is (= 0 (redis :scard (str (build-key date test-title) :urls))))
(is (not (note-exists? (build-key [2013 06 03] test-title))))
(is (not (note-exists? (build-key date "some title"))))))
Loading…
Cancel
Save