src/view/components/tabs.lisp

DOWNLOAD
(defpackage murja.view.components.tabs
  (:use :cl :easy-routes :murja.middleware.auth :murja.view.components.root :spinneret)
  (:import-from :murja.middleware.db :@transaction)
  (:export :*inject-to-head* :deftab))

(in-package :murja.view.components.tabs)

(defclass tab ()
  ((url :initarg :url :initform nil :accessor tab-url)
   (component :initarg :component :initform (error "Component needed") :accessor tab-component)
   (title :initarg :title :initform (error "Title needed") :accessor title)
   (require-login :initarg :require-login :initform nil :accessor require-login)
   (needed-abilities :initarg :abilities :initform nil :accessor abilities)
   (subtab :initarg :subtab :initform nil :accessor subtab)))

;; A hack for deducing runtime if a value is instanceof 'tab.
;; I got tired of trying to find a java-style instanceof operator that would polymorphically work for every value :D
(defmethod is-tab? ((tabb tab))
  t)

(defmethod is-tab? ((tabb t))
  nil)

(defmethod print-object ((tabb tab) output)
  (format output "#<TAB: ~a ~a>" (tab-url tabb) (title tabb)))


(eval-when (:compile-toplevel :load-toplevel :execute)
  (defvar *tabs* (make-hash-table :test 'equal)
    "A list of tabs, keyed by symbol and valued by instances of tab class"))


(defmacro deftab (sym (&key
			 url
			 (title "")
			 subtab 
			 require-login
			 needed-abilities
			 captured-url-params
			 inject-to-head)
		  &body rst)
  `(progn
     (setf (gethash (quote ,sym) *tabs*)
	   (make-instance 'tab :url ,url
			       :abilities (list ,@needed-abilities)
			       :require-login ,require-login
			       :subtab (quote ,subtab)
			       :title ,title
			       :component (lambda (&rest all)
					    (destructuring-bind ,captured-url-params all 
					      (murja.middleware.db:with-db
						  (let ((murja.settings:*settings* (murja.settings:get-settings)))
						    (with-html 
						      ,@rst)))))))
     (defroute ,sym (,url 
		     :method :get
		     :decorators (@transaction
				  (@ssr-authenticated :require-authentication ,require-login )
				  ;;@check-if-initial
				  (@dispatcher ,inject-to-head)
				  )) ()
       (values (quote ,sym) (list ,@captured-url-params)))))

(defun tab-container (selected-tab tablist tab-parameters)
  (assert (is-tab? selected-tab))
  (assert (every #'is-tab? tablist))
  
  (sort tablist (lambda (a b)
		  (string-lessp (title a) (title b))))
  
  (with-html
    (:div.tabs :id "topbar"
	       (:ul.tab-headers
		(dolist (tab tablist)
		  (unless (subtab tab)
		    (:li :class (format nil "tab-header ~a" (if (equalp tab selected-tab)
								"tab-selected"
								""))
			 ;; wonder if this is actually necessary or just an artifact of the old newui branch?
			 :data-href (tab-url tab)
			 (:a :href (tab-url tab)
			     (title tab))))))
	       (apply (tab-component selected-tab) tab-parameters))))
  

(defun @dispatcher (next inject-to-head)
  (multiple-value-bind (sym url-params) (funcall next)
    (let* ((tab (gethash sym *tabs*))
	   ;; inject-to-head gets passed through to macros here as an unevaluated list
	   ;; and I don't understand why that happens, but this fixes the symptom and
	   ;; shouldn't expose too much vulnerabilities as :inject-to-head is trusted data 
	   (inject-to-head (map 'list
				#'eval
				inject-to-head))
	   (usr-abilities (when *user*
			    (coerce (murja.model.user:abilities *user*) 'list))))
      (assert tab)
      (if (or (not (abilities tab))
	      (intersection usr-abilities (abilities tab))
	      (or (not (require-login tab))
		  *user*))
	  (let ((*inject-to-head* inject-to-head))
	    (root-component
	     (tab-container tab 
			    (remove-if-not (lambda (tab)
					     (or (not (require-login tab))
						 (subtab tab)
						 *user*))
					   (alexandria:hash-table-values 
					    *tabs*))
			    url-params)))
	  (progn
	    (setf (hunchentoot:return-code*) 404)
	    "Nothing found")))))