• Publicity: Public Only All

static-pages-procs.tcl


Procedures in this file

Detailed information

[ hide source ] | [ make this the default ]

Content File Source

# packages/static-pages/tcl/static-pages-procs.tcl
ad_library {
    Utilities for static pages.

    @author Brandoch Calef (bcalef@arsdigita.com)
    @creation-date 2001-01-22
    @cvs-id static-pages-procs.tcl,v 1.11.2.13 2003/02/06 13:05:51 jeffd Exp
}


ad_proc -public sp_sync_cr_with_filesystem_scheduled {{}} {

    Sync the filesystem and the content repository in a scheduled
    procedure, rather than manually.  Calls sp_sync_cr_with_filesystem
    just like the www/admin/fs-scan-progress.tcl page does.

    <p>
    Note that if you have comments turned on, be <em>very careful</em>
    running this, as the current implementation of
    sp_sync_cr_with_filesystem will <em>destroy</em> any user
    contributed comments on the file if you temporarily delete the
    file, then run that procedure.

    @author Andrew Piskorski (atp@piskorski.com)
    @creation-date 2002/09/12
} {
    set proc_name {sp_sync_cr_with_filesystem_scheduled}
    ns_log Notice "$proc_name: Starting"

    # sp_sync_cr_with_filesystem callbacks to fill file_items with info:

    proc sp_sch_old_item { path id } {}
    proc sp_sch_new_item { path id } {}
    proc sp_sch_changed_item { path id } {
        # The title may have changed:
        sp_flush_page $id
    }

    # TODO: We can have more than one package instance, so must decide
    # here WHICH package instance to run the sync for.  This should
    # probably be something configurable for each package instance from
    # the admin page, but for now we simply find and sync ALL package
    # instances: --atp@piskorski.com, 2002/09/12 14:02 EDT

    set package_key [sp_package_key_is]

    db_foreach each_apm_package_instance {
        select  package_id, instance_name
        from apm_packages
        where package_key = :package_key
        order by package_id
    } {
        set root_folder_id [sp_root_folder_id $package_id]
        set fs_root "[acs_root_dir][parameter::get -package_id $package_id -parameter {fs_root}]"

        ns_log Debug "$proc_name: About to scan the filesystem for:  package_id '$package_id', instance_name '$instance_name', fs_root '$fs_root':"

        # If our call to sp_sync_cr_with_filesystem fails for some
        # reason, want to continue on trying the other package
        # instances:

        set sync_proc {sp_sync_cr_with_filesystem}
        if { [catch {
            set result [$sync_proc  -package_id $package_id \
                            -file_unchanged_proc    sp_sch_old_item \
                            -file_add_proc          sp_sch_new_item \
                            -file_change_proc       sp_sch_changed_item \
                            -folder_add_proc        sp_sch_new_item \
                            -folder_unchanged_proc  sp_sch_old_item \
                            $fs_root $root_folder_id]
        } errmsg] } {
            global errorInfo
            ns_log Error "$proc_name: For package_id: '$package_id', $sync_proc failed with error:\n${errorInfo}"
        } else {
            ns_log Debug "$proc_name: For package_id: '$package_id': $result"
        }

    } if_no_rows {
        ns_log Warning "$proc_name: NO package ids found for package key: '$package_key'."
    }

    ns_log Debug "$proc_name: Done."
}


