| ;;;; $Id: web-server.lisp,v 1.105 2008/08/14 13:17:05 lisppaste Exp $ ;;;; $Source: /project/lisppaste/cvsroot/lisppaste2/web-server.lisp,v $ ;;;; See the LICENSE file for licensing information. (in-package :lisppaste) (defclass paste () ((number :initarg :number :initform 0 :accessor paste-number) (user :initarg :user :initform "" :accessor paste-user) (title :initarg :title :initform "" :accessor paste-title) (contents :initarg :contents :initform "" :accessor paste-contents) (universal-time :initarg :universal-time :initform 0 :accessor paste-universal-time) (annotations :initarg :annotations :initform nil :accessor paste-annotations) (annotation-counter :initarg :annotation-counter :initform 0 :accessor paste-annotation-counter) (channel :initarg :channel :initform "" :accessor paste-channel) (colorization-mode :initarg :colorization-mode :initform "" :accessor paste-colorization-mode) (maybe-spam :initarg :maybe-spam :initform nil :accessor paste-maybe-spam-p) (is-unicode :initarg :is-unicode :initform :true :accessor paste-is-unicode-p))) (defun paste-display-url (paste) (urlstring (merge-url *display-paste-url* (prin1-to-string (paste-number paste))))) (defun find-paste (number) (find number *pastes* :key #'paste-number)) (defmacro make-paste (&rest arguments) `(progn (funcall 'make-instance 'paste ,@arguments))) (defclass lisppaste-basic-behavior () ()) (defclass lisppaste-basic-handler (handler lisppaste-basic-behavior) ()) (defclass main-handler (lisppaste-basic-handler) ()) (defclass recent-handler (lisppaste-basic-handler) ()) (defclass css-handler (lisppaste-basic-handler) ()) (defclass new-paste-handler (lisppaste-basic-handler) ()) (defclass list-paste-handler (lisppaste-basic-handler) ()) (defclass submit-paste-handler (lisppaste-basic-handler) ()) (defclass display-paste-handler (lisppaste-basic-handler) ()) (defclass rss-handler (lisppaste-basic-handler) ()) (defclass rss-full-handler (lisppaste-basic-handler) ()) (defclass syndication-handler (lisppaste-basic-handler) ()) (defclass stats-handler (lisppaste-basic-handler) ()) (defclass email-redirect-handler (lisppaste-basic-handler) ()) (defclass channel-select-handler (lisppaste-basic-handler) ()) (defclass 404-handler (handler) ()) (define-application lisppaste-application *paste-listener*) (defclass admin-mixin () ()) (define-handler-hierarchy (:application lisppaste-application) (*paste-external-url* ("mark-as-spam" (mark-as-spam-handler lisppaste-basic-behavior) :inexact t) ("mark-as-wrong-channel" (mark-as-wrong-channel-handler lisppaste-basic-behavior) :inexact t) ("administration" (administration-handler lisppaste-basic-behavior admin-mixin)) ("administration/" ("login" (login-handler lisppaste-basic-behavior)) ("logout" (logout-handler lisppaste-basic-behavior)) ("kill-paste" (kill-paste-handler lisppaste-basic-behavior admin-mixin)) ("spam-review" (spam-review-handler lisppaste-basic-behavior admin-mixin))))) (defvar *referer-hash* (make-hash-table :test #'equalp)) (defvar *referer-example-hash* (make-hash-table :test #'equalp)) (defun times-file-for-class (class) (merge-pathnames (format nil "times-~(~A~)" (symbol-name (class-name (class-of class)))) *times-file-root*)) (defun referer-list () (loop for link being the hash-values of *referer-example-hash* using (hash-key host) collect (cons host link))) (defun fix-referers () (loop for count being the hash-values of *referer-hash* using (hash-key host) do (let ((split-host (split-sequence:split-sequence #\. host))) (when (or (and (eql (length split-host) 3) (string-equal (first split-host) "www") (string-equal (second split-host) "google")) (and (eql (length split-host) 4) (string-equal (first split-host) "www") (string-equal (second split-host) "google") (or (string-equal (third split-host) "co") (string-equal (third split-host) "com")) (eql (length (fourth split-host)) 2))) (remhash host *referer-hash*) (incf (gethash "Google" *referer-hash* 0) count))))) (defvar *show-captcha* t) (defmethod handle-request-response :around ((handler lisppaste-basic-behavior) method request) (with-open-file (*trace-output* (times-file-for-class handler) :direction :output :if-exists :append :if-does-not-exist :create) (expire-authorization-tokens) (unwind-protect (let ((*show-captcha* (not (is-authorized request)))) (call-next-method)) (force-output *trace-output*)))) (defun make-css () (let ((colorize:*css-background-class* "paste")) (format nil "body { background: white; color: black; } a { margin:1px; border-collapse: collapse; } a:link { color:#335570; text-decoration: none; background-color: transparent;} a:visited { color:#705533; text-decoration: none; background-color: transparent;} a:hover { color:#000000; text-decoration: none; background-color: #BBCCEE; border: 1px solid #335577; margin: 0px;} a:active { color:#000000; text-decoration: none; background-color: #CCBBFF; border: 1px solid #335577; margin: 0px;} .simple-paste-list { background-color : #E9FFE9 ; border: 2px solid #9D9; padding : 4px; font-size: small; } .simple-paste-list td { border-bottom: 1px dotted #9D9; font-size: small; } table.detailed-paste-list { border-collapse: collapse; border : 1px solid #AAA ; } table.detailed-paste-list td { border : 1px dotted #AAA; } table.info-table { border-collapse: collapse; border : 1px solid #AAA ; background-color: #F9E9F9; empty-cells: hide; } table.info-table td { border : 1px dotted #AAA; background-color: transparent; padding-left: 2em; padding-right: 2em; } table.info-table th { border : 1px dotted #AAA; background-color: transparent; text-align: left; padding-right: 1em; } table.rate-table { border-collapse: collapse; border : 1px solid #AAA ; background-color: #F9E9F9; empty-cells: hide; } table.rate-table td { border : 1px dotted #AAA; background-color: transparent; padding: 2pt; } table.rate-table th { border : 1px dotted #AAA; background-color: transparent; text-align: left; padding: 1pt; } .new-paste-form { background-color : #FFE9E9 ; border: 2px solid #D99; padding : 4px; } .paste-header { background-color : #E9F9F9 ; border: 2px solid #9DD; padding : 4px; margin-bottom : 4px; } .info-text { background-color : #E9F9F9 ; border: 2px solid #9DD; padding : 4px; margin-top : 4px; text-align: justify; } div.ads-text { background-color : #F9E9F9 ; border: 2px solid #D9D; padding : 4px; margin-top : 4px; text-align: justify; font-size: small; display: table; padding-right: 1em; } div.ads-text ul { margin-top: 0; margin-bottom: 0; padding-top: 0; padding-bottom: 0; } div.ads-text li { padding-bottom: 1ex; } .ohloh a:link { margin: 0px; border-collapse: collapse; border: 0; } .ohloh a:active { margin: 0px; border-collapse: collapse; border: 0; } .ohloh a:visited { margin: 0px; border-collapse: collapse; border: 0; } .ohloh a:hover { margin: 0px; border-collapse: collapse; border: 0; } .controls { background-color : #E9E9FF ; border: 2px solid #99D; padding : 4px; display: inline-block; } .small-header { font-weight: bold; font-size: large; } .top-header { text-align : center; font-style: italic; font-weight: bold; font-size: x-large; } .big-warning { text-align : center; font-weight: bold; font-size: x-large; } .paste-area { background-color : #F4F4F4 ; border : 2px solid #AAA ; } .bottom-links { background-color : #F9F9E9; border: 2px solid #DD9; padding : 4px; margin-bottom : 4px;} #main-link { text-align : left; font-weight: bold; } #other-links { text-align : right; } hr { border: 1px solid #999; } @media screen { div.altdiv { display: none; } } table.webutils-form { background-color : #E9E9FF ; border: 2px solid #99D; padding : 4px; } table.webutils-form th { text-align: left; } ~A~&~A~&" (colorize:make-background-css "#F4F4F4") colorize:*coloring-css*))) (defmethod handle-request-response ((handler css-handler) method request) (request-send-headers request :expires 0 :content-type "text/css") (html-stream (request-stream request) (make-css))) (defun rss-link-header () <link rel="alternate" type="application/rss+xml" title="Lisppaste RSS" href=?(urlstring *rss-url*)/>) (defparameter *form-accept-translator* (make-translator)) (define-translator *form-accept-translator* mutate-forms (:oaoo <form $attributes> . body) (<form accept-charset= "utf-8" $attributes> body)) (defun lisppaste-wrap-page (title &rest forms) (<html> (<head> (<title> title) <link type="text/css" rel="stylesheet" href=?(url-path *css-url*)/> (rss-link-header)) (<body> (<div class="top-header"> title) <p/> (apply-translator *form-accept-translator* forms) (bottom-links)))) (defun bottom-links () (list <p/> (<div class="bottom-links"> (<table width="100%"> (<tr> (<td id="main-link"> (<a href=?(urlstring *paste-external-url*)> "Main page")) (<td id="other-links"> (<a href=?(urlstring *new-paste-url*)> "New paste") " | " (<a href=?(urlstring *list-paste-url*)> "List all pastes") " | " (<a href=?(urlstring *syndication-url*)> "Syndication") " | " (<a href="http://common-lisp.net/project/lisppaste/xml-rpc.html"> "XML-RPC") (when *serve-source* (list " | " (<a href=?(urlstring (merge-url *show-component-url* "lisppaste"))> "Source"))) " | " (<a href=?(urlstring *email-redirect-url*)> "Requests Email") " | " (<a href=? (urlstring (handler-url 'administration-handler))> "Administrate") " | " (<a href="http://www.common-lisp.net/project/lisppaste"> "Project home"))))) (<i> "Lisppaste pastes can be made by anyone at any time. Imagine a fearsomely comprehensive disclaimer of liability. Now fear, comprehensively."))) (defmethod application-wrap-page ((application-handler lisppaste-application) request title body &rest extra-headers) (apply #'request-send-headers request :expires 0 :content-type "text/html; charset=utf-8" extra-headers) (xml-output-to-stream (request-stream request) (lisppaste-wrap-page title body))) (defun lisppaste-send-headers-for-html (request) (request-send-headers request :expires 0 :content-type "text/html; charset=utf-8")) (defun recent-paste-list-div (&key (count 10)) (<div class="simple-paste-list"> (<table> (loop for i from 1 to count for j in *pastes* collect (<tr> (<td valign="center"> (<a href=?(paste-display-url j)> (paste-title j))) (<td valign="bottom"> " by " (paste-user j)) (<td valign="bottom"> (paste-channel j)))) (<tr> (<td colspan="3"> (<center> (<b> (<a href=?(urlstring *list-paste-url*)> "More recent pastes...")))))))) (defmethod handle-request-response ((handler recent-handler) method request) (lisppaste-send-headers-for-html request) (xml-output-to-stream (request-stream request) (lisppaste-wrap-page "Recent Pastes" (recent-paste-list-div :count 20)))) (defmethod handle-request-response ((handler main-handler) method request) (lisppaste-send-headers-for-html request) (xml-output-to-stream (request-stream request) (lisppaste-wrap-page (format nil "~A pastebin" *paste-site-name*) (<table width="100%" border="0" cellpadding="2"> (<tr> (<td> (<div class="small-header"> "Recent Pastes")) (<td align="right"> (<div class="small-header"> "Make a new paste"))) (<tr> (<td valign="top" width="40%"> (recent-paste-list-div) <p/> (<div class="small-header"> "About lisppaste") (<div class="info-text"> "Lisppaste is a pastebot / pastebin / nopaste service with syntax highlighting, XML-RPC support, annotations, and more." <p/> "Many times when working via IRC, people want to share a snippet of code with somebody else. However, just pasting the code into IRC creates a flood of text which is hard to read and scrolls by as discussion progresses." <p/> "Thus, the pastebot was invented, which has a web form where users can paste code, and the URL of the paste is announced on the desired channel. Lisppaste is an advanced pastebot running on the IRC server " *irc-network-name* " which has many unique features." (when *no-channel-pastes* (list <p/> "It also allows pastes which are not announced on any channel, which is useful for sections of code which need to be sent to a mailing list or are discussed in ways other than IRC." <p/> "Lisppaste is graciously hosted by " (<b> (<a href="http://www.common-lisp.net/"> "common-lisp.net")) " - a hosting service for projects written in Common Lisp (like this one)." <p/> "Questions? Comments? Want lisppaste in your channel? " (<a href=?(urlstring *email-redirect-url*)> "Email me") ".")))) (<td valign="top" align="right"> (<form method="post" action=?(urlstring *submit-paste-url*)> (generate-new-paste-form :width 60)) <p/> *ohloh* (<div class="ads-text"> *ads*))))))) (defun ban-log (user request) (log-event (format nil "Blocked attempt by ~S, IP ~S, (referred by ~S) to submit a paste.~%Request headers are: ~S.~%Request body is: ~S.~%" user (car (request-header request :x-forwarded-for)) (car (request-header request :referer)) (request-headers request) (request-body request)) :log-file *ban-log-file*)) (defmethod handle-request-response :around ((handler submit-paste-handler) method request) (let ((forwarded-for (car (request-header request :x-forwarded-for)))) (if (and forwarded-for (member forwarded-for *banned-ips* :test #'string-equal)) (progn (ban-log forwarded-for request) (lisppaste-send-headers-for-html request) (xml-output-to-stream (request-stream request) (<html> (<head> <title> "No cookie for you!") (<body> (<h1> (<font color="red"> "Naughty boy!")))))) (call-next-method)))) (defmethod handle-request-response ((handler new-paste-handler) method request) (let* ((annotate-string (body-param "annotate" (request-body request))) (annotate-number (if annotate-string (parse-integer annotate-string :junk-allowed t))) (annotate (if annotate-number (find annotate-number *pastes* :key #'paste-number))) (default-channel (or (and annotate (paste-channel annotate)) (find-if #'(lambda (e) (> (length e) 1)) (list (and (eql method :post) (body-param "channel" (request-body request))) (substitute #\# #\/ (urlstring-unescape (request-unhandled-part request)) :test #'char=) (concatenate 'string "#" (request-cookie request "CHANNEL")) (and *no-channel-pastes* "None") ))))) (cond ((and default-channel (or (and *no-channel-pastes* (string-equal default-channel "None")) (find default-channel *channels* :test #'string-equal))) (request-send-headers request :expires 0 :content-type "text/html; charset=utf-8" :set-cookie (format nil "CHANNEL=~A; path=/" (or (and *no-channel-pastes* (string-equal default-channel "none") "None") (subseq default-channel 1)))) (new-paste-form request :annotate annotate :default-channel default-channel)) (t (lisppaste-send-headers-for-html request) (xml-output-to-stream (request-stream request) (lisppaste-wrap-page "Select a channel" (<form method="post" action=?(urlstring *new-paste-url*)> (<div class="controls"> <input type="hidden" name="annotate" value=?annotate-string /> "Please select a channel to lisppaste to: " (<select name="channel"> (<option value=""> "") (mapcar (lambda (e) (<option value=?e> e)) *channels*)) <input type="submit" value="Submit"/>)))))))) (defun time-delta (time &key (level 2) (ago-p t) (origin (get-universal-time))) (let ((delta (- origin time))) (cond ((< delta 1) "<Doc Brown>From the <i>future</i>...</Doc Brown>") ((< delta (* 60 60)) (format nil "~A~A" (time-delta-primitive delta 1) (if ago-p " ago" ""))) (t (format nil "~A~A" (time-delta-primitive delta level) (if ago-p " ago" "")))))) (defun irc-log-link (utime channel) (format nil "http://ircbrowse.com/cview.html?utime=~A&channel=~A&start=~A&end=~A#utime_requested" (- utime 5) (string-left-trim "#" channel) (- utime (* 60 60)) (+ utime (* 60 60)))) (defun first-<-mod (n &rest nums) (some #'(lambda (n2) (if (< n2 n) (mod n n2) nil)) nums)) (defun time-delta-primitive (delta &optional (level 2)) (let* ((seconds 60) (minutes (* seconds 60)) (hours (* minutes 24)) (days (* hours 7)) (weeks (* hours 487/16)) (months (* weeks 12)) (years (* hours (+ 365 1/4)))) (let ((primitive (cond ((< delta seconds) (format nil "~D second~:P" delta)) ((< delta minutes) (format nil "~D minute~:P" (floor delta seconds))) ((< delta hours) (format nil "~D hour~:P" (floor delta minutes))) ((< delta days) (format nil "~D day~:P" (floor delta hours))) ((< delta weeks) (format nil "~D week~:P" (floor delta days))) ((< delta months) (format nil "~D month~:P" (floor delta weeks))) (t (format nil "~D year~:P" (floor delta months)))))) (if (eql level 1) primitive (format nil "~A, ~A" primitive (time-delta-primitive (first-<-mod delta years months weeks days hours minutes seconds) (1- level))))))) (defun max-length (str n) (if (> (length str) n) (concatenate 'string (subseq str 0 (1- n)) "...") str)) (defmethod handle-request-response ((handler syndication-handler) method request) (lisppaste-send-headers-for-html request) (xml-output-to-stream (request-stream request) (lisppaste-wrap-page "Syndication options" "Lisppaste can be syndicated in a variety of RSS formats for use with your favorite RSS reader." <p/> (<table class="info-table"> (<tr> (<th align="left"> "All channels") (<td> (<a href=?(urlstring *rss-url*)> "Basic")) (<td> (<a href=?(urlstring *rss-full-url*)> "Full"))) (mapcar (lambda (channel) (let ((append (if (and *no-channel-pastes* (string-equal channel "None")) "?none" (substitute #\? #\# channel)))) (<tr> (<th align="left"> channel) (<td> (<a href=?(concatenate 'string (urlstring *rss-url*) append)> "Basic")) (<td> (<a href=?(concatenate 'string (urlstring *rss-full-url*) append)> "Full"))))) *channels*))))) (defun last-paste-date-for-channel (channel) (let ((paste (find channel *pastes* :test #'string-equal :key #'paste-channel))) (if paste (time-delta (paste-universal-time paste) :level 1 :ago-p nil) "Never"))) (defmethod handle-request-response ((handler channel-select-handler) method request) (lisppaste-send-headers-for-html request) (xml-output-to-stream (request-stream request) (lisppaste-wrap-page (format nil "~A channel list" *paste-site-name*) (<table width="100%" border="0" cellpadding="2"> (<tr valign="top" align="center"> (<td> (<div class="info-text"> "Lisppaste is available in " (<b> (prin1-to-string (length (remove "None" *channels* :test #'equal)))) (format nil " channels on the IRC network ~A. Select a channel from the list below and bookmark its URL to paste with direct notification to your channel." *irc-network-name*) <p/> "Questions? Comments? Want lisppaste in your channel? " (<a href=?(urlstring *email-redirect-url*)> "Email me") ".") <p/> (<table class="info-table"> (<tr> (<th align="left"> "Channel Name") (<th align="left"> "") (<th align="left"> "") (<th align="left"> "Last Paste")) (mapcar (lambda (channel) (<tr> (<th align="left"> channel) (<th align="left"> (<a href=?(concatenate 'string (urlstring *new-paste-url*) "/" (urlstring-escape (subseq channel 1)))> "New paste here")) (<th align="left"> (<a href=?(concatenate 'string (urlstring *list-paste-url*) "/" (urlstring-escape (subseq channel 1)))> "List")) (<td align="left"> (last-paste-date-for-channel channel)))) (sort (remove "None" *channels* :test #'string-equal) #'string<))))))))) (defmethod handle-request-response ((handler stats-handler) method request) (lisppaste-send-headers-for-html request) (xml-output-to-stream (request-stream request) (lisppaste-wrap-page "Statistics" (<div> (<span class="small-header"> "Uptime: ") (time-delta *boot-time* :ago-p nil :level 3))))) (defmethod handle-request-response ( |