From 92039138869d2a624b24ccbae2e5e35d93635113 Mon Sep 17 00:00:00 2001 From: Christian Mueller Date: Sat, 14 Jul 2012 22:34:11 +0200 Subject: [PATCH] note editing via passwords enabled --- README.md | 7 +++- messages | 6 ++- src-cljs/main.cljs | 47 ++++++++++++++------- src/NoteHub/storage.clj | 31 ++++++++++---- src/NoteHub/views/common.clj | 3 +- src/NoteHub/views/css_generator.clj | 26 +++++++----- src/NoteHub/views/pages.clj | 64 +++++++++++++++++++---------- test/NoteHub/test/storage.clj | 13 ++++++ test/NoteHub/test/views/pages.clj | 38 ++++++++++++++++- 9 files changed, 177 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 35fb73f..b2c680f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ## 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). 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. -## 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 `/export`, the original markdown content will be displayed in plain text format. diff --git a/messages b/messages index daf1165..0bd2820 100644 --- a/messages +++ b/messages @@ -4,18 +4,22 @@ new-page = New Page status-404 = Nothing found. status-400 = Bad Request. +status-403 = Wrong Password. status-500 = OMG, Server Exploded. created-by = Created by [@chmllr](http://twitter.com/chmllr) loading = Loading... -use-password = User password for editing: +set-passwd = Set password for editing: +enter-passwd = Password: preview = Preview publish = Publish +update = Save published = Published article-views = Article Views statistics = Statistics stats = statistics export = export +edit = edit short-url = short url new-note = New Markdown Note diff --git a/src-cljs/main.cljs b/src-cljs/main.cljs index 073cc66..fce91d8 100644 --- a/src-cljs/main.cljs +++ b/src-cljs/main.cljs @@ -1,43 +1,62 @@ (ns NoteHub.main (:use [jayq.core :only [$ xhr css inner val anim show]]) (:require [goog.dom :as gdom] + [goog.crypt.Md5 :as md5] + [goog.crypt :as crypt] [NoteHub.crossover.lib :as lib] [clojure.browser.dom :as dom])) ; frequently used selectors (def $draft ($ :#draft)) +(def $action ($ :#action)) (def $preview ($ :#preview)) +(def $password ($ :#password)) +(def $plain-password ($ :#plain-password)) (def $input-elems ($ :#input-elems)) -(def $preview-start-line ($ :#preview-start-line)) +(def $dashed-line ($ :#dashed-line)) ; Markdown Converter & Sanitizer instantiation - (def md-converter (Markdown/getSanitizingConverter)) +; instantiate & reset a MD5 hash digester +(def md5 (goog.crypt.Md5.)) +(.reset md5) + ; try to detect iOS (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) -(when $draft +(when $action (do - (val $draft "") + (if (= "update" (val $action)) + (update-preview) + (val $draft "")) ; foces setting is impossible in iOS, so we border the field instead (if ios-detected (.addClass $draft "ui-border") (.focus $draft)))) ; show the preview & publish buttons as soon as the user starts typing. -(.keyup $draft - (fn [e] - (do - (show $preview-start-line) - (show $input-elems) - (inner $preview - (.makeHtml md-converter (val $draft)))))) +(.keyup $draft update-preview) ; 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) (fn [e] - (val ($ :#session-value) - (lib/hash #(.charCodeAt % 0) (str (val $draft) (val ($ :#session-key))))))) + (do + (.update md5 (val $plain-password)) + (val $plain-password nil) + (when (val $plain-password) + (val $password (crypt/byteArrayToHex (.digest md5)))) + (val ($ :#session-value) + (lib/hash #(.charCodeAt % 0) (str (val $draft) (val ($ :#session-key)))))))) diff --git a/src/NoteHub/storage.clj b/src/NoteHub/storage.clj index 3d95d72..719cdf8 100644 --- a/src/NoteHub/storage.clj +++ b/src/NoteHub/storage.clj @@ -2,7 +2,8 @@ (:use [NoteHub.settings] [noir.util.crypt :only [encrypt]] [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 (def db @@ -12,12 +13,15 @@ ; DB hierarchy levels (def note "note") -(def short-url "short-url") (def views "views") +(def password "password") (def sessions "sessions") +(def short-url "short-url") ; 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)) (defn create-session @@ -34,10 +38,21 @@ (let [was-valid (redis/sismember db sessions token)] (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 "Creates a note with the given title and text in the given date namespace" - [date title text] - (redis/hset db note (build-key date title) text)) + ([date title text] (set-note date title text nil)) + ([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 "Gets the note from the given date namespaces for the specified title" @@ -63,9 +78,9 @@ "Deletes the note with the specified coordinates" [date title] (let [key (build-key date title)] - (do - (redis/hdel db views key) - (redis/hdel db note key)))) + (doseq [kw [password views note]] + ; TODO: delete short url by looking for the title + (redis/hdel db kw key)))) (defn short-url-exists? "Checks whether the provided short url is taken (for testing only)" diff --git a/src/NoteHub/views/common.clj b/src/NoteHub/views/common.clj index b6b063b..bd1062d 100644 --- a/src/NoteHub/views/common.clj +++ b/src/NoteHub/views/common.clj @@ -5,6 +5,7 @@ [noir.core :only [defpartial]] [noir.options :only [dev-mode?]] [hiccup.util :only [escape-html]] + [ring.util.codec :only [url-encode]] [hiccup.core] [hiccup.page :only [include-js html5]] [hiccup.element :only [javascript-tag]])) @@ -12,7 +13,7 @@ (defn url "Creates a local url from the given substrings" [& args] - (apply str (interpose "/" (cons "" args)))) + (apply str (interpose "/" (cons "" (map url-encode args))))) ; Creates the main html layout (defpartial generate-layout diff --git a/src/NoteHub/views/css_generator.clj b/src/NoteHub/views/css_generator.clj index ab2a4f9..4ffc00d 100644 --- a/src/NoteHub/views/css_generator.clj +++ b/src/NoteHub/views/css_generator.clj @@ -29,6 +29,10 @@ :margin-left "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 (defn- color [& keys] (get-in {:dark {:background :#333 @@ -70,8 +74,15 @@ (rule "&:visited" :color link-visited)) (rule ".ui-border" - :border-radius :3px - :border [:1px :solid foreground]) + (thin-border foreground)) + (rule ".button" + :cursor :pointer) + (rule ".ui-elem" + helvetica-neue + (thin-border foreground) + :opacity 0.8 + :font-size :1em + :background background) (rule ".landing-button" :box-shadow [0 :2px :5px :#aaa] :text-decoration :none @@ -139,6 +150,8 @@ :margin :2em)) (rule ".centered" :text-align :center) + (rule ".bottom-space" + :margin-bottom :7em) (rule "pre" :border-radius :3px :padding :0.5em @@ -155,20 +168,13 @@ :height :500px) (rule ".hidden" :display :none) - (rule ".button" - :-webkit-appearance :none - helvetica-neue - :cursor :pointer - :opacity 0.8 - :font-size :1em - :background background) (rule ".central-element" central-element) (rule "fieldset" :border :none) (rule "h1" :font-size :2em) - (rule ".dashed-line" + (rule "#dashed-line" :border-bottom [:1px :dashed foreground-halftone] :margin-top :3em :margin-bottom :3em) diff --git a/src/NoteHub/views/pages.clj b/src/NoteHub/views/pages.clj index ff66a71..159bb62 100644 --- a/src/NoteHub/views/pages.clj +++ b/src/NoteHub/views/pages.clj @@ -9,11 +9,10 @@ [clojure.string :rename {replace sreplace} :only [split replace lower-case]] [clojure.core.incubator :only [-?>]] [hiccup.form] - [ring.util.codec :only [url-encode]] [hiccup.core] [hiccup.element] [noir.response :only [redirect status content-type]] - [noir.core :only [defpage]] + [noir.core :only [defpage defpartial]] [cheshire.core] [noir.statuses]) (:import @@ -30,7 +29,7 @@ ; Sets a custom message for each needed HTTP status. ; 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 (let [message (get-message (keyword (str "status-" code)))] (layout message @@ -44,13 +43,13 @@ (defn- wrap [short-url params md-text] (when md-text (layout params (params :title) - [:article (md-to-html md-text)] + [:article.bottom-space (md-to-html md-text)] (let [links (map #(link-to (if (= :short-url %) (url short-url) (str (params :title) "/" (name %))) (get-message %)) - [:stats :export :short-url]) + [:stats :edit :export :short-url]) space (apply str (repeat 4 " ")) separator (str space "·" space) links (interpose separator links)] @@ -79,25 +78,42 @@ [:h2 (get-message :title)] [:br] [: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 - [:article.helvetica-neue {:style "font-size: 1em"} + [:article.helvetica-neue.bottom-space {:style "font-size: 1em"} (md-to-html (slurp "README.md"))] [: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 (defpage "/new" {} - (layout {:js true} (get-message :new-note) - [:article#preview " "] - [:div#preview-start-line.dashed-line.hidden] - [:div.central-element {:style "margin-bottom: 3em"} - (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))])])) + (input-form "/post-note" :publish + (html (hidden-field :session-key (create-session)) + (hidden-field {:id :session-value} :session-value)) + (get-message :loading) :set-passwd)) ; Displays the note (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 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 ;) -(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 (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? @@ -151,8 +173,8 @@ (cons proposed-title (map #(str proposed-title "-" (+ 2 %)) (range)))))] (do - (set-note date title draft) - (redirect (url year month day (url-encode title))))) + (set-note date title draft password) + (redirect (url year month day title)))) (response 400)))) ; Resolving of a short url diff --git a/test/NoteHub/test/storage.clj b/test/NoteHub/test/storage.clj index c8a1a18..cee6434 100644 --- a/test/NoteHub/test/storage.clj +++ b/test/NoteHub/test/storage.clj @@ -33,6 +33,19 @@ (get-note date test-title) (get-note-views date test-title)) "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" (is (not= (get-note date test-title) "any text"))) (testing "session management" diff --git a/test/NoteHub/test/views/pages.clj b/test/NoteHub/test/views/pages.clj index da0d6e6..a3182e7 100644 --- a/test/NoteHub/test/views/pages.clj +++ b/test/NoteHub/test/views/pages.clj @@ -36,7 +36,6 @@ (deftest note-creation (let [session-key (create-session) date (get-date) - ; TODO: replace note generation by a function from pages.clj title "this-is-a-test-note" [year month day] date] (testing "Note creation" @@ -54,6 +53,43 @@ (delete-note 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 (testing "HTTP Status" (testing "of a wrong access"