d_proc -public sp_sync_cr_with_filesystem {
    {
        -file_add_proc ""
        -file_change_proc ""
        -file_unchanged_proc ""
        -file_read_error_proc ""
        -folder_add_proc ""
        -folder_unchanged_proc ""
        -package_id ""
    }
    fs_root
    root_folder_id
    {
       static_page_regexp {}
    }
} {
    Synchronize the content repository with the file system.
    This creates entries in sp_folders and static_pages, so the static_page
    functions must be used to delete entries.

    @param file_add_proc The name of a Tcl proc to be called for each file
                         added.  The full file path and the page_id will be
                         passed to it.

    @param file_change_proc The name of a Tcl proc to be called for each file
                            changed in the database.

    @param file_unchanged_proc The name of a Tcl proc to be called for each file
                            unchanged in the database.

    @param folder_add_proc The name of a Tcl proc to be called for each folder
                           added.  The full file path and the folder_id will be
                           passed to it.

    @param folder_unchanged_proc The name of a Tcl proc to be called for each folder
                                 unchanged in the database.

    @param fs_root The starting path in the filesystem.  Files below this point will 
                   be scanned.

    @param root_folder_id The id of the root folder in the static-pages system (and in 
                          the content repository) obtained from 
                          <code>static_page.get_root_folder</code>.

    @param static_page_regexp A regexp to identify static pages.

    @param package_id Optionally, the package id of the Static Pages
    instance.  If not specified, determined from ad_conn.

    @author Andrew Piskorski (atp@piskorski.com)
    @creation-date 2001/08/27
} {
   if { $package_id eq "" } {
      set package_id [ad_conn package_id]
   }

   if { [catch { set return_val [sp_sync_cr_with_filesystem_internal \
           -file_add_proc          $file_add_proc         \
           -file_change_proc       $file_change_proc      \
           -file_unchanged_proc    $file_unchanged_proc   \
           -file_read_error_proc   $file_read_error_proc  \
           -folder_add_proc        $folder_add_proc       \
           -folder_unchanged_proc  $folder_unchanged_proc \
           -package_id             $package_id            \
           -stack_depth  2 \
           {return_mesg}  $fs_root  $root_folder_id  $static_page_regexp ]
   } result] } {
      # We caught an unexpected error, so clean up the mutex, and then
      # re-throw the exact same error:

      sp_sync_cr_with_filesystem_unlock $package_id

      global errorInfo
      error $result $errorInfo
   } else {
      return $return_mesg
   }
}


ad_proc -private sp_sync_cr_with_filesystem_unlock {package_id} {
   Unlocks the sp_sync_cr_with_filesystem_times variable - use upon
   abnormal termination of the sp_sync_cr_with_filesystem_internal
   stuff.  We have it as a separate proc here to make it convenient to
   call from within multiple different procedures.

   @author Andrew Piskorski (atp@piskorski.com)
   @creation-date 2001/08/27
} {
    set mutex [nsv_get . {sp_sync_cr_fs_mutex}]
    ns_mutex lock $mutex
    nsv_set {sp_sync_cr_fs_times} $package_id {}
    ns_mutex unlock $mutex
}


