;;; File: ;;; plans.ss ;;; Author: ;;; Samuel A. Rebelsky ;;; Version: ;;; 1.0 of April 2001 ;;; Summary: ;;; Provides support for a very simple plans system. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Overall strategy ;;; All plans are stored in a single file. That file contains one value: ;;; a of lists. Each element list has five strings: ;;; account ;;; password (okay, it's not a sophisticated or secure system) ;;; email account ;;; name ;;; plan ;;; Both the directory and the file must be world-writable (ugh!). ;;; The directory should not be world-readable, but the file should be. ;;; CGI variables: ;;; operation: the desired operation to perform. This can be ;;; start-create - request the creation of a new account ;;; finish-create - finish the creation of that new account ;;; start-update - request the update of an account ;;; finish-update - finish that update ;;; lookup - looks up values ;;; login - the standard login page ;;; Two variables are used in everything but start_create ;;; account: the name used to identify the current user (used in ;;; everything but start-create and login) ;;; password: the password for the current user (currently left ;;; readable and therefore insecure) (used in everything but ;;; create) ;;; Four additional variables are used in finish_create and finish_update ;;; newpassword: the user's potentially new password ;;; email: the user's potentially new email ;;; name: the user's potentially new name ;;; plan: the user's potentially new plan ;;; One additional variable is used in lookup ;;; planner: the userid of the person whose plan is being looked ;;; up ;;; To set up your own installation of plans, ;;; (1) Make a copy of this file. ;;; (2) Create the file plans.cgi which contains the lines ;;; #/bin/bash ;;; /home/rebelsky/bin/schemeweb plans.ss ;;; (3) Create the appropriate directory for storing the plans. ;;; For example, mkdir /home/account/public_html/Plans ;;; (4) Set the permissions to "write, can't read" ;;; chmod a+w /home/account/public_html/Plans ;;; chmod a-r /home/account/public_html/Plans ;;; chmod u+r /home/account/public_html/Plans ;;; (5) Create an appropriate plans file that contains just ;;; an empty list. ;;; (6) Set the permissions to "can write and read". ;;; chmod a+rw /home/account/public_html/Plans/plans. ;;; (7) Update the SchemePlans variable below. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Generic Preconditions and Postconditions ;;; Preconditions (unless specified otherwise): ;;; The plans file must exist and be readable. ;;; The plans directory must exist and be writable. ;;; Postconditions: ;;; Does not update the plans file unless specified otherwise. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Helpful Libraries ;;; Load some of the standard Web helper procedures I rely on. (load "/home/rebelsky/Web/Scheme/webutils.ss") ;;; Load some of my favorite utility functions, like remove (load "/home/rebelsky/Web/Scheme/utils.ss") ;;; MzScheme provides all sorts of fun mail utilities (require-library "mail.ss" "net") ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Important Constants ;;; Name: ;;; plans-directory ;;; Type: ;;; string ;;; Value: ;;; The directory used to store the plans. It should end in ;;; a slash. (define plans-directory "/home/rebelsky/Web/SchemePlans/") ;;; Name: ;;; plans-file ;;; Type: ;;; string ;;; Value: ;;; The directory used to store the plans. It should end in ;;; a slash. (define plans-file (string-append plans-directory "plans")) ;;; Name: ;;; cgi-script ;;; Type: ;;; string ;;; Value: ;;; The location of the corresponding CGI script. (define cgi-script "http://www.cs.grinnell.edu/~rebelsky/Scheme/plans.cgi") ;;; Name: ;;; moderator-email ;;; Type: ;;; string ;;; Value: ;;; The email address of the Plans moderator (define moderator-email "rebelsky@grinnell.edu") ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Generic Helper Procedures ;;; Procedure: ;;; send-email ;;; Parameters: ;;; address, the email address of the intended recipient ;;; subject, the subject of the message ;;; body, the body of the message ;;; Purpose: ;;; Sends an appropriately-formatted email message to the recipient. ;;; Useful for sending out error messages and the ilk. ;;; Produces: ;;; Nothing. ;;; Preconditions: ;;; Running on a Unix system. ;;; moderator-email has been set (see variables above). ;;; Postconditions: ;;; Email has been sent to the intended recipient. That email ;;; appears to be from the moderator. (define send-email (lambda (address subject body) (send-mail-message (string-append "Plans System <" moderator-email ">") ; Sender (string-append "Plans: " subject) ; Subject (list address) ; List of recipients null ; List of cc null ; List of bcc (list body ; Body as strings (string-append "You can find plans at " cgi-script "."))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Plans Helper Procedures ;;; Procedure: ;;; add-plan ;;; Parameters: ;;; plan, a list of five strings ;;; (account password email fullname plantext) ;;; plans, a list of plans ;;; Purpose: ;;; Adds an account for the given account. ;;; Produces: ;;; result, a list ;;; Preconditions: ;;; There is not already an entry for account [Verified]. ;;; The plans file has been locked. [Unverified] ;;; plans is a list of five strings. [Unverified]. ;;; Postconditions: ;;; If no errors occurred, ;;; result's car is #t ;;; the entry for account has been updated ;;; email has been sent to the old email address ;;; If any errors occurred, ;;; result's car is #f ;;; result's cadr is a report on the kind of error ;;; the entry for account has not changed (define add-plan (lambda (plan plans) ; Get the old plan (let ((oldplan (get-plan (car plan) plans))) ; If the user already has a plan ... (if oldplan ; Give up, it's not legal to add a new one. (list #f "You cannot add an account that already exists.") ; Otherwise ... (begin ; Tell the user he/she has a plan. (send-email (caddr plan) "Plan Created" (string-append "Your plan was just added. " "If you did not add your plan, " "please contact the administrator ASAP.")) ; Update the plans file. (write-plans (cons plan plans)) ; Return the appropriate result. (list #t)))))) ;;; Procedure: ;;; get-plan ;;; Parameters: ;;; account, the account name for a user ;;; plans, a list of plans ;;; Purpose: ;;; Gets a plan from the list of plans. ;;; Produces: ;;; #f, if the plan is not found ;;; plan, otherwise ;;; Preconditions: ;;; plans has the appropriate form. ;;; Postconditions: ;;; The result is #f if there is no plan for account in plans. ;;; The result is some entry for account, if there is an entry ;;; for account. (define get-plan (lambda (account plans) (search (left-section user-plan? account) plans))) ;;; Procedure: ;;; list-accounts ;;; Parameters: ;;; plans, a list of plans. ;;; Purpose: ;;; Creates a list of all the accounts on the system (just the ;;; user names). ;;; Produces: ;;; accounts, a list. ;;; Preconditions: ;;; plans is a list of lists of strings of the approved form. ;;; Postconditions: ;;; Every value in accounts is a valid account. ;;; Every valid account is a value in accounts. (define list-accounts (lambda (plans) (map car plans))) ;;; Procedure: ;;; read-plans ;;; Parameters: ;;; None ;;; Purpose: ;;; Reads and returns the list of plans. ;;; Produces: ;;; plans, a list of plans. ;;; Preconditions: ;;; The plans file should be locked. ;;; The plans file must exist and have the appropriate form. ;;; Postconditions: ;;; plans is a list. ;;; Each element of plans is a five-element list of the form ;;; (account password email fullname plan). ;;; Warning: ;;; If you use this without making sure that the plans file is ;;; locked, weird things can happend. Use read-plans whenever ;;; possible. (define read-plans (lambda () (let* ((port (open-input-file plans-file)) (plans (read port))) (close-input-port port) plans))) ;;; Procedure: ;;; user-plan? ;;; Parameters: ;;; account, a string ;;; plan, a plan (list of five strings) ;;; Purpose: ;;; Determines if a plan belongs to a particular account. ;;; Produces: ;;; matches, a boolean value ;;; Preconditions: ;;; plan is a list of five strings ;;; Postconditions: ;;; Returns #t if the plan belongs to the particular user. ;;; Returns #f otherwise. (define user-plan? (lambda (account plan) (equal? account (car plan)))) ;;; Procedure: ;;; update-account ;;; Parameters: ;;; plan, a plan (list of five strings) ;;; plans, the current list of plans. ;;; Purpose: ;;; Updates the account for the given account. ;;; Produces: ;;; result, a list ;;; Preconditions: ;;; There is already an entry for account [Verified]. ;;; plans is in the appropriate form. [Unverified]. ;;; All the parameters are strings. [Unverified]. ;;; Postconditions: ;;; If no errors occurred, ;;; result's car is #t ;;; the entry for account has been updated ;;; email has been sent to the old email address ;;; If any errors occurred, ;;; result's car is #f ;;; result's cadr is a report on the kind of error ;;; the entry for account has not changed (define update-plan (lambda (plan plans) ; Find the old plan (let ((oldplan (get-plan (car plan) plans))) ; If there is no old plan ... (if (not oldplan) ; Give up, it's not possible to update. (list #f "You cannot update an account that does not exist.") ; Otherwise, ... (begin ; Inform the user the plan has been updated. (send-email (caddr oldplan) "Plan Updated" (string-append "Your plan was just updated. " "If you did not update your plan, " "please contact the administrator ASAP")) ; Update the plans file (write-plans (cons plan (remove (left-section user-plan? (car plan)) plans))) ; Return a nice value. (list #t)))))) ;;; Procedure: ;;; write-plans ;;; Parameter: ;;; newplans, a list of plans ;;; Purpose: ;;; Replaces the plans file with the given list of plans. ;;; Produces: ;;; Nothing. ;;; Preconditions: ;;; The plans file must be locked so that other programs don't ;;; update it at the same time. ;;; Postconditions: ;;; The plans file now contains newplans. (define write-plans (lambda (newplans) ; Bye bye old plans! (delete-file plans-file) ; We can only write to a port, so open one (let ((port (open-output-file plans-file))) ; Write the new plans in one lump. (write newplans port) ; Put in a newline to be nice. (newline port) ; That's it, we're done. (close-output-port port)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Interface Utilities ;;; Procedure: ;;; start-cgi ;;; Parameters: ;;; None ;;; Purpose: ;;; Starts the CGI script. ;;; Produces: ;;; plans, a list of plans. (We need that list in almost every ;;; case.) ;;; Preconditions: ;;; Just the normal ones. ;;; Postconditions: ;;; The plans file is locked until further notice. (define start-cgi (lambda () (lockfile plans-file) (unsynchronized-read-plans))) ;;; Procedure: ;;; fivefields ;;; Parameters: ;;; account, a string ;;; password, a string ;;; email, a string ;;; fullname, a string ;;; plan, a string ;;; Purpose: ;;; Generates HTML for the five basic plan fields. ;;; Produces: ;;; fieldcode, a string ;;; Preconditions: ;;; None ;;; Postconditions: ;;; fieldcode is valid HTML for the five fields. (define fivefields (lambda (account password email fullname plan) (string-append "Account: " (textfield "account" account) (string #\newline) "
" "Password: " (textfield "newpassword" password) (string #\newline) "
" "Email: " (textfield "email" email) (string #\newline) "
" "Name: " (textfield "fullname" fullname) (string #\newline) "
" "Plan: " (textarea "plan" plan) "
"))) ;;; Procedure: ;;; login-code ;;; Parameters: ;;; None ;;; Purpose: ;;; Generates HTML code for the login screen. ;;; Produces: ;;; html, a string ;;; Preconditions: ;;; None ;;; Postconditions: ;;; Returns valid HTML. (define login-code (lambda () (string-append (form cgi-script "Login" (string-append (hidden "operation" "login") (textfield "account" "account") (textfield "password" "password"))) (form cgi-script "Create Account" (hidden "operation" "start-create"))))) ;;; Procedure: ;;; plans-intro ;;; Parameters: ;;; title, a string ;;; Purpose: ;;; Generates the introduction that appears at the top of ;;; every plan page. ;;; Produces: ;;; intro-code, a string ;;; Preconditions: ;;; title is valid HTML. ;;; Postconditions: ;;; Returns valid HTML that can be placed in the body of a page. (define plans-intro (lambda (title) (string-append (paragraph (markup "b" "Scheme Plans System")) (heading 1 title)))) ;;; Procedure: ;;; plan->html ;;; Parameters: ;;; plan, a list of strings ;;; Purpose: ;;; Converts the plan to HTML. ;;; Produces: ;;; plan-code, a string ;;; Preconditions: ;;; plan has the form (account password email name planinfo) ;;; Each value is valid HTML. ;;; Postconditions: ;;; plan-code is valid HTML. ;;; plan-code provides a reasonable representation of the ;;; plan information (without the password). (define plan->html (lambda (plan) (let ((one-row (lambda (label value) (string-append "" "" label "" "" value "" "" (string #\newline))))) (string-append "" (string #\newline) (one-row "Account" (car plan)) (one-row "aka" (list-ref plan 3)) (one-row "Email" (string-append "" (markup "code" (list-ref plan 2)) "")) (one-row "Plan" (list-ref plan 4)) "
" (string #\newline))))) ;;; Procedure: ;;; standard-buttons ;;; Parameters: ;;; account, a string ;;; password, a string ;;; Purpose: ;;; Creates HTML for the standard buttons that appear on most ;;; pages. ;;; Produces: ;;; button-code, a string ;;; Preconditions: ;;; account and password represent a valid account. ;;; Postconditions: ;;; button-code is valid HTML for some standard buttons. ;;; That code can't be in a form. (define standard-buttons (lambda (account password) (string-append (form cgi-script "Lookup" (string-append (hidden "account" account) (hidden "password" password) (hidden "operation" "lookup") (textfield "planner" ""))) (form cgi-script "Update" (string-append (hidden "account" account) (hidden "password" password) (hidden "operation" "start-update"))) (form cgi-script "Logout" (hidden "operation" ""))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; HTML Interfaces ;;; Procedure: ;;; creation-page ;;; Parameters: ;;; None ;;; Purpose: ;;; Produces a page that lets someone create an account in ;;; the plans system. ;;; Produces: ;;; page, a string ;;; Preconditions: ;;; None ;;; Postconditions: ;;; page is valid HTML for a complete page. (define creation-page (lambda () (make-page (head "[Plans] Create an Account") (body (string-append (plans-intro "Create an Account") (form cgi-script "Create" (string-append (hidden "operation" "finish-create") (fivefields "" "" "" "" "")))))))) ;;; Procedure: ;;; default-page ;;; Parameters: ;;; account, a string ;;; password, a string ;;; Purpose: ;;; Produces the default page (the one you normally get after ;;; logging in). ;;; Produces: ;;; page, a string ;;; Preconditions: ;;; account and password describe a valid account (define default-page (lambda (account password) (make-page (head "[Plans] Have Fun!") (body (standard-buttons account password))))) ;;; Procedure: ;;; error-page ;;; Parameters: ;;; message, a string ;;; Purpose: ;;; Generates a page that reports on an error. ;;; Produces: ;;; page, a string ;;; Preconditions: ;;; none ;;; Postconditions: ;;; page is valid HTML for a page. (define error-page (lambda (message extrastuff) (make-page (head "[Plans] ERROR!") (body (string-append (plans-intro "Error") (paragraph message) extrastuff))))) ;;; Procedure: ;;; finish-add-page ;;; Parameters: ;;; plans, a list of plans ;;; account, a string ;;; password, a string ;;; email, a string ;;; fullname, a string ;;; plan, a string ;;; Purpose: ;;; Finishes creating a plan. ;;; Produces: ;;; page, a string ;;; Preconditions: ;;; account and password name a valid account. ;;; Postconditions: ;;; page is valid HTML for a complete page. (define finish-creation-page (lambda (plans account password email fullname plan) (let ((result (add-plan (list account password email fullname plan) plans))) (if (car result) (make-page (head (string-append "[Plans] Added: " account)) (body (string-append (plans-intro (string-append "Added entry for " account)) (plan->html (list account password email fullname plan)) "
" (string #\newline) (standard-buttons account password)))) (error-page (string-append "Creation error: " (cadr result)) (login-code)))))) ;;; Procedure: ;;; finish-update-page ;;; Parameters: ;;; plans, a list of plans ;;; account, a string ;;; password, a string ;;; email, a string ;;; fullname, a string ;;; plan, a string ;;; Purpose: ;;; Finishes updating a plan. ;;; Produces: ;;; page, a string ;;; Preconditions: ;;; account and password name a valid account. ;;; Postconditions: ;;; page is valid HTML for a complete page. (define finish-update-page (lambda (plans account password email fullname plan) (let ((result (update-plan (list account password email fullname plan) plans))) (if (car result) (make-page (head (string-append "[Plans] Updated: " account)) (body (string-append (plans-intro (string-append "Updated entry for " account)) (plan->html (list account password email fullname plan)) "
" (string #\newline) (standard-buttons account password)))) (error-page (string-append "Update error: " (cadr result)) ""))))) ;;; Procedure: ;;; login-page ;;; Parameters: ;;; None ;;; Purpose: ;;; Produces a page that lets someone log in to the plans system. ;;; Produces: ;;; page, a string ;;; Preconditions: ;;; None ;;; Postconditions: ;;; page is valid HTML for a complete page. (define login-page (lambda () (make-page (head "[Plans] Please Log In") (body (string-append (plans-intro "Login") (login-code)))))) ;;; Procedure: ;;; lookup-page ;;; Parameters: ;;; plans, a list of plans ;;; account, a string ;;; password, a string ;;; desired-account, a string ;;; Purpose: ;;; Produces a page that gives information on the desired account. ;;; Produces: ;;; page, a string ;;; Preconditions: ;;; account and password name a valid account. ;;; Postconditions: ;;; page is valid HTML for a complete page. (define lookup-page (lambda (plans account password desired-account) (make-page (head (string-append "[Plans] Lookup: " desired-account)) (body (string-append (plans-intro (string-append "Lookup: " desired-account)) (let ((info (search (left-section user-plan? desired-account) plans))) (if info (plan->html info) (paragraph (string-append "Sorry, no plan for " desired-account)))) "
" (string #\newline) (standard-buttons account password)))))) ;;; Procedure: ;;; update-page ;;; Parameters: ;;; plans, a list of plans ;;; account, a string ;;; password, a string ;;; email, a string ;;; fullname, a string ;;; plan, a string ;;; Purpose: ;;; Builds a page in which someone can update his or her plan. ;;; Produces: ;;; page, a string ;;; Preconditions: ;;; account and password name a valid account. ;;; Postconditions: ;;; page is valid HTML for a complete page. (define update-page (lambda (plans account password) (let ((plan (search (left-section user-plan? account) plans))) (make-page (head (string-append "[Plans] Update plan for " account)) (body (string-append (plans-intro (string-append "Update plan for " account)) (form cgi-script "Update" (string-append (hidden "operation" "finish-update") (apply fivefields plan))) "
" (string #\newline) (standard-buttons account password))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Primary User interface ;;; Procedure: ;;; page ;;; Parameters: ;;; None ;;; Purpose: ;;; Generates an appropriate HTML page for the current request, ;;; if there is one. ;;; Produces: ;;; html, a string ;;; Preconditions: ;;; Just the normal ones. ;;; Postconditions: ;;; Returns HTML for a valid page. The form of the page ;;; depends on the various CGI variables described at the ;;; beginning of this program. (define page (lambda () ; Determine what the operation is. (let ((op (get-cgi-var "operation" ""))) (cond ; If there is no basic operation, print the standard ; login page. ((string=? "" op) (login-page)) ; If we're creating a new account, print the account ; creation page. ((string=? op "start-create") (creation-page)) ; Otherwise, we need to read the plans file before ; continuing. (else ; We're going to be using the plans file, so lock it. (lockfile plans-file) (let ( ; Read the plans, since we probably need them. (plans (read-plans)) ; Get the account and password (account (get-cgi-var "account" "")) (password (get-cgi-var "password" "")) (newpassword (get-cgi-var "newpassword" "")) (email (get-cgi-var "email" "")) (fullname (get-cgi-var "fullname" "")) (plan (get-cgi-var "plan" ""))) (let ((result-page (handle-request op plans account password newpassword email fullname plan))) (unlockfile plans-file) result-page))))))) ; A simple helper to process a request that has already been ; extracted. (define handle-request (lambda (op plans account password newpassword email fullname plan) (cond ; Are we ready to finish creating the page? ((string=? op "finish-create") (finish-creation-page plans account newpassword email fullname plan)) ; Check that the account and password are valid. ((not (search (lambda (plan) (and (string=? (car plan) account) (string=? (cadr plan) password))) plans)) (error-page "Invalid Login" (login-code))) ; Start updating ((string=? op "start-update") (update-page plans account password)) ; Finish updating - enter into file ((string=? op "finish-update") (finish-update-page plans account newpassword email fullname plan)) ; Look up someone ((string=? op "lookup") (lookup-page plans account password (get-cgi-var "planner" ""))) ; Default page ((string=? op "login") (default-page account password)) (else (error-page "Significant Error" (standard-buttons account password)))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Stuff for testing (define samr (list "rebelsky" "foo" "rebelsky@grinnell.edu" "Sam Rebelsky" "Sorry, I never plan to do anything.")) (define william (list "william" "will" "rebelsky@grinnell.edu" "William Lloyd Rebelsky" "To convice my parents to call me Will.")) (define test-update (lambda () (display (update-page (read-plans) "sample" "sample")))) (define test-login (lambda () (handle-request "login" (read-plans) "sample" "sample" "" "" "")))