From 6df0d7990d76bf5332a77bd073f8a272b1ce8cd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=BCller?= Date: Sat, 15 Aug 2015 20:40:21 +0200 Subject: [PATCH 01/38] clojure notehub removed; let's start from scratch --- .gitignore | 13 -- Procfile | 1 - project.clj | 26 ---- scripts/expose.sh | 10 -- scripts/run.sh | 9 -- src/notehub/api.clj | 126 ---------------- src/notehub/css.clj | 260 ---------------------------------- src/notehub/handler.clj | 169 ---------------------- src/notehub/storage.clj | 147 ------------------- src/notehub/views.clj | 124 ---------------- system.properties | 1 - test/notehub/test/api.clj | 214 ---------------------------- test/notehub/test/handler.clj | 129 ----------------- test/notehub/test/storage.clj | 70 --------- 14 files changed, 1299 deletions(-) delete mode 100644 Procfile delete mode 100644 project.clj delete mode 100755 scripts/expose.sh delete mode 100755 scripts/run.sh delete mode 100644 src/notehub/api.clj delete mode 100644 src/notehub/css.clj delete mode 100644 src/notehub/handler.clj delete mode 100644 src/notehub/storage.clj delete mode 100644 src/notehub/views.clj delete mode 100644 system.properties delete mode 100644 test/notehub/test/api.clj delete mode 100644 test/notehub/test/handler.clj delete mode 100644 test/notehub/test/storage.clj diff --git a/.gitignore b/.gitignore index e653c91..24ebcf5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1 @@ -Makefile -PrivateMakefile dump.rdb -resources/public/cljs -pom.xml -*jar -/lib/ -/classes/ -.lein-* -.crossover-cljs -target/ -.nrepl-port -.idea/ -NoteHub.iml diff --git a/Procfile b/Procfile deleted file mode 100644 index 0fef382..0000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: lein with-profile production ring server-headless $PORT diff --git a/project.clj b/project.clj deleted file mode 100644 index 42b4412..0000000 --- a/project.clj +++ /dev/null @@ -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"]) diff --git a/scripts/expose.sh b/scripts/expose.sh deleted file mode 100755 index f70f4fc..0000000 --- a/scripts/expose.sh +++ /dev/null @@ -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 diff --git a/scripts/run.sh b/scripts/run.sh deleted file mode 100755 index e62edac..0000000 --- a/scripts/run.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -export DEVMODE=1 - -if ! pgrep "redis-server" > /dev/null; then - redis-server & -fi - -lein ring server-headless 8080 diff --git a/src/notehub/api.clj b/src/notehub/api.clj deleted file mode 100644 index 2a920f5..0000000 --- a/src/notehub/api.clj +++ /dev/null @@ -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))}))) diff --git a/src/notehub/css.clj b/src/notehub/css.clj deleted file mode 100644 index 4597010..0000000 --- a/src/notehub/css.clj +++ /dev/null @@ -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" - }] - ))) diff --git a/src/notehub/handler.clj b/src/notehub/handler.clj deleted file mode 100644 index 0f30409..0000000 --- a/src/notehub/handler.clj +++ /dev/null @@ -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)})) - diff --git a/src/notehub/storage.clj b/src/notehub/storage.clj deleted file mode 100644 index 450784a..0000000 --- a/src/notehub/storage.clj +++ /dev/null @@ -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")))) diff --git a/src/notehub/views.clj b/src/notehub/views.clj deleted file mode 100644 index 391e75a..0000000 --- a/src/notehub/views.clj +++ /dev/null @@ -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) "—" 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 -NoteHub API Testing - - - - -
- - - - - - - -
- -
-
- - -
- Submit -
-
- - -

Request

-
-    
-

Response