d_proc -private sp_sync_cr_with_filesystem_internal {
    {
        -file_add_proc ""
        -file_change_proc ""
        -file_unchanged_proc ""
        -file_read_error_proc ""
        -folder_add_proc ""
        -folder_unchanged_proc ""
        -package_id ""
        -stack_depth 1
    }
    return_mesg_var
    fs_root
    root_folder_id
    {
       static_page_regexp {}
    }
} {
    This procedure was originally named sp_sync_cr_with_filesystem
    procedure, but has been renamed and modified so that it can be
    wrapped inside the new sp_sync_cr_with_filesystem, to support the
    mutex locking.
    <p>
    We wrap it because at the end of this proc, we must set
    sp_sync_cr_with_filesystem_times($package_id) back to empty string.
    But if we hit some random untrapped error partway through, we'll
    never get there.  Therefore, we wrap this proc inside another, and
    have the wrapper proc catch any errors thrown by this proc, set the
    var back to empty string, then re-throw the error.
    <p>
    This procedure takes the exact same arguments as its
    sp_sync_cr_with_filesystem wrapper proc, except for the addition of
    return_mesg_var.
    <p>
    You should <em>never</em> call this procedure, except from
    sp_sync_cr_with_filesystem.

    @param return_mesg_var Name of variable in which to return text
    message, for presentation on a web page to the user.

    @param package_id <em>Must</em> be passed in, for this internal
    version of the proc.

    @author Brandoch Calef (bcalef@arsdigita.com)
    @author Andrew Piskorski (atp@piskorski.com)
    @creation-date 2001-02-07
} {
    set proc_name {sp_sync_cr_with_filesystem_internal}

    if { $package_id eq "" } {
        error "package_id '$package_id' is not valid."
    }
    upvar $return_mesg_var return_mesg
    set return_mesg {}


    # Make sure that only 1 copy of this proc per package instance
    # ever runs at once:

    set mutex [nsv_get . {sp_sync_cr_fs_mutex}]
    set nsv {sp_sync_cr_fs_times}

    # These multiple nsv operations need to all be atomic, so use a
    # mutex:

    ns_mutex lock $mutex

    if { ![nsv_exists $nsv $package_id] } {
        # The package_id isn't in the array yet at all, so another copy
        # is not running.
        set other_start_time {}
    } else {
        set other_start_time [nsv_get sp_sync_cr_fs_times $package_id]
    }

    if { $other_start_time eq "" } {
        # We're ok, no other copy is running.
        nsv_set $nsv $package_id [ns_time]
        set run_p 1
    } else {
        set run_p 0
    }

    ns_mutex unlock $mutex

    if { ! $run_p } {
        # Another copy is running, must abort:
        set time_diff [expr {[ns_time] -  $other_start_time}]

        set other_time_pretty [ns_httptime $other_start_time]
        # Could also use: [clock format [clock seconds]]

        set mesg "sp_sync_cr_with_filesystem: Already running. sp_sync_cr_fs_times($package_id) == $other_time_pretty$time_diff seconds ago."
        ns_log Warning $mesg

        set return_mesg "Another copy of this procedure is already running for
        this package instance.  It started running $time_diff seconds
        ago, at $other_time_pretty.  Only one copy may run at a time.
        Please wait and then try again."

        # Whether you actually see this happen depends whether the
        # second thread running this proc gets scheduled or not before
        # the first one completes.  If your machine is slow enough, or
        # you have enough threads going at once, you will see it.
        # --atp@piskorski.com, 2002/12/16 03:57 EST

        return 0
    }


    set sync_session_id [db_nextval sp_session_id_seq]

    set fs_trimmed [string trimright $fs_root "/"]
    set fs_trimmed_length [string length $fs_trimmed]

    set static_page_regexp "\\.[join [split [string trim [parameter::get -package_id $package_id -parameter AllowedExtensions]] " "] "$|\\."]$"

    # TODO: What happens if at some point, an Admin CHANGES the
    # fs_root parameter for a Static Pages package instance?  BAD
    # THINGS, I suspect.  We're probably invisibly orphaning content
    # inside the Content Repository.  Look into this.  For now, simply
    # DO NOT change the fs_root of an already in use Static Pages
    # package instance.
    # --atp@piskorski.com, 2002/09/15 10:03 EDT

    foreach file [ad_find_all_files $fs_root] {
        if { [regexp -nocase $static_page_regexp $file match] } {
            # Chop the starting path off of the full pathname and split it up:
            set path [split [string range $file $fs_trimmed_length end] "/"]
            # Throw away the first entry (empty) and the last entry (which is the filename):
            set path [lrange $path 1 [expr {[llength $path]-2}]]

            set cumulative_path ""
            set parent_folder_id $root_folder_id
            foreach directory $path {
                append cumulative_path "$directory/"
                if (![info exists path_exists($cumulative_path)]) {
                    # check db
                    set folder_id [db_string get_folder_id {
                        select nvl(content_item.get_id(:cumulative_path,:root_folder_id),0)
                        from dual
                    }]
                    # If the folder doesn't exist, create it.
                    if { $folder_id == 0} {
                        set folder_id [db_exec_plsql create_new_folder {}]
                        if { [string length $folder_add_proc] > 0 } {
                            uplevel $stack_depth "$folder_add_proc $cumulative_path $folder_id"
                        }
                    } else {
                        if { [string length $folder_unchanged_proc] > 0 } {
                            uplevel $stack_depth "$folder_unchanged_proc $cumulative_path $folder_id"
                        }
                    }
                    set path_exists($cumulative_path$folder_id
                    db_dml insert_path {
                        insert into sp_extant_folders (session_id,folder_id)
                        values (:sync_session_id,:folder_id)
                    }

                } else {
                    set folder_id $path_exists($cumulative_path)
                }
            set parent_folder_id $folder_id
            }

            # If the file is already in the db:
            #    Fetch it from the db and load the file from the filesystem
            #    If they differ:
            #        Insert the filesystem version into the db.
            # If the file isn't in the db:
            #    Insert it.

            # set sp_filename to the file path relative to the OpenACS
            # install dir, this is what gets inserted into the db - DaveB
            set sp_filename [sp_get_relative_file_path $file]
            set mtime_from_fs [file mtime $file]

            if [db_0or1row check_db_for_page {
                select static_page_id, mtime as mtime_from_db from static_pages
                where filename = :sp_filename
            }] {
                if { [catch {
                    set fp [open $file r]
                    set file_from_fs [read $fp]
                    close $fp
                } errmsg]} {
                    # Log and return an appropriate message, then
                    # continue on trying to process the other files.
                    # We do NOT want to abort the whole scan just
                    # because one file had problems:
                    # --atp@piskorski.com, 2002/09/12 16:49 EDT

                    set mesg "$proc_name: Error reading file: '$file':  [ns_quotehtml $errmsg]"
                    ns_log Error $mesg
                    if { $file_read_error_proc ne "" } {
                        uplevel $stack_depth [list $file_read_error_proc $file $static_page_id $mesg]
                    }
                    continue
                }

                set file_updated 0

                set storage_type [db_string get_storage_type ""]

                switch $storage_type {
                    "file" {
                        if {$mtime_from_fs != $mtime_from_db} {
                            set file_updated 1
                        }
                    }
                    "lob" {
                        db_1row get_db_page {
                            select content as file_from_db from cr_revisions
                            where revision_id = content_item.get_live_revision(:static_page_id)
                        }
                        if {$file_from_db != $file_from_fs} {
                             set file_updated 1
                        }
                    }
                }

                if {$file_updated == 1} {
                    db_dml update_db_file {
                        update cr_revisions set content = empty_blob()
                        where revision_id = content_item.get_live_revision(:static_page_id)
                        returning content into :1
                    } -blob_files [list $file]
                    if {$storage_type=="file"} {
                        db_dml update_static_page {
                            update static_pages set mtime = :mtime_from_fs
                            where  static_page_id = :static_page_id
                        }
                    }
                    if { [string length $file_change_proc] > 0 } {
                        uplevel $stack_depth "$file_change_proc $file $static_page_id"
                    }
                } else {
                    if { [string length $file_unchanged_proc] > 0 } {
                        uplevel $stack_depth "$file_unchanged_proc $file $static_page_id"
                    }
                }
                db_dml insert_file {
                    insert into sp_extant_files (session_id,static_page_id)
                    values (:sync_session_id,:static_page_id)
                }
            } else {
                # The file is NOT in the db yet at all:
                set static_page_id {}

                # Try to extract a title:
                if { [catch {
                    set fp [open $file r]
                    set file_contents [read $fp]
                    close $fp
                } errmsg]} {
                    # Log and return an appropriate message, then
                    # continue on trying to process the other files.
                    # We do NOT want to abort the whole scan just
                    # because one file had problems:
                    # --atp@piskorski.com, 2002/09/12 16:49 EDT

                    set mesg "$proc_name: Error reading file: '$file':  [ns_quotehtml $errmsg]"
                    ns_log Error $mesg
                    if { $file_read_error_proc ne "" } {
                        uplevel $stack_depth [list $file_read_error_proc $file $static_page_id $mesg]
                    }
                    continue
                }

                # TODO:  This is very HTML specific:  --atp@piskorski.com, 2001/08/13 21:58 EDT
                if { ![regexp -nocase {<title.*?>(.+?)</title} $file_contents match page_title] } {
                    regexp {[^/]*$} $file page_title
                }

                # Insert into the db:
                # the Oracle driver apparently doesn't support passing BLOBs to
                # PL/SQL (or passing a BLOB from a file into a PL/SQL function),
                # so the PL/SQL call and the BLOB loading must be performed
                # separately.  This is simple (get item_id from static_page.new(),
                # then update cr_revisions to insert the blob) but involved direct
                # manipulation of the cr_revisions table.

                # If you run two copies of sp_sync_cr_with_filesystem
                # at once, you CAN get "ORA-00001: unique constraint
                # (CR_ITEMS_UNIQUE_NAME) violated" errors here when
                # calling static_page.new - thus the addition of mutex
                # locking.  --atp@piskorski.com, 2001/08/27 01:20 EDT

                set mime_type [cr_filename_to_mime_type -create $sp_filename]

                if { [catch {
                    set static_page_id [db_exec_plsql do_sp_new {}]
                } errmsg] } {
                    # Something failed:

                    set mesg "$proc_name: do_sp_new failed for file '$file' with error:  [ns_quotehtml $errmsg]"
                    ns_log Error $mesg
                    if { $file_read_error_proc ne "" } {
                        uplevel $stack_depth [list $file_read_error_proc $file $static_page_id $mesg]
                    }
                    continue

                } else {
                    # Everything is ok:

                    # Check if -blobs [list $file_contents] would be faster:
                    db_dml insert_file_contents {} -blob_files [list $file]

                    if { [string length $file_add_proc] > 0 } {
                        uplevel $stack_depth "$file_add_proc $file $static_page_id"
                    }
                    db_dml insert_file {
                        insert into sp_extant_files (session_id,static_page_id)
                        values (:sync_session_id,:static_page_id)
                    }
                }
            }
        }
    }

    # TODO: This is very wrong.  Should NEVER just delete all content!
    # Note that the canonical content of the file itself lives in the
    # file system and can easily be re-imported to the database, but
    # this ALSO blindly deletes all user-contributed comments which
    # point to the file!
    #
    # See also: http://openacs.org/bboard/q-and-a-fetch-msg.tcl?msg_id=0002U2
    #
    # --atp@piskorski.com, 2001/08/13 15:07 EDT

    # Clean up any files that are in the db but no longer in the filesystem:

    # TODO: Why are we doing these two deletes after calling
    # static_page.delete_stale_items?
    # --atp@piskorski.com, 2001/08/23 02:20 EDT 

    db_exec_plsql delete_old_files {
    begin
        static_page.delete_stale_items(:sync_session_id,:package_id);

        delete from sp_extant_folders where session_id = :sync_session_id;
        delete from sp_extant_files where session_id = :sync_session_id;
    end;
    }

    # TODO: We should have a sp_deleted_item hook just like we do for
    # new, old, and changd items.  As it is now, we delete the file
    # out of the database but provide NO notification that we did so.
    # --atp@piskorski.com, 2002/12/12 12:43 EST

    sp_sync_cr_with_filesystem_unlock $package_id
    set return_mesg "Done."
    return 0
}


ad_proc -public sp_root_folder_id { package_id } {
    Returns the id of the root folder associated with package_id,
    creating one if necessary.

    @author Brandoch Calef (bcalef@arsdigita.com)
    @creation-date 2001-02-23
} {
    # We must use db_exec_plsql rather than simply selecting from dual
    # because static_page.get_root_folder will do DML (to create the
    # folder) if it can't find a root folder.
    return [db_exec_plsql get_root_folder_id {
        begin
            :1 := static_page.get_root_folder(:package_id);
        end;
    }]
}

d_proc -public sp_change_matching_permissions {
    root_folder_id
    contained_string
    grant_or_revoke
} {
    Grant or revoke permissions on all files below root_folder_id
    whose filenames contain contained_string.

    @author Brandoch Calef (bcalef@arsdigita.com)
    @creation-date 2001-02-23
} {
    if { $grant_or_revoke != "grant" && $grant_or_revoke != "revoke" } {
        ns_log Warning "sp_change_matching_permissions called with grant_or_revoke = $grant_or_revoke"
        return
    }

    db_exec_plsql grant_or_revoke_matching_permissions "
        begin
        for file_row in (
            select static_page_id from static_pages
            where folder_id in (
                select folder_id from sp_folders
                start with folder_id = :root_folder_id
                connect by parent_id = prior folder_id)
            and filename like '%${contained_string}%'
        ) loop
            acs_permission.${grant_or_revoke}_permission(
                object_id => file_row.static_page_id,
                grantee_id => acs.magic_object_id('the_public'),
                privilege => 'general_comments_create'
            );
        end loop;
        end;
    "
}

d_proc -public sp_change_matching_display {
    root_folder_id
    contained_string
    show_full_comments_p
} {
    Set all files below root_folder_id whose filenames contain 
    contained_string to have comments either shown (full contents of
    comments are displayed on the page) or summarized (title line of
    comments are listed).

    @author Brandoch Calef (bcalef@arsdigita.com)
    @creation-date 2001-02-23
} {
    if { $show_full_comments_p != "t" && $show_full_comments_p != "f" } {
        ns_log Warning "sp_change_matching_permissions called with show_full_comments_p = $show_full_comments_p"
        return
    }

    db_foreach matching_static_page "
        select static_page_id from static_pages
             where folder_id in (
                 select folder_id from sp_folders
                 start with folder_id = :root_folder_id
                 connect by parent_id = prior folder_id)
             and filename like '%${contained_string}%'
        " {
            sp_flush_page $static_page_id
        }

    db_dml show_or_summarize_comments_matching "
        update static_pages set show_comments_p = :show_full_comments_p
                where static_page_id in (
            select static_page_id from static_pages
            where folder_id in (
                select folder_id from sp_folders
                start with folder_id = :root_folder_id
                connect by parent_id = prior folder_id)
            and filename like '%${contained_string}%'
            )
    "
}

