Browse Source

note editing via passwords enabled

master
Christian Mueller 14 years ago
parent
commit
9203913886
  1. 7
      README.md
  2. 6
      messages
  3. 45
      src-cljs/main.cljs
  4. 31
      src/NoteHub/storage.clj
  5. 3
      src/NoteHub/views/common.clj
  6. 26
      src/NoteHub/views/css_generator.clj
  7. 64
      src/NoteHub/views/pages.clj
  8. 13
      test/NoteHub/test/storage.clj
  9. 38
      test/NoteHub/test/views/pages.clj

7
README.md

@ -1,6 +1,6 @@
## About ## About
[NoteHub](http://notehub.org) is a free and hassle-free anonymous hosting for markdown pages. It can be used for publishing of markdown-formatted text. [NoteHub](http://notehub.org) is a free and hassle-free anonymous hosting for markdown pages. It can be used for publishing of markdown-formatted texts.
NoteHub was an one-app-one-language [experiment](http://notehub.org/2012/6/16/how-notehub-is-built) and is implemented entirely in [Clojure](http://clojure.org) (ClojureScript). NoteHub was an one-app-one-language [experiment](http://notehub.org/2012/6/16/how-notehub-is-built) and is implemented entirely in [Clojure](http://clojure.org) (ClojureScript).
The [source code](https://github.com/chmllr/NoteHub) can be found on GitHub. The [source code](https://github.com/chmllr/NoteHub) can be found on GitHub.
@ -37,7 +37,10 @@ See an example of the font formatting [here](http://notehub.org/2012/6/16/how-no
After you've specified this in the url, you can copy the corresponding short url of the article and share it. After you've specified this in the url, you can copy the corresponding short url of the article and share it.
## Export & Statistics ## After Publishing
During the note publishing a password can be set.
This password unlocks the note for an editing.
The edit mode can be entered by appending of `/edit`to the note url.
By appending of `/stats` to any note url, everyone can see a rudimentary statistics (currently, the number of note views only). By appending of `/stats` to any note url, everyone can see a rudimentary statistics (currently, the number of note views only).
By appending of `/export`, the original markdown content will be displayed in plain text format. By appending of `/export`, the original markdown content will be displayed in plain text format.

6
messages

@ -4,18 +4,22 @@ new-page = New Page
status-404 = Nothing found. status-404 = Nothing found.
status-400 = Bad Request. status-400 = Bad Request.
status-403 = Wrong Password.
status-500 = OMG, Server Exploded. status-500 = OMG, Server Exploded.
created-by = Created by [@chmllr](http://twitter.com/chmllr) created-by = Created by [@chmllr](http://twitter.com/chmllr)
loading = Loading... loading = Loading...
use-password = User password for editing: set-passwd = Set password for editing:
enter-passwd = Password:
preview = Preview preview = Preview
publish = Publish publish = Publish
update = Save
published = Published published = Published
article-views = Article Views article-views = Article Views
statistics = Statistics statistics = Statistics
stats = statistics stats = statistics
export = export export = export
edit = edit
short-url = short url short-url = short url
new-note = New Markdown Note new-note = New Markdown Note

45
src-cljs/main.cljs

@ -1,43 +1,62 @@
(ns NoteHub.main (ns NoteHub.main
(:use [jayq.core :only [$ xhr css inner val anim show]]) (:use [jayq.core :only [$ xhr css inner val anim show]])
(:require [goog.dom :as gdom] (:require [goog.dom :as gdom]
[goog.crypt.Md5 :as md5]
[goog.crypt :as crypt]
[NoteHub.crossover.lib :as lib] [NoteHub.crossover.lib :as lib]
[clojure.browser.dom :as dom])) [clojure.browser.dom :as dom]))
; frequently used selectors ; frequently used selectors
(def $draft ($ :#draft)) (def $draft ($ :#draft))
(def $action ($ :#action))
(def $preview ($ :#preview)) (def $preview ($ :#preview))
(def $password ($ :#password))
(def $plain-password ($ :#plain-password))
(def $input-elems ($ :#input-elems)) (def $input-elems ($ :#input-elems))
(def $preview-start-line ($ :#preview-start-line)) (def $dashed-line ($ :#dashed-line))
; Markdown Converter & Sanitizer instantiation ; Markdown Converter & Sanitizer instantiation
(def md-converter (Markdown/getSanitizingConverter)) (def md-converter (Markdown/getSanitizingConverter))
; instantiate & reset a MD5 hash digester
(def md5 (goog.crypt.Md5.))
(.reset md5)
; try to detect iOS ; try to detect iOS
(def ios-detected (.match (.-userAgent js/navigator) "(iPad|iPod|iPhone)")) (def ios-detected (.match (.-userAgent js/navigator) "(iPad|iPod|iPhone)"))
(defn update-preview
[]
"Updates the preview"
(do
(show $dashed-line)
(show $input-elems)
(inner $preview
(.makeHtml md-converter (val $draft)))))
; set focus to the draft textarea (if there is one) ; set focus to the draft textarea (if there is one)
(when $draft (when $action
(do (do
(val $draft "") (if (= "update" (val $action))
(update-preview)
(val $draft ""))
; foces setting is impossible in iOS, so we border the field instead ; foces setting is impossible in iOS, so we border the field instead
(if ios-detected (if ios-detected
(.addClass $draft "ui-border") (.addClass $draft "ui-border")
(.focus $draft)))) (.focus $draft))))
; show the preview & publish buttons as soon as the user starts typing. ; show the preview & publish buttons as soon as the user starts typing.
(.keyup $draft (.keyup $draft update-preview)
(fn [e]
(do
(show $preview-start-line)
(show $input-elems)
(inner $preview
(.makeHtml md-converter (val $draft))))))
; when the publish button is clicked, compute the hash of the entered text and ; when the publish button is clicked, compute the hash of the entered text and
; provided session key and assign to the field session-value ; provided session key and assign to the field session-value;
; moreover, compute the password hash as md5 before transmission
(.click ($ :#publish-button) (.click ($ :#publish-button)
(fn [e] (fn [e]
(do
(.update md5 (val $plain-password))
(val $plain-password nil)
(when (val $plain-password)
(val $password (crypt/byteArrayToHex (.digest md5))))
(val ($ :#session-value) (val ($ :#session-value)
(lib/hash #(.charCodeAt % 0) (str (val $draft) (val ($ :#session-key))))))) (lib/hash #(.charCodeAt % 0) (str (val $draft) (val ($ :#session-key))))))))

31
src/NoteHub/storage.clj

@ -2,7 +2,8 @@
(:use [NoteHub.settings] (:use [NoteHub.settings]
[noir.util.crypt :only [encrypt]] [noir.util.crypt :only [encrypt]]
[noir.options :only [dev-mode?]]) [noir.options :only [dev-mode?]])
(:require [clj-redis.client :as redis])) (:require [clj-redis.client :as redis]
[clojure.contrib.string :as ccs]))
; Initialize the data base ; Initialize the data base
(def db (def db
@ -12,12 +13,15 @@
; DB hierarchy levels ; DB hierarchy levels
(def note "note") (def note "note")
(def short-url "short-url")
(def views "views") (def views "views")
(def password "password")
(def sessions "sessions") (def sessions "sessions")
(def short-url "short-url")
; Concatenates all fields to a string ; Concatenates all fields to a string
(defn- build-key [[year month day] title] (defn build-key
"Returns a storage-key for the given note coordinates"
[[year month day] title]
(print-str year month day title)) (print-str year month day title))
(defn create-session (defn create-session
@ -34,10 +38,21 @@
(let [was-valid (redis/sismember db sessions token)] (let [was-valid (redis/sismember db sessions token)]
(do (redis/srem db sessions token) was-valid)))) (do (redis/srem db sessions token) was-valid))))
(defn update-note
"Updates a note with the given store key if the specified password is correct"
[key text passwd]
(when (= passwd (redis/hget db password key))
(redis/hset db note key text)))
(defn set-note (defn set-note
"Creates a note with the given title and text in the given date namespace" "Creates a note with the given title and text in the given date namespace"
[date title text] ([date title text] (set-note date title text nil))
(redis/hset db note (build-key date title) text)) ([date title text passwd]
(let [key (build-key date title)]
(do
(redis/hset db note key text)
(when (not (ccs/blank? passwd))
(redis/hset db password key passwd))))))
(defn get-note (defn get-note
"Gets the note from the given date namespaces for the specified title" "Gets the note from the given date namespaces for the specified title"
@ -63,9 +78,9 @@
"Deletes the note with the specified coordinates" "Deletes the note with the specified coordinates"
[date title] [date title]
(let [key (build-key date title)] (let [key (build-key date title)]
(do (doseq [kw [password views note]]
(redis/hdel db views key) ; TODO: delete short url by looking for the title
(redis/hdel db note key)))) (redis/hdel db kw key))))
(defn short-url-exists? (defn short-url-exists?
"Checks whether the provided short url is taken (for testing only)" "Checks whether the provided short url is taken (for testing only)"

3
src/NoteHub/views/common.clj

@ -5,6 +5,7 @@
[noir.core :only [defpartial]] [noir.core :only [defpartial]]
[noir.options :only [dev-mode?]] [noir.options :only [dev-mode?]]
[hiccup.util :only [escape-html]] [hiccup.util :only [escape-html]]
[ring.util.codec :only [url-encode]]
[hiccup.core] [hiccup.core]
[hiccup.page :only [include-js html5]] [hiccup.page :only [include-js html5]]
[hiccup.element :only [javascript-tag]])) [hiccup.element :only [javascript-tag]]))
@ -12,7 +13,7 @@
(defn url (defn url
"Creates a local url from the given substrings" "Creates a local url from the given substrings"
[& args] [& args]
(apply str (interpose "/" (cons "" args)))) (apply str (interpose "/" (cons "" (map url-encode args)))))
; Creates the main html layout ; Creates the main html layout
(defpartial generate-layout (defpartial generate-layout

26
src/NoteHub/views/css_generator.clj

@ -29,6 +29,10 @@
:margin-left "auto" :margin-left "auto"
:margin-right "auto")) :margin-right "auto"))
(defn thin-border [foreground]
(mixin :border-radius :3px
:border [:1px :solid foreground]))
; Resolves the theme name & tone parameter to a concrete color ; Resolves the theme name & tone parameter to a concrete color
(defn- color [& keys] (defn- color [& keys]
(get-in {:dark {:background :#333 (get-in {:dark {:background :#333
@ -70,8 +74,15 @@
(rule "&:visited" (rule "&:visited"
:color link-visited)) :color link-visited))
(rule ".ui-border" (rule ".ui-border"
:border-radius :3px (thin-border foreground))
:border [:1px :solid foreground]) (rule ".button"
:cursor :pointer)
(rule ".ui-elem"
helvetica-neue
(thin-border foreground)
:opacity 0.8
:font-size :1em
:background background)
(rule ".landing-button" (rule ".landing-button"
:box-shadow [0 :2px :5px :#aaa] :box-shadow [0 :2px :5px :#aaa]
:text-decoration :none :text-decoration :none
@ -139,6 +150,8 @@
:margin :2em)) :margin :2em))
(rule ".centered" (rule ".centered"
:text-align :center) :text-align :center)
(rule ".bottom-space"
:margin-bottom :7em)
(rule "pre" (rule "pre"
:border-radius :3px :border-radius :3px
:padding :0.5em :padding :0.5em
@ -155,20 +168,13 @@
:height :500px) :height :500px)
(rule ".hidden" (rule ".hidden"
:display :none) :display :none)
(rule ".button"
:-webkit-appearance :none
helvetica-neue
:cursor :pointer
:opacity 0.8
:font-size :1em
:background background)
(rule ".central-element" (rule ".central-element"
central-element) central-element)
(rule "fieldset" (rule "fieldset"
:border :none) :border :none)
(rule "h1" (rule "h1"
:font-size :2em) :font-size :2em)
(rule ".dashed-line" (rule "#dashed-line"
:border-bottom [:1px :dashed foreground-halftone] :border-bottom [:1px :dashed foreground-halftone]
:margin-top :3em :margin-top :3em
:margin-bottom :3em) :margin-bottom :3em)

64
src/NoteHub/views/pages.clj

@ -9,11 +9,10 @@
[clojure.string :rename {replace sreplace} :only [split replace lower-case]] [clojure.string :rename {replace sreplace} :only [split replace lower-case]]
[clojure.core.incubator :only [-?>]] [clojure.core.incubator :only [-?>]]
[hiccup.form] [hiccup.form]
[ring.util.codec :only [url-encode]]
[hiccup.core] [hiccup.core]
[hiccup.element] [hiccup.element]
[noir.response :only [redirect status content-type]] [noir.response :only [redirect status content-type]]
[noir.core :only [defpage]] [noir.core :only [defpage defpartial]]
[cheshire.core] [cheshire.core]
[noir.statuses]) [noir.statuses])
(:import (:import
@ -30,7 +29,7 @@
; Sets a custom message for each needed HTTP status. ; Sets a custom message for each needed HTTP status.
; The message to be assigned is extracted with a dynamically generated key ; The message to be assigned is extracted with a dynamically generated key
(doseq [code [400 404 500]] (doseq [code [400 403 404 500]]
(set-page! code (set-page! code
(let [message (get-message (keyword (str "status-" code)))] (let [message (get-message (keyword (str "status-" code)))]
(layout message (layout message
@ -44,13 +43,13 @@
(defn- wrap [short-url params md-text] (defn- wrap [short-url params md-text]
(when md-text (when md-text
(layout params (params :title) (layout params (params :title)
[:article (md-to-html md-text)] [:article.bottom-space (md-to-html md-text)]
(let [links (map #(link-to (let [links (map #(link-to
(if (= :short-url %) (if (= :short-url %)
(url short-url) (url short-url)
(str (params :title) "/" (name %))) (str (params :title) "/" (name %)))
(get-message %)) (get-message %))
[:stats :export :short-url]) [:stats :edit :export :short-url])
space (apply str (repeat 4 " ")) space (apply str (repeat 4 " "))
separator (str space "·" space) separator (str space "·" space)
links (interpose separator links)] links (interpose separator links)]
@ -79,25 +78,42 @@
[:h2 (get-message :title)] [:h2 (get-message :title)]
[:br] [:br]
[:a.landing-button {:href "/new" :style "color: white"} (get-message :new-page)]] [:a.landing-button {:href "/new" :style "color: white"} (get-message :new-page)]]
[:div.dashed-line] [:div#dashed-line]
; dynamically generates three column, retrieving corresponding messages ; dynamically generates three column, retrieving corresponding messages
[:article.helvetica-neue {:style "font-size: 1em"} [:article.helvetica-neue.bottom-space {:style "font-size: 1em"}
(md-to-html (slurp "README.md"))] (md-to-html (slurp "README.md"))]
[:div.centered.helvetica-neue (md-to-html (get-message :created-by))])) [:div.centered.helvetica-neue (md-to-html (get-message :created-by))]))
; input form for the markdown text with a preview area
(defpartial input-form [form-url command fields content passwd-msg]
(let [css-class (when (= :publish command) :hidden)]
(layout {:js true} (get-message :new-note)
[:article#preview " "]
[:div#dashed-line {:class css-class}]
[:div.central-element.helvetica-neue {:style "margin-bottom: 3em"}
(form-to [:post form-url]
(hidden-field :action command)
(hidden-field :password)
fields
(text-area {:class :max-width} :draft content)
[:fieldset#input-elems {:class css-class}
(get-message passwd-msg)
(text-field {:class "ui-elem"} :plain-password)
(submit-button {:class "button ui-elem"
:id :publish-button} (get-message command))])])))
; Update Note Page
(defpage "/:year/:month/:day/:title/edit" {:keys [year month day title]}
(input-form "/update-note" :update
(html (hidden-field :key (build-key [year month day] title)))
(get-note [year month day] title) :enter-passwd))
; New Note Page ; New Note Page
(defpage "/new" {} (defpage "/new" {}
(layout {:js true} (get-message :new-note) (input-form "/post-note" :publish
[:article#preview " "] (html (hidden-field :session-key (create-session))
[:div#preview-start-line.dashed-line.hidden] (hidden-field {:id :session-value} :session-value))
[:div.central-element {:style "margin-bottom: 3em"} (get-message :loading) :set-passwd))
(form-to [:post "/post-note"]
(hidden-field :session-key (create-session))
(hidden-field {:id :session-value} :session-value)
(text-area {:class :max-width} :draft (get-message :loading))
[:fieldset#input-elems.hidden
(submit-button {:class "button ui-border"
:id :publish-button} (get-message :publish))])]))
; Displays the note ; Displays the note
(defpage "/:year/:month/:day/:title" {:keys [year month day title theme header-font text-font] :as params} (defpage "/:year/:month/:day/:title" {:keys [year month day title theme header-font text-font] :as params}
@ -123,8 +139,14 @@
[:td (get-message :article-views)] [:td (get-message :article-views)]
[:td views]]]))) [:td views]]])))
; Updates a note
(defpage [:post "/update-note"] {:keys [key draft password]}
(if (update-note key draft password)
(redirect (apply url (split key #" ")))
(response 403)))
; New Note Posting — the most "complex" function in the entire app ;) ; New Note Posting — the most "complex" function in the entire app ;)
(defpage [:post "/post-note"] {:keys [draft session-key session-value]} (defpage [:post "/post-note"] {:keys [draft password session-key session-value]}
; first we collect all info needed to evaluate the validity of the note creation request ; first we collect all info needed to evaluate the validity of the note creation request
(let [valid-session (invalidate-session session-key) ; was the note posted from a newly generated form? (let [valid-session (invalidate-session session-key) ; was the note posted from a newly generated form?
valid-draft (not (ccs/blank? draft)) ; has the note a meaningful content? valid-draft (not (ccs/blank? draft)) ; has the note a meaningful content?
@ -151,8 +173,8 @@
(cons proposed-title (cons proposed-title
(map #(str proposed-title "-" (+ 2 %)) (range)))))] (map #(str proposed-title "-" (+ 2 %)) (range)))))]
(do (do
(set-note date title draft) (set-note date title draft password)
(redirect (url year month day (url-encode title))))) (redirect (url year month day title))))
(response 400)))) (response 400))))
; Resolving of a short url ; Resolving of a short url

13
test/NoteHub/test/storage.clj

@ -33,6 +33,19 @@
(get-note date test-title) (get-note date test-title)
(get-note-views date test-title)) (get-note-views date test-title))
"2"))) "2")))
(testing "of note update"
(is (= (do
(set-note date test-title test-note "12345qwert")
(get-note date test-title))
test-note))
(is (= (do
(update-note (build-key date test-title) "update" "12345qwert")
(get-note date test-title))
"update"))
(is (= (do
(update-note (build-key date test-title) "not authorized" "44444")
(get-note date test-title))
"update")))
(testing "of the note access" (testing "of the note access"
(is (not= (get-note date test-title) "any text"))) (is (not= (get-note date test-title) "any text")))
(testing "session management" (testing "session management"

38
test/NoteHub/test/views/pages.clj

@ -36,7 +36,6 @@
(deftest note-creation (deftest note-creation
(let [session-key (create-session) (let [session-key (create-session)
date (get-date) date (get-date)
; TODO: replace note generation by a function from pages.clj
title "this-is-a-test-note" title "this-is-a-test-note"
[year month day] date] [year month day] date]
(testing "Note creation" (testing "Note creation"
@ -54,6 +53,43 @@
(delete-note date title) (delete-note date title)
(not (note-exists? date title))))))) (not (note-exists? date title)))))))
(deftest note-update
(let [session-key (create-session)
date (get-date)
title "test-note"
[year month day] date]
(testing "Note update"
(is (has-status
(send-request
[:post "/post-note"]
{:session-key session-key
:draft "test note"
:password "qwerty"
:session-value (str (lib/hash #(.codePointAt % 0)
(str "test note" session-key)))}) 302))
(is (note-exists? date title))
(is (substring? "test note"
((send-request (url year month day title)) :body)))
(is (has-status
(send-request
[:post "/update-note"]
{:key (build-key [year month day] title)
:draft "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"]
{:key (build-key [year month day] title)
:draft "UPDATED CONTENT"
:password "qwerty" }) 302))
(is (substring? "UPDATED CONTENT"
((send-request (url year month day title)) :body)))
(is (do
(delete-note date title)
(not (note-exists? date title)))))))
(deftest requests (deftest requests
(testing "HTTP Status" (testing "HTTP Status"
(testing "of a wrong access" (testing "of a wrong access"

Loading…
Cancel
Save