Browse Source

note editing via passwords enabled

master
Christian Mueller 14 years ago
parent
commit
9203913886
  1. 7
      README.md
  2. 6
      messages
  3. 47
      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 @@ @@ -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 @@ -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.

6
messages

@ -4,18 +4,22 @@ new-page = New Page @@ -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

47
src-cljs/main.cljs

@ -1,43 +1,62 @@ @@ -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))))))))

31
src/NoteHub/storage.clj

@ -2,7 +2,8 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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)"

3
src/NoteHub/views/common.clj

@ -5,6 +5,7 @@ @@ -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 @@ @@ -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

26
src/NoteHub/views/css_generator.clj

@ -29,6 +29,10 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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)

64
src/NoteHub/views/pages.clj

@ -9,11 +9,10 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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

13
test/NoteHub/test/storage.clj

@ -33,6 +33,19 @@ @@ -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"

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

@ -36,7 +36,6 @@ @@ -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 @@ @@ -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"

Loading…
Cancel
Save