ad_proc -private  sp_get_full_file_path { file } {
    takes a relative path and returns the full file path
} {
    set full_path [cr_fs_path STATIC_PAGES]
    append full_path $file
    return $full_path
}

ad_proc -private sp_get_relative_file_path { file } {
    Takes a full file path and returns the path relative to the
    static-page storage directory, usually /web/openacs/www/
} {
    set relative_path [string range $file [string length [cr_fs_path STATIC_PAGES]] end]

    ns_log debug "**[cr_fs_path STATIC_PAGES]**"
    ns_log debug "relative path:$relative_path"
    return $relative_path
}


ad_proc -private sp_get_page_info_query { page_id } {
    Returns a SQL query to get the page title and comment display policy.

    @author Brandoch Calef (bcalef@arsdigita.com)
    @creation-date 2001-02-23
} {
    return [db_string get_page_info "select '{'||content_item.get_title($page_id)||'} '||decode(show_comments_p,'t',1,0) from static_pages where static_page_id = $page_id"]
}


ad_proc -private sp_get_page_id { filename } {
    Returns a two item list of the page_id and the static-pages
    package_id it belongs to.

    @author Dave Bauer (dave@thedesignexperience.org)
    @creation-date 2001-07-30
} {
    # This package is no longer a singleton, so can't use
    # apm_package_id_from_key here: --atp@piskorski.com, 2001/08/26
    # 22:37 EDT

    set package_key [sp_package_key_is]
    if { [db_0or1row page_and_package_ids {}] } {
        set results [list $static_page_id $package_id]
    } else {
        set results [list -1 -1]
    }

    return $results
}

