;;;; -*- coding: utf-8; mode: lisp -*- ;;;; Copyright 2007-2009, Matthias Andreas Benkard. ;;;------------------------------------------------------------------------ ;;; This file is part of The Mulkblog Project. ;;; ;;; The Mulkblog Project is free software. You can redistribute it and/or ;;; modify it under the terms of the Affero General Public License as ;;; published by Affero, Inc.; either version 1 of the License, or ;;; (at your option) any later version. ;;; ;;; The Mulkblog Project is distributed in the hope that it will be ;;; useful, but WITHOUT ANY WARRANTY; without even the implied warranty ;;; of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;;; Affero General Public License for more details. ;;; ;;; You should have received a copy of the Affero General Public ;;; License in the COPYING file that comes with The Mulkblog Project; if ;;; not, write to Affero, Inc., 510 Third Street, Suite 225, San ;;; Francisco, CA 94107 USA. ;;;------------------------------------------------------------------------ (in-package #:mulk.journal) (defun link-to (action &key comment-id post-id (absolute nil)) (with-output-to-string (out) (format out "~A" (if absolute "http://matthias.benkard.de/journal" ;; When testing on the local webserver, don't ;; use /journal as a relative URI, because it ;; won't work. (if (eq *site* :mst-plus) (http-getenv "SCRIPT_NAME") "/journal"))) (multiple-value-call #'(lambda (&rest args) (apply #'format out args)) (ecase action (:index "") (:full-index "/?full") (:view-atom-feed (values "/feed")) (:view-comment-feed (values "/comment-feed")) (:view (cond (comment-id (values "/~D#comment-~D" post-id comment-id)) (post-id (values "/~D" post-id)) (t "/"))) ((:edit :preview) (values "/~D/preview" post-id)) (:post-comment (values "/~D" post-id)) (:trackback (values "/~D/trackback" post-id)) (:view-atom-entry (values "/~D/atom" post-id)) (:save (values "/~D/save" post-id)) (:moderation-page "/moderate") (:css "/../journal.css"))))) (defun show-comment-feed () #.(locally-enable-sql-reader-syntax) (revalidate-cache-or-die "application/atom+xml; charset=UTF-8") (when (eq *mode* :http) (http-add-header "Last-Modified" (http-timestamp (compute-journal-last-modified-date))) (http-add-header "Content-Language" "de") (http-send-headers "application/atom+xml; charset=UTF-8")) (flet ((atom-time (time) (format-date nil "%4yr%-%2mon%-%2day%T%2hr%:%2min%:%2sec%Z" time 0))) (with-xml-output (*standard-output* :encoding "utf-8") (with-tag ("feed" '(("xmlns" "http://www.w3.org/2005/Atom"))) (with-tag ("title") (xml-as-is "Kommentare — Kompottkins Weisheiten")) (emit-simple-tags :updated (atom-time (max (or (single-object (select [max [slot-value 'journal-entry 'date]] :from [journal-entry] :flatp t)) 0) (or (single-object (select [max [slot-value 'journal-entry 'last-modification]] :from [journal-entry] :flatp t)) 0))) :id "urn:uuid:9cd7a24c-10a6-4895-a97b-8df6b426e4a0") (with-tag ("subtitle") (xml-as-is "Geschwafel zum Geschwafel eines libertärsozialistischen Geeks")) (with-tag ("author") (emit-simple-tags :name "Various")) (with-tag ("link" `(("rel" "alternate") ("type" "text/html") ("href" ,(link-to :index :absolute t))))) (with-tag ("link" `(("rel" "self") ("type" "application/atom+xml") ("href" ,(link-to :view-comment-feed :absolute t))))) (let ((number 0)) (dolist (journal-comment (select 'journal-comment :where [= [slot-value 'journal-comment 'spam-p] "f"] :order-by '(([date] :desc)) :flatp t)) (with-slots (entry uuid date body author website spam-p id) journal-comment (unless spam-p (incf number) (with-tag ("entry") (emit-simple-tags :title (format nil "Kommentar zu: ~A" (title-of entry)) :id (format nil "urn:uuid:~(~A~)" uuid) :updated (atom-time date) :published (atom-time date)) (with-tag ("link" `(("rel" "alternate") ("type" "text/html") ("href" ,(link-to :view :comment-id id :post-id (id-of entry) :absolute t))))) (when (<= number 8) ;; We only include the body for the most recent ;; posts in order to save bandwidth. (with-tag ("content" `(("type" "xhtml") ("xml:lang" "de") ("xml:base" ,(link-to :index :absolute t)))) (with-tag ("div" '(("xmlns" "http://www.w3.org/1999/xhtml"))) (xml-as-is (with-yaclml-output-to-string (<:as-html (render-comment-body body))))))))))))))) #.(restore-sql-reader-syntax-state)) (defun show-atom-entry () #.(locally-enable-sql-reader-syntax) (revalidate-cache-or-die "application/atom+xml; charset=UTF-8") (when (eq *mode* :http) (http-add-header "Last-Modified" (http-timestamp (compute-journal-last-modified-date))) (http-add-header "Content-Language" "de") (http-send-headers "application/atom+xml; charset=UTF-8")) (with-xml-output (*standard-output* :encoding "utf-8") (show-atom-entry-xml (find-entry *post-number*) :full-content t :include-edit-links t))) (defun show-atom-entry-xml (journal-entry &key full-content include-edit-links) (flet ((atom-time (time) (format-date nil "%4yr%-%2mon%-%2day%T%2hr%:%2min%:%2sec%Z" time 0))) (with-slots (title date body categories last-modification id) journal-entry (with-tag ("entry") (emit-simple-tags :title title :id (format nil "urn:uuid:~(~A~)" (uuid-of journal-entry)) :updated (atom-time (or last-modification date)) :published (atom-time date)) (with-tag ("link" `(("rel" "alternate") ("type" "text/html") ("href" ,(link-to :view :post-id id :absolute t))))) (when include-edit-links (with-tag ("link" `(("rel" "service.edit") ("type" "application/atom+xml") ("href" ,(link-to :view-atom-entry :post-id id :absolute t)) ("title" ,title))))) (when full-content ;; Escaped HTML or embedded XHTML? What shall we prefer? #+(or) (with-tag ("content" `(("type" "html") ("xml:lang" "de") ("xml:base" ,(link-to :index :absolute t)))) (xml-out (if (equal (entry-type-of journal-entry) "html") (body-of journal-entry) (journal-markup->html (body-of journal-entry))))) (with-tag ("content" `(("type" "xhtml") ("xml:lang" "de") ("xml:base" ,(link-to :index :absolute t)))) (with-tag ("div" '(("xmlns" "http://www.w3.org/1999/xhtml"))) (xml-as-is (if (equal (entry-type-of journal-entry) "html") (body-of journal-entry) (journal-markup->html (body-of journal-entry)))))))))) #.(restore-sql-reader-syntax-state)) (defun show-atom-feed (&key include-edit-links full-content) #.(locally-enable-sql-reader-syntax) (revalidate-cache-or-die "application/atom+xml; charset=UTF-8") (when (eq *mode* :http) (http-add-header "Last-Modified" (http-timestamp (compute-journal-last-modified-date))) (http-add-header "Content-Language" "de") (http-send-headers "application/atom+xml; charset=UTF-8")) (flet ((atom-time (time) (format-date nil "%4yr%-%2mon%-%2day%T%2hr%:%2min%:%2sec%Z" time 0))) (with-xml-output (*standard-output* :encoding "utf-8") (with-tag ("feed" '(("xmlns" "http://www.w3.org/2005/Atom"))) (emit-simple-tags :title "Kompottkins Weisheiten" :updated (atom-time (max (or (single-object (select [max [slot-value 'journal-entry 'date]] :from [journal-entry] :flatp t)) 0) (or (single-object (select [max [slot-value 'journal-entry 'last-modification]] :from [journal-entry] :flatp t)) 0))) :id "urn:uuid:88ad4730-90bc-4cc1-9e1f-d4cdb9ce177c") (with-tag ("subtitle") (xml-as-is "Geschwafel eines libertärsozialistischen Geeks")) (with-tag ("author") (emit-simple-tags :name "Matthias Benkard")) (with-tag ("link" `(("rel" "alternate") ("type" "text/html") ("href" ,(link-to :index :absolute t))))) (with-tag ("link" `(("rel" "self") ("type" "application/atom+xml") ("href" ,(link-to :view-atom-feed :absolute t))))) (when include-edit-links (with-tag ("link" `(("rel" "service.post") ("type" "application/atom+xml") ("href" ,(link-to :view-atom-entry :absolute t))))) (with-tag ("link" `(("rel" "service.feed") ("type" "application/atom+xml") ("href" ,(link-to :view-atom-entry :absolute t))))) #+(or) (with-tag ("link" `(("rel" "service.categories") ("type" "application/atom+xml") ("href" ,(link-to :view-atom-entry :absolute t)))))) (let ((number 0)) (dolist (journal-entry (select 'journal-entry :order-by '(([date] :desc)) :flatp t)) ;; We only include the body for the most recent posts in ;; order to save bandwidth. (show-atom-entry-xml journal-entry :full-content (or (and (last-modification-of journal-entry) (> (last-modification-of journal-entry) (- (get-universal-time) (* 30 24 60 60)))) (<= number 8) full-content) :include-edit-links include-edit-links) (incf number)))))) #.(restore-sql-reader-syntax-state)) (let ((scanner (ppcre:create-scanner "(\\n|\\r|\\r\\n)(\\n|\\r|\\r\\n)+"))) (defun render-comment-body (text) (loop for last-position = 0 then (cadr matches) for matches = (ppcre:all-matches scanner text) then (cddr matches) while (not (endp matches)) do (<:p (<:as-html (subseq text last-position (car matches)))) finally (<:p (<:as-html (subseq text last-position)))))) (defun show-journal-entry (journal-entry &key (comments-p nil)) (with-slots (id title body categories date type) journal-entry (show-journal-entry-with-components id title body categories date type (comments-about journal-entry :ordered-p t :ham-p t) comments-p (trackbacks-about journal-entry :ordered-p t :ham-p t)))) (defun show-journal-entry-with-components (id title body categories posting-date type comments comments-p trackbacks) (unless *full-entry-view* (<:tr (<:td (<:a :href (link-to :view :post-id id) (<:as-html title))) (<:td :style "text-align: right" (<:as-is (format-date nil "%day%.%mon%.%yr%, %hr%:%2min%" posting-date))) (<:td (<:a :href (link-to :view :post-id id) (<:as-is (format nil "~D Kommentar~:*~[e~;~:;e~]" (length comments))))))) (when *full-entry-view* (<:div :class :journal-entry (<:h2 (<:a :href (link-to :view :post-id id) (<:as-html title))) (<:div :class :journal-entry-header (<:span :class :journal-entry-date (<:as-html (format-date nil "%@day-of-week%, den %day%.%mon%.%yr%, %hr%:%2min%." posting-date))) (unless (null categories) (<:span :class :journal-entry-category (<:as-html (format nil "Abgeheftet unter ..."))))) (<:div :class :journal-entry-body (<:as-is (if (equal type "html") body (journal-markup->html body)))) (<:div :class :journal-entry-footer (<:form :class :journal-entry-delete-button-form :style "display: inline;" :method "post" :action (link-to :index) (<:div :style "display: inline;" (<:input :type "hidden" :name "action" :value "delete") (<:input :type "hidden" :name "id" :value (prin1-to-string id)) (<:button :type "submit" (<:as-is "Löschen")))) " | " (<:form :class :journal-entry-edit-button-form :style "display: inline;" :method "get" :action (link-to :edit :post-id id) (<:div :style "display: inline;" (<:input :type "hidden" :name "id" :value (prin1-to-string id)) (<:button :type "submit" (<:as-is "Bearbeiten")))) " | " (<:a :href (link-to :view :post-id id) (<:as-is (format nil "~D Kommentar~:*~[e~;~:;e~]" (length comments)))))) (when (and comments-p (not (null comments))) (<:div :class :journal-comments (<:h2 "Kommentare") (dolist (comment comments) (show-comment comment)))) (when (and comments-p (not (null trackbacks))) (<:div :class :journal-comments (<:h2 "Trackbacks") (dolist (trackback trackbacks) (show-trackback trackback)))) (when comments-p (<:as-is (format nil "" (link-to :view :post-id id :absolute t) (ppcre:regex-replace "--" title "—") (link-to :trackback :post-id id :absolute t))) (<:div :class :journal-new-comment (<:h2 "Neuen Kommentar schreiben") (<:p (<:as-is "Bitte beachten Sie, daß E-Mail-Adressen niemals veröffentlicht werden und nur von Matthias eingesehen werden können.")) (<:p (<:strong "Hinweise: ") "Diese Website verwendet " (<:a :href "http://akismet.com/" "Akismet") " zur Spamerkennung. " (<:as-is "E-Mail-Adressen werden auch gegenüber Akismet unter Verschluß gehalten. Nur unformatierter Text ist erlaubt. Leerzeilen trennen Absätze.")) (<:form :action (link-to :view :post-id id) :method "post" :accept-charset #+(or) "ISO-10646-UTF-1" "UTF-8" :enctype #+(or) "multipart/form-data" "application/x-www-form-urlencoded" (<:div :style "display: none" (<:input :type "hidden" :name "id" :value (prin1-to-string id)) (<:input :type "hidden" :name "action" :value "post-comment")) (<:div :style "display: table" (loop for (name . desc) in '(("author" . "Name (nötig)") ("email" . "E-Mail") ("website" . "Website")) do (<:div :style "display: table-row" (<:div :style "display: table-cell; vertical-align: top" (<:label :for name :style "vertical-align: top" (<:as-is (format nil "~A: " desc)))) (<:div :style "display: table-cell;" (<:input :type "text" :name name :id name)))) (<:div :style "display: table-row" (<:div :style "display: table-cell; vertical-align: top" (<:label :for "comment-body" :style "vertical-align: top" (<:as-html "Kommentar: "))) (<:div :style "display: table-cell" (<:textarea :name "comment-body" :id "comment-body" :rows 10 :cols 40)))) (<:div (<:button :type "submit" (<:as-is "Veröffentlichen")))))))) (defun call-with-web-journal (page-title thunk) ;; TODO: Check how to make Squid not wait for the CGI script's ;; termination, which makes generating a Last-Modified header ;; feel slower to the end user rather than faster. ;; (when (eq *mode* :http) (http-add-header "Last-Modified" (http-timestamp (compute-journal-last-modified-date))) (http-add-header "Content-Language" "de") (http-add-header "Cache-Control" "public") (http-send-headers "text/html; charset=UTF-8")) (