-
-    
-
- - From da3535e06ce698a58545c22dae82a551d14cd010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=BCller?= Date: Wed, 30 Sep 2015 20:21:03 +0200 Subject: [PATCH 25/38] styling improved --- resources/public/js/publishing.js | 25 ------------------------- resources/public/style.css | 4 ++-- 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/resources/public/js/publishing.js b/resources/public/js/publishing.js index a65aab5..b674a72 100644 --- a/resources/public/js/publishing.js +++ b/resources/public/js/publishing.js @@ -4,28 +4,8 @@ var timer = null; var timerDelay = iosDetected ? 800 : 400; var show = function(elem) { elem.style.display = "block" } var $note, $action, $preview, $plain_password, $input_elems, $dashed_line, $proposed_title, updatePreview; -var firstLines_; var backendTimer; -function updateProposedTitle() { - clearTimeout(backendTimer); - backendTimer = setTimeout(function () { - var http = new XMLHttpRequest(); - var url = "/propose-title"; - http.open("POST", url, true); - http.onreadystatechange = function() { - if(http.readyState == 4 && http.status == 200) { - var now = new Date(); - $proposed_title.innerHTML = - "Expected URL: https://www.notehub.org/" + - now.getFullYear() + "/" + (now.getMonth()+1) + "/" + now.getDate() + "/" + - http.responseText; - } - } - http.send($note.value); - }, 500); -} - function md2html(input){ return marked(input); } @@ -46,11 +26,6 @@ function onLoad () { show($dashed_line); show($input_elems); $preview.innerHTML = md2html(content); - var firstLines = content.split("\n", 2); - if(firstLines_ != firstLines) { - firstLines_ = firstLines; - updateProposedTitle(); - } }, delay); }; if($action){ diff --git a/resources/public/style.css b/resources/public/style.css index 80a14e8..9771e6a 100644 --- a/resources/public/style.css +++ b/resources/public/style.css @@ -140,7 +140,7 @@ h6 { #hero h2 { margin: 2em; - font-family: 'Helvetica Neue', 'Helvetica', 'Arial', 'Lucida Grande', 'sans-serif'; + font-family: 'Helvetica Neue', 'Helvetica', 'Arial', 'Lucida Grande', sans-serif; font-weight: 300; } @@ -149,7 +149,7 @@ article { flex: 1; text-align: justify; margin-top: 3em; - font-family: Georgia, 'Georgia'; + font-family: Georgia, 'Georgia', serif; margin-right: auto; margin-left: auto; } From b05f103a3785b85339f490da86a7ef0c8a10d299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=BCller?= Date: Wed, 30 Sep 2015 20:40:05 +0200 Subject: [PATCH 26/38] /new page implemented --- messages.json | 27 ----------------------- package.json | 1 + resources/new.html | 33 ++++++++++++++++++++++++++++ resources/public/js/publishing.js | 36 +++++++++++++++++-------------- resources/template.html | 4 ++-- server.js | 15 +++++++++++-- src/page.js | 11 +++++++--- 7 files changed, 77 insertions(+), 50 deletions(-) delete mode 100644 messages.json create mode 100644 resources/new.html diff --git a/messages.json b/messages.json deleted file mode 100644 index bc7364f..0000000 --- a/messages.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "page-title": "Free Pastebin for One-Off Markdown Publishing", - "title": "Free and Hassle-free Pastebin for Markdown Notes.", - "name": "NoteHub", - "new-page": "New Page", - "status-404": "Not Found", - "status-400": "Bad Request", - "status-403": "Forbidden", - "status-500": "Internal Server Error", - "footer": "Source code on [GitHub](https://github.com/chmllr/NoteHub) · Hosted on [Heroku](http://heroku.com) · DB on [RedisLabs](http://redislabs.com) · SSL by [CloudFlare](http://cloudflare.com)
Created by [@chmllr](https://github.com/chmllr)", - "loading": "Loading...", - "set-passwd": "Password for editing", - "enter-passwd": "Password", - "publish": "Publish", - "update": "Save", - "published": "Published", - "publisher": "Publisher", - "edited": "Edited", - "views": "Article Views", - "statistics": "Statistics", - "stats": "statistics", - "export": "export", - "notehub": "⌂ notehub", - "edit": "edit", - "short-url": "short url", - "api-title": "API" -} \ No newline at end of file diff --git a/package.json b/package.json index 07d3198..bce9858 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "express": "^4.13.3", "lru-cache": "^2.6.5", "marked": "^0.3.5", + "md5": "^2.0.0", "sequelize": "^3.8.0", "sqlite3": "^3.1.0" } diff --git a/resources/new.html b/resources/new.html new file mode 100644 index 0000000..eca2b64 --- /dev/null +++ b/resources/new.html @@ -0,0 +1,33 @@ + + + + + NoteHub — New Page + + + + + + + + + +
+ +
+
+ + + + + +
+
+ + + diff --git a/resources/public/js/publishing.js b/resources/public/js/publishing.js index b674a72..ac2abf6 100644 --- a/resources/public/js/publishing.js +++ b/resources/public/js/publishing.js @@ -1,41 +1,45 @@ -var $ = function(id){ return document.getElementById(id); } +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 show = function(elem) { + elem.style.display = "block" +} var $note, $action, $preview, $plain_password, $input_elems, $dashed_line, $proposed_title, updatePreview; var backendTimer; -function md2html(input){ +function md2html(input) { return marked(input); } -function onLoad () { +function onLoad() { $note = $("note"); - $action = $("action"); + $action = document.getElementsByTagName("form")[0].method; $preview = $("preview"); $plain_password = $("plain-password"); $proposed_title = $("proposed-title"); $input_elems = $("input-elems"); $dashed_line = $("dashed-line"); - updatePreview = function(){ + updatePreview = function() { clearTimeout(timer); var content = $note.value; var delay = Math.min(timerDelay, timerDelay * (content.length / 400)); - timer = setTimeout(function(){ + timer = setTimeout(function() { show($dashed_line); show($input_elems); $preview.innerHTML = md2html(content); }, delay); }; - if($action){ - 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); - $plain_password.value = null; - $("signature").value = md5($("session").value + $note.value); - } - if(iosDetected) $note.className += " ui-border"; else $note.focus(); + 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); + $plain_password.value = null; + $("signature").value = md5($("session").value + $note.value); } + if (iosDetected) $note.className += " ui-border"; + else $note.focus(); } diff --git a/resources/template.html b/resources/template.html index 06048a6..c6c16b1 100644 --- a/resources/template.html +++ b/resources/template.html @@ -2,8 +2,8 @@ NoteHub — %TITLE% - - + + diff --git a/server.js b/server.js index 980849e..905a393 100644 --- a/server.js +++ b/server.js @@ -1,15 +1,26 @@ var express = require('express'); var page = require('./src/page'); var storage = require('./src/storage'); +var md5 = require('md5'); var LRU = require("lru-cache"); var app = express(); var CACHE = new LRU(30); +var getTimeStamp = () => { + var timestamp = new Date().getTime(); + timestamp = Math.floor(timestamp / 10000000); + return (timestamp).toString(16) +} + app.use(express.static(__dirname + '/resources/public')); app.get('/new', function (req, res) { - res.send("opening new note mask") + res.send(page.newNotePage(getTimeStamp() + md5(Math.random()))); +}); + +app.post('/note', function (req, res) { + console.log(req.params); }); app.get("/:year/:month/:day/:title", function (req, res) { @@ -22,7 +33,7 @@ app.get(/\/([a-zA-Z0-9]*)/, function (req, res) { var link = req.params["0"].toLowerCase(); if (CACHE.has(link)) res.send(CACHE.get(link)); else storage.getNote(link).then(note => { - var content = page.build(note); + var content = page.buildNote(note); CACHE.set(link, content); res.send(content); }); diff --git a/src/page.js b/src/page.js index e18a2bb..c346a76 100644 --- a/src/page.js +++ b/src/page.js @@ -1,10 +1,15 @@ var marked = require("marked"); var fs = require("fs"); -var template = fs.readFileSync("resources/template.html", "utf-8"); -var buildHTML = (id, title, content) => template +var pageTemplate = fs.readFileSync("resources/template.html", "utf-8"); +var newNoteTemplate = fs.readFileSync("resources/new.html", "utf-8"); +var buildPage = (id, title, content) => pageTemplate .replace("%TITLE%", title) .replace(/%LINK%/g, id) .replace("%CONTENT%", content); -module.exports.build = note => buildHTML(note.id, note.title, marked(note.text)); +module.exports.buildNote = note => buildPage(note.id, note.title, marked(note.text)); + +module.exports.newNotePage = session => newNoteTemplate + .replace("%METHOD%", "POST") + .replace("%SESSION%", session); From 3228de20f3854e2d5f581cf9ec276bf6832635d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=BCller?= Date: Tue, 6 Oct 2015 22:15:38 +0200 Subject: [PATCH 27/38] new css styles --- resources/public/style.css | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/resources/public/style.css b/resources/public/style.css index 9771e6a..d3e48bd 100644 --- a/resources/public/style.css +++ b/resources/public/style.css @@ -106,8 +106,8 @@ body { } h1, h2, h3, h4, h5, h6 { - font-family: 'PT Serif', 'Georgia'; - font-weight: bold; + font-family: "PT Serif", Georgia, serif; + font-weight: bolder; } h1 { @@ -149,7 +149,7 @@ article { flex: 1; text-align: justify; margin-top: 3em; - font-family: Georgia, 'Georgia', serif; + font-family: sans-serif; margin-right: auto; margin-left: auto; } @@ -189,7 +189,13 @@ article img { article p { line-height: 140%; - font-size: 1.2em; + font-size: 1.05em; +} + +blockquote { + font-family: "PT Serif", Georgia, serif; + padding-left: 1em; + border-left: 4px solid #097; } article > h1:first-child { @@ -249,8 +255,6 @@ textarea { table { border-collapse: collapse; width: 100%; - font-family: 'Helvetica Neue', 'Helvetica', 'Arial', 'Lucida Grande', 'sans-serif'; - font-weight: 300; } th { From 8cc0360c577372b849d7271d669bc01006cb8f94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=BCller?= Date: Sat, 17 Oct 2015 21:57:56 +0200 Subject: [PATCH 28/38] POST request parsing added --- package.json | 1 + resources/public/js/publishing.js | 3 ++- server.js | 19 +++++++++++++++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index bce9858..4e582e4 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ }, "homepage": "https://github.com/chmllr/NoteHub", "dependencies": { + "body-parser": "^1.14.1", "express": "^4.13.3", "lru-cache": "^2.6.5", "marked": "^0.3.5", diff --git a/resources/public/js/publishing.js b/resources/public/js/publishing.js index ac2abf6..03b34b9 100644 --- a/resources/public/js/publishing.js +++ b/resources/public/js/publishing.js @@ -38,7 +38,8 @@ function onLoad() { $("publish-button").onclick = function(e) { if ($plain_password.value != "") $("password").value = md5($plain_password.value); $plain_password.value = null; - $("signature").value = md5($("session").value + $note.value); + $("signature").value = md5($("session").value + + $note.value.replace(/[\n\r]/g, "")); } if (iosDetected) $note.className += " ui-border"; else $note.focus(); diff --git a/server.js b/server.js index 905a393..f570187 100644 --- a/server.js +++ b/server.js @@ -2,9 +2,13 @@ var express = require('express'); var page = require('./src/page'); var storage = require('./src/storage'); var md5 = require('md5'); -var LRU = require("lru-cache"); +var LRU = require("lru-cache") +var bodyParser = require('body-parser'); var app = express(); + +app.use(bodyParser.urlencoded({ extended: true })); + var CACHE = new LRU(30); var getTimeStamp = () => { @@ -20,7 +24,13 @@ app.get('/new', function (req, res) { }); app.post('/note', function (req, res) { - console.log(req.params); + var body = req.body, session = body.session, note = body.note; + if (session.indexOf(getTimeStamp()) != 0) + return sendResponse(res, 400, "Session expired"); + var expectedSignature = md5(session + note.replace(/[\n\r]/g, "")); + if (expectedSignature != body.signature) + return sendResponse(res, 400, "Signature mismatch"); + sendResponse(res, 200, JSON.stringify(body)); }); app.get("/:year/:month/:day/:title", function (req, res) { @@ -39,6 +49,11 @@ app.get(/\/([a-zA-Z0-9]*)/, function (req, res) { }); }); +var sendResponse = (res, code, message) => { + res.status(code); + res.send(message); +}; + var server = app.listen(3000, function () { console.log('NoteHub server listening on port %s', server.address().port); }); From 7ff35d2da34b8a9bf046510be65d48cd1b82a08b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=BCller?= Date: Sat, 17 Oct 2015 22:39:35 +0200 Subject: [PATCH 29/38] note creation implemented --- server.js | 2 +- src/storage.js | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/server.js b/server.js index f570187..60509fd 100644 --- a/server.js +++ b/server.js @@ -30,7 +30,7 @@ app.post('/note', function (req, res) { var expectedSignature = md5(session + note.replace(/[\n\r]/g, "")); if (expectedSignature != body.signature) return sendResponse(res, 400, "Signature mismatch"); - sendResponse(res, 200, JSON.stringify(body)); + storage.addNote(note, body.password).then(note => res.redirect("/" + note.id)); }); app.get("/:year/:month/:day/:title", function (req, res) { diff --git a/src/storage.js b/src/storage.js index 4395336..5a208d5 100644 --- a/src/storage.js +++ b/src/storage.js @@ -16,7 +16,7 @@ var Note = sequelize.define('Note', { published: { type: Sequelize.DATE, defaultValue: Sequelize.NOW }, edited: { type: Sequelize.DATE, allowNull: true, defaultValue: null }, password: Sequelize.STRING(16), - views: Sequelize.INTEGER, + views: { type: Sequelize.INTEGER, defaultValue: 0 } }); module.exports.getNote = id => { @@ -30,3 +30,21 @@ module.exports.getNoteId = deprecatedId => { where: { deprecatedId: deprecatedId } }).then(note => note.id); } + +var generateId = () => [1, 1, 1, 1, 1] + .map(() => { + var code = Math.floor(Math.random() * 36); + return String.fromCharCode(code + (code < 10 ? 48 : 87)); + }) + .join(""); + +var getFreeId = () => { + var id = generateId(); + return Note.findById(id).then(result => result ? getFreeId() : id); +}; + +module.exports.addNote = (note, password) => getFreeId().then(id => Note.create({ + id: id, + text: note, + password: password +})); \ No newline at end of file From a717d15664642143a42e0460ae13981bc787f3e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=BCller?= Date: Sat, 17 Oct 2015 22:49:37 +0200 Subject: [PATCH 30/38] /export added --- server.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/server.js b/server.js index 60509fd..1db70ef 100644 --- a/server.js +++ b/server.js @@ -24,10 +24,10 @@ app.get('/new', function (req, res) { }); app.post('/note', function (req, res) { - var body = req.body, session = body.session, note = body.note; + var body = req.body, session = body.session, note = body.note; if (session.indexOf(getTimeStamp()) != 0) return sendResponse(res, 400, "Session expired"); - var expectedSignature = md5(session + note.replace(/[\n\r]/g, "")); + var expectedSignature = md5(session + note.replace(/[\n\r]/g, "")); if (expectedSignature != body.signature) return sendResponse(res, 400, "Signature mismatch"); storage.addNote(note, body.password).then(note => res.redirect("/" + note.id)); @@ -39,8 +39,14 @@ app.get("/:year/:month/:day/:title", function (req, res) { .then(id => res.redirect("/" + id)); }); -app.get(/\/([a-zA-Z0-9]*)/, function (req, res) { - var link = req.params["0"].toLowerCase(); +app.get(/\/([a-z0-9]+\/export)/, function (req, res) { + var link = req.params["0"].replace("/export", ""); + res.set({ 'Content-Type': 'text/plain', 'Charset': 'utf-8' }); + storage.getNote(link).then(note => res.send(note.text)); +}); + +app.get(/\/([a-z0-9]+)/, function (req, res) { + var link = req.params["0"]; if (CACHE.has(link)) res.send(CACHE.get(link)); else storage.getNote(link).then(note => { var content = page.buildNote(note); From 147b935a1191a3c42719c92734c5f81fc1386701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=BCller?= Date: Sat, 17 Oct 2015 22:50:33 +0200 Subject: [PATCH 31/38] pre/code styling fix --- resources/public/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/public/style.css b/resources/public/style.css index d3e48bd..105e098 100644 --- a/resources/public/style.css +++ b/resources/public/style.css @@ -214,7 +214,7 @@ article > h1:first-child { code, pre { - font-size: 1.2em; + font-size: 1.05em; background: #efefef; font-family: monospace; } From c0ea94ad7b17c5f018c84169ac215236d61ff6a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=BCller?= Date: Sat, 17 Oct 2015 22:59:37 +0200 Subject: [PATCH 32/38] refactoring --- server.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/server.js b/server.js index 1db70ef..4f13dd5 100644 --- a/server.js +++ b/server.js @@ -26,10 +26,10 @@ app.get('/new', function (req, res) { app.post('/note', function (req, res) { var body = req.body, session = body.session, note = body.note; if (session.indexOf(getTimeStamp()) != 0) - return sendResponse(res, 400, "Session expired"); + return res.status(400).send("Session expired"); var expectedSignature = md5(session + note.replace(/[\n\r]/g, "")); if (expectedSignature != body.signature) - return sendResponse(res, 400, "Signature mismatch"); + return res.status(400).send("Signature mismatch"); storage.addNote(note, body.password).then(note => res.redirect("/" + note.id)); }); @@ -55,11 +55,6 @@ app.get(/\/([a-z0-9]+)/, function (req, res) { }); }); -var sendResponse = (res, code, message) => { - res.status(code); - res.send(message); -}; - var server = app.listen(3000, function () { console.log('NoteHub server listening on port %s', server.address().port); }); From b078891190dafb14c0a585a6ab66f72c29d980ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=BCller?= Date: Sat, 17 Oct 2015 23:40:13 +0200 Subject: [PATCH 33/38] /edit page implemented --- resources/{new.html => edit.html} | 5 +++-- resources/public/js/publishing.js | 4 ++-- server.js | 6 ++++++ src/page.js | 12 +++++++++--- 4 files changed, 20 insertions(+), 7 deletions(-) rename resources/{new.html => edit.html} (88%) diff --git a/resources/new.html b/resources/edit.html similarity index 88% rename from resources/new.html rename to resources/edit.html index eca2b64..ef0b928 100644 --- a/resources/new.html +++ b/resources/edit.html @@ -15,11 +15,12 @@
-
+ + - +