ad_proc -public sp_flush_page { page_id } {
    Flushes the cache entry for a static page.  This should be done whenever the
    page title or show_comment_p setting change.

    @author Brandoch Calef (bcalef@arsdigita.com)
    @creation-date 2001-02-23
} {
    util_memoize_flush [list sp_get_page_info_query $page_id]
}


ad_proc -public sp_package_key_is {} {
    Simply returns the package key string for this package.
    @author Andrew Piskorski (atp@piskorski.com)
    @creation-date 2001/08/26
} {
    # TODO: Might want to have this pull and cache the actual key from
    # the database.
    return {static-pages}
}


ad_proc -private sp_package_url {package_key} {
    <p>Given a package key, return a URL of a mounted
    package instance. If there is more than one instance
    of the package mounted, the one with the lowest
    <code>package_id</code> will be returned. If the
    package is not instantiated or not mounted anywhere,
    an error is raised. The proc is meant to be memoized.
    </p>
} {
    set proc_name {sp_package_url}

    set found_p [db_0or1row get_any_package_instance {
        select min(package_id) as package_id
        from apm_packages
        where package_key = :package_key
    }]

    if { !$found_p } {
        error "$proc_name: the '$package_key' package is not instantiated."
    }

    set found_p [db_0or1row get_mount_point {}]

    if { !$found_p } {
        error "$proc_name: the '$package_key' package is not mounted."
    }

    return $url
}


