diff --git a/API.md b/API.md index a758ea7..6c3c813 100644 --- a/API.md +++ b/API.md @@ -26,14 +26,14 @@ Parameter | Explanation | Type `noteID` | Note-ID | **required** `version` | Used API version | **required** -will return a JSON object containing following self explaining fields: `note`, `longURL`, `shortURL`, `statistics`, `status`. +will return a JSON object containing following self explaining fields: `note`, `longPath`, `shortPath`, `statistics`, `status`. Example: { note: , - longURL: "http://notehub.org/2014/1/3/lorem-ipsum", - shortURL: "http://notehub.org/0vrcp", + longPath: "/2014/1/3/lorem-ipsum", + shortPath: "/0vrcp", statistics: { published: "2014-1-3", edited: "2014-1-12", @@ -71,14 +71,14 @@ The Signature is the MD5 hash of the following string concatenation: The signature serves as a proof, that the request is authentic and will be issued by the publisher corresponding to the provided PID. -The response of the server will contain the fields `noteID`, `longURL`, `shortURL`, `status`. +The response of the server will contain the fields `noteID`, `longPath`, `shortPath`, `status`. Example: { noteID: "2014/1/3/lorem-ipsum", - longURL: "http://notehub.org/2014/1/3/lorem-ipsum", - shortURL: "http://notehub.org/0vrcp", + longPath: "/2014/1/3/lorem-ipsum", + shortPath: "/0vrcp", status: { success: true, comment: "some server message" @@ -109,13 +109,13 @@ The Signature is the MD5 hash of the following string concatenation: pid + psk + noteID + note + password -The response of the server will contain the fields `longURL`, `shortURL`, `status`. +The response of the server will contain the fields `longPath`, `shortPath`, `status`. Example: { - longURL: "http://notehub.org/2014/1/3/lorem-ipsum", - shortURL: "http://notehub.org/0vrcp", + longPath: "/2014/1/3/lorem-ipsum", + shortPath: "/0vrcp", status: { success: true, comment: "some server message" diff --git a/messages b/messages index 0e91e58..eb8fb71 100644 --- a/messages +++ b/messages @@ -1,4 +1,4 @@ -page-title = Publish Markdown for Free +page-title = Pastebin for Markdown: Publish Your Markdown Anonymously title = Free and Hassle-free Pastebin for Markdown Pages. name = NoteHub new-page = New Page @@ -23,4 +23,4 @@ stats = statistics export = export edit = edit short-url = short url -api-title = NoteHub API +api-title = API diff --git a/resources/public/js/main.js b/resources/public/js/main.js index 0047d25..4e85040 100644 --- a/resources/public/js/main.js +++ b/resources/public/js/main.js @@ -1,36 +1,24 @@ -var hash = function (input) { - var shortMod = function(i) { return i % 32767 }; - var charCodes = input.split("") - .filter(function(c){ return c != "\n" && c != "\r" }) - .map(function(c){ return c.charCodeAt(0) }); - var h = 0; - for(var i in charCodes) - h = shortMod(h + shortMod(charCodes[i] * (h % 2 != 0 ? 16381 ^ i : 16381 & i))); - return h; -} - var $ = function(id){ return document.getElementById(id); } var iosDetected = navigator.userAgent.match("(iPad|iPod|iPhone)"); var timer = null; var timerDelay = iosDetected ? 800 : 400; var show = function(elem) { elem.style.display = "block" } -var $draft, $action, $preview, $password, $plain_password, $input_elems, $dashed_line, updatePreview; +var $note, $action, $preview, $plain_password, $input_elems, $dashed_line, updatePreview; function md2html(input){ return marked(input); } function loadPage() { - $draft = $("draft"); + $note = $("note"); $action = $("action"); $preview = $("preview"); - $password = $("password"); $plain_password = $("plain-password"); $input_elems = $("input-elems"); $dashed_line = $("dashed-line"); updatePreview = function(){ clearTimeout(timer); - var content = $draft.value; + var content = $note.value; var delay = Math.min(timerDelay, timerDelay * (content.length / 400)); timer = setTimeout(function(){ show($dashed_line); @@ -39,14 +27,14 @@ function loadPage() { }, delay); }; if($action){ - if($action.value == "update") updatePreview(); else $draft.value = ""; - $draft.onkeyup = updatePreview; + if($action.value == "update") updatePreview(); else $note.value = ""; + $note.onkeyup = updatePreview; $("publish-button").onclick = function(e) { - if($plain_password.value != "") $password.value = md5($plain_password.value); + if($plain_password.value != "") $("password").value = md5($plain_password.value); $plain_password.value = null; - $("session-value").value = hash($draft.value + $("session-key").value); + $("signature").value = md5($("session").value + $note.value); } - if(iosDetected) $draft.className += " ui-border"; else $draft.focus(); + if(iosDetected) $note.className += " ui-border"; else $note.focus(); } var mdDocs = document.getElementsByClassName("markdown"); diff --git a/settings b/settings index af40dcc..70b3e6f 100644 --- a/settings +++ b/settings @@ -1 +1,2 @@ max-title-length = 40 +domain = http://notehub.org/ diff --git a/src/NoteHub/api.clj b/src/NoteHub/api.clj index 747eb67..ad844b5 100644 --- a/src/NoteHub/api.clj +++ b/src/NoteHub/api.clj @@ -7,9 +7,9 @@ :only [replace blank? lower-case split-lines]]) (:require [NoteHub.storage :as storage])) -(def api-version "1.0") +(def version "1.0") -(def domain "http://notehub.org/") +(def domain (get-setting :domain)) ; Concatenates all fields to a string (defn build-key @@ -28,10 +28,10 @@ ([success message & params] (assoc (create-response success) :message (apply format message params)))) -(defn- getURL [noteID & description] +(defn- getPath [noteID & description] (if description - (str domain (storage/get-short-url noteID)) - (str domain (sreplace noteID #" " "/")))) + (str "/" (storage/get-short-url noteID)) + (str "/" (sreplace noteID #" " "/")))) (let [md5Instance (java.security.MessageDigest/getInstance "MD5")] (defn get-signature @@ -43,13 +43,13 @@ (.toString (new java.math.BigInteger 1 (.digest md5Instance)) 16))))) (defn get-note [noteID] - {:note (storage/get-note noteID) - :longURL (getURL noteID) - :shortURL (getURL noteID :short) - :statistics (storage/get-note-statistics noteID) - :status (if (storage/note-exists? noteID) - (create-response true) - (create-response false "noteID '%s' unknown" noteID))}) + (if (storage/note-exists? noteID) + {:note (storage/get-note noteID) + :longPath (getPath noteID) + :shortPath (getPath noteID :short) + :statistics (storage/get-note-statistics noteID) + :status (create-response true)} + (create-response false "noteID '%s' unknown" noteID))) (defn post-note ([note pid signature] (post-note note pid signature nil)) @@ -76,12 +76,10 @@ (do (storage/add-note noteID note password) (storage/create-short-url noteID) - { - :noteID noteID - :longURL (getURL noteID) - :shortURL (getURL noteID :short) - :status (create-response true) - })) + {:noteID noteID + :longPath (getPath noteID) + :shortPath (getPath noteID :short) + :status (create-response true)})) {:status (create-response false (first errors))})))) @@ -97,9 +95,7 @@ (if (empty? errors) (do (storage/edit-note noteID note) - { - :longURL (getURL noteID) - :shortURL (getURL noteID :short) - :status (create-response true) - }) + {:longPath (getPath noteID) + :shortPath (getPath noteID :short) + :status (create-response true)}) {:status (create-response false (first errors))}))) diff --git a/src/NoteHub/server.clj b/src/NoteHub/server.clj index 953f45a..250a6b8 100644 --- a/src/NoteHub/server.clj +++ b/src/NoteHub/server.clj @@ -6,6 +6,5 @@ (defn -main [& m] (let [mode (keyword (or (first m) :prod)) port (Integer. (get (System/getenv) "PORT" "8080"))] - (server/start port {:mode mode - :ns 'NoteHub}))) + (server/start port {:mode mode :ns 'NoteHub}))) diff --git a/src/NoteHub/storage.clj b/src/NoteHub/storage.clj index 70cd6eb..f3f3a51 100644 --- a/src/NoteHub/storage.clj +++ b/src/NoteHub/storage.clj @@ -40,28 +40,18 @@ (defn get-psk [pid] (redis/hget db publisher pid)) -(defn create-session - [] +(defn create-session [] (let [token (encrypt (str (rand-int Integer/MAX_VALUE)))] (do (redis/sadd db sessions token) token))) -(defn invalidate-session - [token] +(defn invalidate-session [token] ; Jedis is buggy & returns an NPE for token == nil (when token (let [was-valid (redis/sismember db sessions token)] (do (redis/srem db sessions token) was-valid)))) -; TODO: deprecated -(defn update-note - [noteID text passwd] - (let [stored-password (redis/hget db password noteID)] - (when (and stored-password (= passwd stored-password)) - (redis/hset db edited noteID (get-current-date)) - (redis/hset db note noteID text)))) - (defn edit-note [noteID text] (do @@ -97,8 +87,8 @@ "Return views, publishing and editing timestamp" [noteID] { :views (redis/hget db views noteID) - :published (redis/hget db published noteID) - :edited (redis/hget db edited noteID) }) + :published (redis/hget db published noteID) + :edited (redis/hget db edited noteID) }) (defn note-exists? [noteID] diff --git a/src/NoteHub/views/pages.clj b/src/NoteHub/views/pages.clj index 895eb35..5bdce6e 100644 --- a/src/NoteHub/views/pages.clj +++ b/src/NoteHub/views/pages.clj @@ -4,12 +4,12 @@ [NoteHub.storage :as storage] [cheshire.core :refer :all]) (:use - [NoteHub.storage] ; TODO: delete this [NoteHub.settings] [NoteHub.views.common] [clojure.string :rename {replace sreplace} :only [escape split replace blank? split-lines lower-case]] [clojure.core.incubator :only [-?>]] + [noir.util.crypt :only [encrypt]] [hiccup.form] [hiccup.core] [hiccup.element] @@ -17,21 +17,8 @@ [noir.core :only [defpage defpartial]] [noir.statuses])) -(defn get-hash - "A simple hash-function, which computes a hash from the text field - content and given session number. It is intended to be used as a spam - protection / captcha alternative. (Probably doesn't work for UTF-16)" - [s] - (let [short-mod #(mod % 32767) - char-codes (map #(.codePointAt % 0) (remove #(contains? #{"\n" "\r"} %) (map str s))) - zip-with-index (map list char-codes (range))] - (reduce - #(short-mod (+ % - (short-mod (* (first %2) - ((if (odd? %) - bit-xor - bit-and) 16381 (second %2)))))) - 0 zip-with-index))) +(when-not (storage/valid-publisher? api/domain) + (storage/register-publisher api/domain)) ; Sets a custom message for each needed HTTP status. ; The message to be assigned is extracted with a dynamically generated key @@ -70,15 +57,19 @@ [:div.central-element.helvetica {:style "margin-bottom: 3em"} (form-to {:autocomplete :off} [:post form-url] (hidden-field :action command) + (hidden-field :version api/version) (hidden-field :password) fields - (text-area {:class :max-width} :draft content) + (text-area {:class :max-width} :note content) [:fieldset#input-elems {:class css-class} (text-field {:class "ui-elem" :placeholder (get-message passwd-msg)} :plain-password) (submit-button {:class "button ui-elem" :id :publish-button} (get-message command))])]))) +(defn generate-session [] + (encrypt (str (rand-int Integer/MAX_VALUE)))) + ; Routes ; ====== @@ -98,24 +89,10 @@ ; Displays the note (defpage "/:year/:month/:day/:title" {:keys [year month day title theme header-font text-font] :as params} (wrap - (create-short-url params) + (storage/create-short-url params) (select-keys params [:title :theme :header-font :text-font]) (:note (api/get-note (api/build-key [year month day] title))))) -; Update Note Page -(defpage "/:year/:month/:day/:title/edit" {:keys [year month day title]} - (let [noteID (api/build-key [year month day] title)] - (input-form "/update-note" :update - (html (hidden-field :key noteID)) - (:note (api/get-note noteID)) :enter-passwd))) - -; New Note Page -(defpage "/new" {} - (input-form "/post-note" :publish - (html (hidden-field :session-key (create-session)) - (hidden-field {:id :session-value} :session-value)) - (get-message :loading) :set-passwd)) - ; Provides Markdown of the specified note (defpage "/:year/:month/:day/:title/export" {:keys [year month day title]} (when-let [md-text (:note (api/get-note (api/build-key [year month day] title)))] @@ -137,53 +114,55 @@ [:td (get-message :article-views)] [:td (:views stats)]]]))) -; 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 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 (blank? draft)) ; has the note a meaningful content? - ; is the hash code correct? - valid-hash (try - (= (Short/parseShort session-value) - (get-hash (str draft session-key))) - (catch Exception e nil))] - ; check whether the new note can be added - (if (and valid-session valid-draft valid-hash) - ; if yes, we compute the current date, extract a title string from the text, - ; which will be a part of the url and look whether this title is free today; - ; if not, append "-n", where "n" is the next free number - (let [[year month day] (api/get-date) - untrimmed-line (filter #(or (= \- %) (Character/isLetterOrDigit %)) - (-> draft split-lines first (sreplace " " "-") lower-case)) - trim (fn [s] (apply str (drop-while #(= \- %) s))) - title-uncut (-> untrimmed-line trim reverse trim reverse) - max-length (get-setting :max-title-length #(Integer/parseInt %) 80) - ; TODO: replace to ccs/take when it gets fixed - proposed-title (apply str (take max-length title-uncut)) - date [year month day] - title (first (drop-while #(note-exists? (api/build-key date %)) - (cons proposed-title - (map #(str proposed-title "-" (+ 2 %)) (range)))))] - (do - (add-note (api/build-key date title) draft password) - (redirect (url year month day title)))) - (response 400)))) - ; Resolving of a short url (defpage "/:short-url" {:keys [short-url]} - (when-let [params (resolve-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 (url year month day title) long-url (if (empty? rest-params) core-url (util/url core-url rest-params))] (redirect long-url)))) +; New Note Page +(defpage "/new" {} + (input-form "/post-note" :publish + (html (hidden-field :session (storage/create-session)) + (hidden-field {:id :signature} :signature)) + (get-message :loading) :set-passwd)) + +; Update Note Page +(defpage "/:year/:month/:day/:title/edit" {:keys [year month day title]} + (let [noteID (api/build-key [year month day] title)] + (input-form "/update-note" :update + (html (hidden-field :noteID noteID)) + (:note (api/get-note noteID)) :enter-passwd))) + +; Creates New Note from Web +(defpage [:post "/post-note"] {:keys [session note signature password version]} + (if (= signature (api/get-signature session note)) + (let [pid api/domain + psk (storage/get-psk pid)] + (if (storage/valid-publisher? pid) + (let [resp (api/post-note note pid (api/get-signature (str pid psk note)) password)] + (if (get-in resp [:status :success]) + (redirect (:longPath resp)) + (response 400))) + (response 500))) + (response 400))) + +; Updates a note +(defpage [:post "/update-note"] {:keys [noteID note password version]} + (let [pid api/domain + psk (storage/get-psk pid)] + (if (storage/valid-publisher? pid) + (let [resp (api/update-note noteID note pid + (api/get-signature (str pid psk noteID note password)) + password)] + (if (get-in resp [:status :success]) + (redirect (:longPath resp)) + (response 403))) + (response 500)))) + ; Here lives the API (defpage "/api" args @@ -199,7 +178,3 @@ (defpage [:put "/api/note"] {:keys [version noteID note pid signature password]} (generate-string (api/update-note noteID note pid signature password))) - - - - diff --git a/test/NoteHub/test/api.clj b/test/NoteHub/test/api.clj index c8ba67d..085627d 100644 --- a/test/NoteHub/test/api.clj +++ b/test/NoteHub/test/api.clj @@ -10,7 +10,7 @@ (def pid "somePlugin") (def pid2 "somePlugin2") (def note-title (str (apply print-str (get-date)) " hello-world-this-is-a-test-note")) -(def note-url (str domain (apply str (interpose "/" (get-date))) "/hello-world-this-is-a-test-note")) +(def note-url (str "/" (apply str (interpose "/" (get-date))) "/hello-world-this-is-a-test-note")) (defn substring? [a b] (not (= nil (re-matches (re-pattern (str "(?s).*" a ".*")) b)))) (defmacro isnt [arg] `(is (not ~arg))) @@ -43,8 +43,8 @@ (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 (= (:longPath post-response) (:longPath get-response) note-url)) + (is (= (:shortPath post-response) (:shortPath get-response))) (is (= "1" (get-in get-response [:statistics :views]))) (isnt (get-in get-response [:statistics :edited])) (is (= "2" (get-in (get-note (:noteID post-response)) [:statistics :views]))))) diff --git a/test/NoteHub/test/storage.clj b/test/NoteHub/test/storage.clj index 68a0e34..e5e9c3c 100644 --- a/test/NoteHub/test/storage.clj +++ b/test/NoteHub/test/storage.clj @@ -43,11 +43,7 @@ test-note)) (is (valid-password? (build-key date test-title) "12345qwert")) (is (= (do - (update-note (build-key date test-title) "update" "12345qwert") - (get-note (build-key date test-title))) - "update")) - (is (= (do - (update-note (build-key date test-title) "not authorized" "44444") + (edit-note (build-key date test-title) "update") (get-note (build-key date test-title))) "update"))) (testing "of the note access" diff --git a/test/NoteHub/test/views/pages.clj b/test/NoteHub/test/views/pages.clj index 1bebd7b..79dd3fb 100644 --- a/test/NoteHub/test/views/pages.clj +++ b/test/NoteHub/test/views/pages.clj @@ -1,6 +1,6 @@ (ns NoteHub.test.views.pages (:use [NoteHub.views.pages] - [NoteHub.api :only [build-key get-date]] + [NoteHub.api :only [build-key get-signature get-date]] [noir.util.test] [NoteHub.views.common :only [url]] [NoteHub.storage] @@ -38,9 +38,9 @@ (is (has-status (send-request [:post "/post-note"] - {:session-key session-key - :draft test-note - :session-value (str (get-hash (str test-note session-key)))}) 302)) + {:session session-key + :note test-note + :signature (get-signature session-key test-note)}) 302)) (is (note-exists? (build-key date title))) (is (substring? "Hello _world_" ((send-request (url year month day title)) :body))) @@ -51,35 +51,33 @@ (deftest note-update (let [session-key (create-session) date (get-date) - title "test-note" - [year month day] date] + title "this-is-a-test-note" + [year month day] date + hash (get-signature session-key test-note)] (testing "Note update" (is (has-status (send-request [:post "/post-note"] - {:session-key session-key - :draft "test note" + {:session session-key + :note test-note :password "qwerty" - :session-value (str (get-hash (str "test note" session-key)))}) 302)) + :signature hash}) 302)) (is (note-exists? (build-key date title))) - (is (substring? "test note" - ((send-request (url year month day title)) :body))) + (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" + {: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 (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" + {: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 (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)))))))) @@ -100,13 +98,3 @@ (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 hash-function - (testing "Self-made hash function" - (testing "for correct hashes" - (is (= 0 (get-hash ""))) - (is (= 6178 (get-hash "test тест"))) - (is (= 6178 (get-hash (str "test\n \rтест")))) - (is (= 274 (get-hash "Hello world!")))) - (testing "for a wrong hash" - (is (not= 6178 (get-hash "wrong hash"))))))