;;; 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
"