ad_proc -public sp_serve_html_page { } {
    Registered proc to serve up static pages.

    @author Brandoch Calef (bcalef@arsdigita.com)
    @creation-date 2001-01-23
} {
    set filename [ad_conn file]
    set sp_filename [sp_get_relative_file_path $filename]

    # In order to determine per-instance parameters like
    # TemplatingEnabledP, need to know the package_id of the
    # static-pages instance where this page is located, which is
    # likely NOT the package_id returned by [ad_conn package_id]:
    foreach [list page_id package_id] \
        [util_memoize [list sp_get_page_id $sp_filename]] { break }

    set templating_enabled_p [parameter::get -package_id $package_id -parameter TemplatingEnabledP -default 0]
    set comment_p [parameter::get -package_id $package_id -parameter CommentsDisplayedP -default 1]

    set file [ad_conn file]
    ad_conn -set subsite_id [site_node::closest_ancestor_package -include_self -package_key "acs-subsite"]

    # If the page is in the db, serve it carefully; otherwise just dump it out.
    # We are careful to use ns_returnfile if possible since it is much more 
    # efficient than reading the whole file into a tcl string and doing ns_return, 
    # plus it will give 304's if the file is unchanged.

    if { $page_id >= 0 } {
        set page_info [util_memoize [list sp_get_page_info_query $page_id]]

        # TODO: Below, what if we only allow registered users to make
        # comments?  Or some smaller group of users?  What then?
        # --atp@piskorski.com, 2001/08/22 23:09 EDT

        # We only show the link here if the_public has 
        # general_comments_create privilege on the page.  Why the_public
        # rather than the current user?  Because we don't want admins to
        # be seeing "Add a comment" links on non-commentable pages.

        set comment_link ""
        if { $comment_p } {
            if { [permission::permission_p -party_id [acs_magic_object the_public] -object_id $page_id -privilege general_comments_create] } {
                append comment_link "<center>[general_comments_create_link -object_name [lindex $page_info 0] $page_id [ad_conn url]]</center>"
            }
            append comment_link "[general_comments_get_comments -print_content_p [lindex $page_info 1] $page_id [ad_conn url]]"
        }

        # Here if comment_link is empty and we are not templating, just ns_returnfile.
        if { $comment_link eq ""
            && ! $templating_enabled_p } {
            ns_returnfile 200 text/html $filename
            return
        } else {
            if { [catch {
                set fp [open $filename r]
                set file_contents [read $fp]
                close $fp
            } errmsg] } {
                ad_return_error "Error reading file" \
                    "This error was encountered while reading $filename: $errmsg"
                ad_script_abort
            }


            # Tcl needs a case-insensitive [string first] function.
            #
            set body_close [string first "</body" [string tolower $file_contents]]
            if { $body_close >= 0 } {
                set body "[string range $file_contents 0 [expr {$body_close-1}]]${comment_link}[string range $file_contents $body_close end]"
            } else {
                set body "${file_contents}$comment_link"
            }
        }
    } else {
        if { ! $templating_enabled_p } {
            # doing ns_returnfile here means we will get 304's when we can
            ns_returnfile 200 text/html $file
            return {}
        } else {
            set body [template::util::read_file $file]
        }
    }

    # If we did not return a file directly above we need to return the body, possibly after 
    # wrapping it in the master template.
    if { $templating_enabled_p } {
        # Strip out the <body>..</body> part as page will now be part of a master template
        set headers ""
        set sp_scripts ""
        set title ""
        if {[regexp -nocase {(.*?)<body.*?>(.*)</body.*?>} $body match headers bodyless]} {
            set body $bodyless
        }
        # Get 0 or 1 <title>...</title> data to pass up to master template html headers
        regexp -nocase {<title.*?>(.*?)</title.*?>} $headers match title

        # Get 0 or more <script>...</script> tags to pass up to master template html headers
        while {[regexp -nocase {(<script.*?>.*?</script.*?>)(.*$)} $headers match ascript headers]} {
            append sp_scripts "\n$ascript"
        }

        set file_mtime [clock format [file mtime $file]]

        set body [template::adp_parse [acs_root_dir]/[parameter::get -package_id $package_id -parameter TemplatePath] [list body $body sp_scripts $sp_scripts title "$title" file_mtime $file_mtime page_id $page_id] ]
    }
    ns_return 200 text/html $body
}


ad_proc -private sp_register_extension {} {
    Register the handler for each static page file extension.
} {
    set proc_name {sp_register_extension}

    set package_key [sp_package_key_is]
    set package_ids [db_list all_static_pages_package_instances {
        select package_id
        from apm_packages
        where package_key = :package_key
    }]

    # Generate unique list of all file-name extensions used by all
    # package instances:

    array set extensions_arr [list]
    foreach package_id $package_ids {
        foreach extension [split [string tolower [string trim [parameter::get -package_id $package_id -parameter AllowedExtensions]]] " "] {
            set extensions_arr($extension) {}
        }
    }

    foreach extension [array names extensions_arr] {
        # TODO: Are we supposed to use the sp_serve_html_page proc for
        # ALL file-name extensions, even if they're PDF or MS Word
        # documents?  I think not!  Need a better way to map file-name
        # extensions to proper static-pages extension handler procs.
        # --atp@piskorski.com, 2002/12/11 22:55 EST

        if { [regexp {htm} $extension] } {
            set handler_proc {sp_serve_html_page}
            rp_register_extension_handler $extension $handler_proc
            rp_register_extension_handler [string toupper $extension$handler_proc
        } else {
            ns_log Warning "$proc_name: NOT registering any proc to handle files with extension '$extension'."

            # TODO: Add a PDF or other extension handler?  Necessary
            # only if you want to be able to make comments on non-HTML
            # files, I think.
            # --atp@piskorski.com, 2002/12/11 22:55 EST
        }
    }
}