0.00%
Search · Index

Weblog Page

Filtered by date 2009-02-13, 1 - 7 of 7 Postings (all, summary)

Install Full Text Search using OpenFTS (deprecated see tsearch2)

Created by Gustaf Neumann, last modified by Gustaf Neumann 13 Feb 2009, at 11:28 AM

By Joel Aufrecht and Malte Sussdorff

OpenACS docs are written by the named authors, and may be edited by OpenACS documentation staff.

OpenFTS and tsearch1 use is deprecated in favor of Tsearch2. See Install Full Text Search using Tsearch2. Tsearch2 is much easier to install, requiring only compilation of one module from PostgreSQL contrib, with an automated install process using the tsearch2-driver package.

If you want full text search, and you are running PostgreSQL, install this module to support FTS. Do this step after you have installed both PostgreSQL and AOLserver. You will need the openfts tarball in /tmp.

  1. Install Tsearch. This is a PostgreSQL module that OpenFTS requires.

    [root root]# su - postgres
    [postgres pgsql]$ cd /usr/local/src/postgresql-7.3.4/contrib/tsearch/
    [postgres tsearch]$ make
    sed 's,MODULE_PATHNAME,$libdir/tsearch,g' tsearch.sql.in >tsearch.sql
    /usr/bin/flex  -8 -Ptsearch_yy -o'parser.c' parser.l(many lines omitted)
    rm -f libtsearch.so
    ln -s libtsearch.so.0.0 libtsearch.so
    [postgres tsearch]$ make install
    mkdir /usr/local/pgsql/share/contrib
    mkdir /usr/local/pgsql/doc/contrib
    (2 lines omitted)
    /bin/sh ../../config/install-sh -c -m 755 libtsearch.so.0.0 /usr/local/pgsql/lib/tsearch.so
    [postgres tsearch]$ exit
    logout
    
    [root root]#
    su - postgres
    cd /usr/local/src/postgresql-7.3.4/contrib/tsearch
    make
    make install
    exit
  2. Unpack the OpenFTS tarball and compile and install the driver.

    [root root]# cd /usr/local/src
    [root src]# tar xzf /tmp/Search-OpenFTS-tcl-0.3.2.tar.gz
    [root src]# cd /usr/local/src/Search-OpenFTS-tcl-0.3.2/
    [root Search-OpenFTS-tcl-0.3.2]# ./configure --with-aolserver-src=/usr/local/src/aolserver/aolserver --with-tcl=/usr/lib/
    checking prefix... /usr/local
    checking for gcc... gcc
    (many lines omitted)
    configure: creating ./config.status
    config.status: creating Makefile.global
    [root Search-OpenFTS-tcl-0.3.2]# make
    (cd parser; make all)
    make[1]: Entering directory `/usr/local/src/Search-OpenFTS-tcl-0.3.2/parser'
    (many lines omitted)
    packages provided were {Lingua::Stem::Snowball 0.3.2}
    processed fts_base_snowball.tcl
    [root Search-OpenFTS-tcl-0.3.2]# cd aolserver
    [root aolserver]# make
    gcc -c -fPIC  -DPACKAGE=\"OPENFTS\" -DVERSION=\"0.3.2\" -DHAVE_UNISTD_H=1 -DSTDC_HEADERS=1 -DHAVE_SYS_TYPES_H=1 -DHAVE_SYS_STAT_H=1 -DHAVE_STDLIB_H=1 -DHAVE_STR
    (many lines omitted)
    n_stem.o italian_stem.o norwegian_stem.o portuguese_stem.o russian_stem.o nsfts.o  -o nsfts.so
    [root aolserver]# cp nsfts.so /usr/local/aolserver/bin/
    [root aolserver]#
    cd /usr/local/src
    tar xzf /tmp/Search-OpenFTS-tcl-0.3.2.tar.gz
    cd /usr/local/src/Search-OpenFTS-tcl-0.3.2/
    ./configure --with-aolserver-src=/usr/local/src/aolserver/aolserver --with-tcl=/usr/lib/
    make
    cd aolserver
    make
    cp nsfts.so /usr/local/aolserver/bin
  3. Build some supplemental modules.

    [root aolserver]# cd /usr/local/src/Search-OpenFTS-tcl-0.3.2
    [root Search-OpenFTS-tcl-0.3.2]# cp -r pgsql_contrib_openfts /usr/local/src/postgresql-7.3.4/contrib
    [root Search-OpenFTS-tcl-0.3.2]# cd /usr/local/src/postgresql-7.3.4/contrib/pgsql_contrib_openfts
    [root pgsql_contrib_openfts]# make
    sed 's,MODULE_PATHNAME,$libdir/openfts,g' openfts.sql.in >openfts.sql
    gcc -O2 -Wall -Wmissing-prototypes -Wmissing-declarations -fpic -I. -I../../src/include   -c -o openfts.o openfts.c
    gcc -shared -o openfts.so openfts.o
    rm openfts.o
    [root pgsql_contrib_openfts]# su postgres
    [postgres pgsql_contrib_openfts]$ make install
    /bin/sh ../../config/install-sh -c -m 644 openfts.sql /usr/local/pgsql/share/contrib
    /bin/sh ../../config/install-sh -c -m 755 openfts.so /usr/local/pgsql/lib
    /bin/sh ../../config/install-sh -c -m 644 ./README.openfts /usr/local/pgsql/doc/contrib
    [postgres pgsql_contrib_openfts]$ exit
    [root pgsql_contrib_openfts]#
    cd /usr/local/src/Search-OpenFTS-tcl-0.3.2
    cp -r pgsql_contrib_openfts /usr/local/src/postgresql-7.3.4/contrib
    cd /usr/local/src/postgresql-7.3.4/contrib/pgsql_contrib_openfts
    make
    su postgres
    make install
    exit

If you are installing Full Text Search, add required packages to the new database. (In order for full text search to work, you must also install the PostgreSQL OpenFTS module and prerequisites.)

[$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$ /usr/local/pgsql/bin/psql $OPENACS_SERVICE_NAME -f /usr/local/src/postgresql-7.3.4/contrib/tsearch/tsearch.sql
BEGIN
CREATE
(many lines omitted)
INSERT 0 1
COMMIT
[$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$ /usr/local/pgsql/bin/psql $OPENACS_SERVICE_NAME -f /usr/local/src/postgresql-7.3.4/contrib/pgsql_contrib_openfts/openfts.sql
CREATE
CREATE
[$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$
/usr/local/pgsql/bin/psql $OPENACS_SERVICE_NAME -f /usr/local/src/postgresql-7.3.4/contrib/tsearch/tsearch.sql
/usr/local/pgsql/bin/psql $OPENACS_SERVICE_NAME -f /usr/local/src/postgresql-7.3.4/contrib/pgsql_contrib_openfts/openfts.sql

Note

If you get the error ERROR: could not access file "$libdir/tsearch": no such file or directory It is probably because PostgreSQL's libdir configuration variable points to a diffent directory than where tsearch is. You can find out where PostgreSQL expects to find tsearch via

pg_config --pkglibdir

If you have installed OpenFTS, you can enable it for this service. Uncomment this line from config.tcl. (To uncomment a line in a tcl file, remove the # at the beginning of the line.)

#ns_param   nsfts           ${bindir}/nsfts.so
  1. Click Admin on the top of the default home page. If prompted, log in with the account and password you entered during install.

  2. Click on the Install software link.

  3. Click on the Install new service link.

  4. Click on the Install link next to OpenFTS Driver.

  5. Restart the service.

    [$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$ svc -t /service/$OPENACS_SERVICE_NAME
    
    [$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$
  6. Wait a minute, then browse back to the home page.

  7. Click on Admin on the top of the screen.

  8. Click on Main Site Administration in the "Subsite Administration" section.

  9. Click on Site Map in the "Advanced Features" section.

  10. Mount the OpenFTS Full Text Search Engine in the site map.

    1. Click the new sub folder link on the "/" line, the first line under Main Site:/.

    2. Type openfts and click New.

    3. On the new openfts line, click the mount link.

    4. Click OpenFTS Driver.

    5. On the openfts line, click set parameters.

    6. Change openfts_tcl_src_path to /usr/local/src/Search-OpenFTS-tcl-0.3.2/ and click Set Parameters

  11. Mount the Search interface in the site map.

    1. Click the new sub folder link on the Main Site line.

    2. Type search and click New.

    3. Click the new application link on the search line.

    4. Type search where it says untitled, choose search from the drop-down list, and click New.

  12. Restart the service.

    [$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$ svc -t /service/$OPENACS_SERVICE_NAME
    
    [$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$
  13. Wait a minute, then click on Main Site at the top of the page.

  14. Initialize the OpenFTS Engine. This creates a set of tables in the database to support FTS.

    Near the bottom of the page, click on the OpenFTS Driver link. Click on Administration. Click on Initialize OpenFTS Engine. Click Initialize OpenFTS Engine.

  15. Add the FTS Engine service contract

    1. Click on the DevAdmin.

    2. Click on the Service Contract link.

    3. On the FtsEngineDriver line, click Install.

  16. Restart the service.

    [$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$ svc -t /service/$OPENACS_SERVICE_NAME
    
    [$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$

Enabling Full Text Search in packages at the moment is not trivial. It involves a couple of steps, which I will illustrate taking lars-blogger as an example package

  1. Install the package.

    1. Click Admin on the top of the default home page. If prompted, log in with the account and password you entered during install.

    2. Click on the Install software link.

    3. Click on the Install new application link.

    4. Click on the Install link next to Weblogger.

    5. Install all required packages as well (always say okay until you shall restart the server)

  2. Load the service contracts datamodell and enable the service contract

    [$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$ cd packages/lars-blogger/sql/postgresql
    [$OPENACS_SERVICE_NAME postgresql]$ psql $OPENACS_SERVICE_NAME -f lars-blogger-sc-create.sql

    Note: Usually this script is called package_name-sc-create.sql

  3. Restart the service.

    [$OPENACS_SERVICE_NAME postgresql]$ svc -t /service/$OPENACS_SERVICE_NAME
    
                    [$OPENACS_SERVICE_NAME postgresl]$

If you are lucky, Full Text Search is enabled now, if not consult http://openacs.org/forums/message-view?message_id=154759. This link also contains some hints on how to make sure it is enabled.

Subsites Requirements

Created by Gustaf Neumann, last modified by Gustaf Neumann 13 Feb 2009, at 09:45 AM

By Rafael H. Schloming and Dennis Gregorovic

OpenACS docs are written by the named authors, and may be edited by OpenACS documentation staff.

The following is a requirements document for OpenACS 4 Subsites, part of the OpenACS 4 Kernel. The Subsites system allows one OpenACS server instance to serve multiple user communities, by enabling the suite of available OpenACS applications to be customized for defined user communities.

Many online communities are also collections of discrete subcommunities, reflecting real-world relationships. For example, a corporate intranet/extranet website serves both units within the company (e.g., offices, departments, teams, projects) and external parties (e.g., customers, partners, vendors). Subsites enable a single OpenACS instance to provide each subcommunity with its own "virtual website," by assembling OpenACS packages that together deliver a feature set tailored to the needs of the subcommunity.

The OpenACS subsite system allows a single OpenACS installation to serve multiple communities. At an implementation level this is primarily accomplished by having an application "scope" its content to a particular package instance. The request processor then figures out which package_id a particular URL references and then provides this information through the ad_conn api ([ad_conn package_id], [ad_conn package_url]).

The other piece of the subsite system is a subsite package that provides subsite admins a "control panel" for administering their subsite. This is the same package used to provide all the community core functionality available at the "main" site which is in fact simply another subsite.

The Subsites functionality is intended for use by two different classes of users:

  1. Package programmers (referred to as 'the programmer') must develop subcommunity-aware applications.

  2. Site administrators (referred to as 'the administrator') use subsites to provide tailored "virtual websites" to different subcommunities.

Joe Programmer is working on the forum package and wants to make it subsite-aware. Using [ad_conn package_id], Joe adds code that only displays forum messages associated with the current package instance. Joe is happy to realize that parameter::get is already smart enough to return configuration parameters for the current package instance, and so he has to do no extra work to tailor configuration parameters to the current subsite.

Jane Admin maintains www.company.com. She learns of Joe's work and would like to set up individual forums for the Boston and Austin offices of her company. The first thing she does is use the APM to install the new forum package.

Next, Jane uses the Subsite UI to create subsites for the Boston and Austin offices. Then Jane uses the Subsite UI to create forums for each office.

Now, the Boston office employees have their own forum at http://www.company.com/offices/boston/forum, and similarly for the Austin office. At this point, the Boston and Austin office admins can customize the configurations for each of their forums, or they can just use the defaults.

A subsite API is required for programmers to ensure their packages are subsite-aware. The following functions should be sufficient for this:

10.10.0 Package creation

The system must provide an API call to create a package, and it must be possible for the context (to which the package belongs) to be specified.

10.20.0 Package deletion

The system must provide an API call to delete a package and all related objects in the subsite's context.

10.30.0 Object's package information

Given an object ID, the system must provide an API call to determine the package (ID) to which the object belongs.

10.40.0 URL from package

Given a package (ID), the system must provide an API call to return the canonical URL for that package.

10.50.0 Main subsite's package_id

The system must provide an API call to return a package ID corresponding to the main subsite's package ID (the degenerate subsite).

The Programmer's User Interface

There is no programmer's UI, other than the API described above.

The Administrator's User Interface

The UI for administrators is a set of HTML pages that are used to drive the underlying API for package instance management (i.e. adding, removing, or altering packages). It is restricted to administrators of the current subsite such that administrators can only manage their own subsites. Of course, Site-Wide Administrators can manage all subsites.

  • 20.10.0 Package creation

    20.10.1 The administrator should be able to create a package and make it available at a URL underneath the subsite.

  • 20.20.0 Package deactivation

    20.20.1 The administrator should be able to deactivate any package, causing it to be inaccessible to users.

    20.20.5 Deactivating a package makes the package no longer accessible, but it does not remove data created within the context of that package.

Document Revision # Action Taken, Notes When? By Whom?
0.1 Creation 08/18/2000 Dennis Gregorovic
0.2 Edited, reviewed 08/29/2000 Kai Wu

Escaping to the procedural world

Created by Anett Szabo, last modified by Gustaf Neumann 13 Feb 2009, at 09:37 AM

Declarative languages can be very powerful and reliable, but sometimes it is easier to think about things procedurally. One way to do this is by using a procedural language in the database client. For example, with AOLserver we generally program in Tcl, a procedural language, and read the results of SQL queries. For example, in the /news module of the ArsDigita Community System, we want to

  • query for the current news
  • loop through the rows that come back and display one line for each row (with a link to a page that will show the full story)
  • for the first three rows, see if the news story is very short. If so, just display it on this page
The words above that should give a SQL programmer pause are in the last bullet item: if and for the first three rows. There are no clean ways in standard SQL to say "do this just for the first N rows" or "do something special for a particular row if its data match a certain pattern".

Here's the AOLserver Tcl program. Note that depending on the contents of an item in the news table, the Tcl program may execute an SQL query (to figure out if there are user comments on a short news item).


set selection [ns_db select $db "select *
from news
where sysdate between release_date and expiration_date
and approved_p = 't'
order by release_date desc, creation_date desc"]

while { [ns_db getrow $db $selection] } {
set_variables_after_query
# we use the luxury of Tcl to format the date nicely
ns_write "<li>[util_AnsiDatetoPrettyDate $release_date]: "
if { $counter <= 3 && [string length $body] < 300 } {
# it is one of the top three items and it is rather short
# so, let's consider displaying it right here
# first, let's go back to Oracle to find out if there are any
# comments on this item
set n_comments [database_to_tcl_string $db_sub "select count(*) from general_comments where on_what_id = $news_id and on_which_table = 'news'"]
if { $n_comments > 0 } {
# there are some comments; just show the title
ns_write "<a href=\"item.tcl?news_id=$news_id\">$title</a>\n"
} else {
# let's show the whole news item
ns_write "$title\n<blockquote>\n[util_maybe_convert_to_html $body $html_p]\n"
if {[parameter::get -parameter SolicitCommentsP -default 1]} {
ns_write "<br><br>\n<A HREF=\"comment-add.tcl?news_id=$news_id\">comment</a>\n"
}
ns_write "</blockquote>\n"
}
} else {
ns_write "<a href=\"item.tcl?news_id=$news_id\">$title</a>\n"
}
}

Suppose that you have a million rows in your news table, you want five, but you can only figure out which five with a bit of procedural logic. Does it really make sense to drag those million rows of data all the way across the network from the database server to your client application and then throw out 999,995 rows?

Or suppose that you're querying a million-row table and want the results back in a strange order. Does it make sense to build a million-row data structure in your client application, sort them in the client program, then return the sorted rows to the user?

Visit http://www.scorecard.org/chemical-profiles/ and search for "benzene". Note that there are 328 chemicals whose names contain the string "benzene":


select count(*)
from chemical
where upper(edf_chem_name) like upper('%benzene%');

COUNT(*)
----------
328
The way we want to display them is
  • exact matches on top
  • line break
  • chemicals that start with the query string
  • line break
  • chemicals that contain the query string
Within each category of chemicals, we want to sort alphabetically. However, if there are numbers or special characters in front of a chemical name, we want to ignore those for the purposes of sorting.

Can you do all of that with one query? And have them come back from the database in the desired order?

You could if you could make a procedure that would run inside the database. For each row, the procedure would compute a score reflecting goodness of match. To get the order correct, you need only ORDER BY this score. To get the line breaks right, you need only have your application program watch for changes in score. For the fine tuning of sorting equally scored matches alphabetically, just write another procedure that will return a chemical name stripped of leading special characters, then sort by the result. Here's how it looks:


select edf_chem_name,
edf_substance_id,
score_chem_name_match_score(upper(edf_chem_name),upper('%benzene%'))
as match_score
from chemical
where upper(edf_chem_name) like upper('%benzene%');
order by score_chem_name_match_score(upper(edf_chem_name),upper('benzene')),
score_chem_name_for_sorting(edf_chem_name)

We specify the procedure score_chem_name_match_score to take two arguments: one the chemical name from the current row, and one the query string from the user. It returns 0 for an exact match, 1 for a chemical whose name begins with the query string, and 2 in all other cases (remember that this is only used in queries where a LIKE clause ensures that every chemical name at least contains the query string. Once we defined this procedure, we'd be able to call it from a SQL query, the same way that we can call built-in SQL functions such as upper.

So is this possible? Yes, in all "enterprise-class" relational database management systems. Historically, each DBMS has had a proprietary language for these stored procedures. Starting in 1997, DBMS companies began to put Java byte-code interpreters into the database server. Oracle added Java-in-the-server capability with its 8.1 release in February 1999. If you're looking at old systems such as Scorecard, though, you'll be looking at procedures in Oracle's venerable PL/SQL language:


create or replace function score_chem_name_match_score
(chem_name IN varchar, query_string IN varchar)
return integer
AS
BEGIN
IF chem_name = query_string THEN
return 0;
ELSIF instr(chem_name,query_string) = 1 THEN
return 1;
ELSE
return 2;
END IF;
END score_chem_name_match_score;
Notice that PL/SQL is a strongly typed language. We say what arguments we expect, whether they are IN or OUT, and what types they must be. We say that score_chem_name_match_score will return an integer. We can say that a PL/SQL variable should be of the same type as a column in a table:

create or replace function score_chem_name_for_sorting (chem_name IN varchar)
return varchar
AS
stripped_chem_name chem_hazid_ref.edf_chem_name%TYPE;
BEGIN
stripped_chem_name := ltrim(chem_name,'1234567890-+()[],'' #');
return stripped_chem_name;
END score_chem_name_for_sorting;
The local variable stripped_chem_name is going to be the same type as the edf_chem_name column in the chem_hazid_ref table.

If you are using the Oracle application SQL*Plus to define PL/SQL functions, you have to terminate each definition with a line containing only the character "/". If SQL*Plus reports that there were errors in evaluating your definition, you then have to type "show errors" if you want more explanation. Unless you expect to write perfect code all the time, it can be convenient to leave these SQL*Plus incantations in your .sql files. Here's an example:



-- note that we prefix the incoming arg with v_ to keep it
-- distinguishable from the database column of the same name
-- this is a common PL/SQL convention
create or replace function user_group_name_from_id (v_group_id IN integer)
return varchar
IS
-- instead of worrying about how many characters to
-- allocate for this local variable, we just tell
-- Oracle "make it the same type as the group_name
-- column in the user_groups table"
v_group_name user_groups.group_name%TYPE;
BEGIN
if v_group_id is null
then return '';
end if;
-- note the usage of INTO below, which pulls a column
-- from the table into a local variable
select group_name into v_group_name
from user_groups
where group_id = v_group_id;
return v_group_name;
END;
/
show errors

Choosing between PL/SQL and Java

How to choose between PL/SQL and Java? Easy: you don't get to choose. In lots of important places, e.g., triggers, Oracle forces you to specify blocks of PL/SQL. So you have to learn at least the rudiments of PL/SQL. If you're going to build major packages, Java is probably a better long-term choice.

Reference

 

 

---

based on  SQL for Web Nerds

Security Notes

Created by Gustaf Neumann, last modified by Gustaf Neumann 13 Feb 2009, at 09:35 AM

By Richard Li

OpenACS docs are written by the named authors, and may be edited by OpenACS documentation staff.

The security system was designed for security. Thus, decisions requiring trade-offs between ease-of-use and security tend to result in a system that may not be as easy to use but is more secure.

If a user switches to HTTPS after logging into the system via HTTP, the user must obtain a secure token. To insure security, the only way to obtain a secure token in the security system is to authenticate yourself via password over an HTTPS connection. Thus, users may need to log on again to a system when switching from HTTP to HTTPS. Note that logging on to a system via HTTPS gives the user both insecure and secure authentication tokens, so switching from HTTPS to HTTP does not require reauthentication.

This method of authentication is important in order to establish, in as strong a manner as possible, the identity of the owner of the secure token. In order for the security system to offer stronger guarantees of someone who issues a secure token, the method of authentication must be as strong as the method of transmission.

If a developer truly does not want such a level of protection, this system can be disabled via source code modification only. This can be accomplished by commenting out the following lines in the sec_handler procedure defined in security-procs.tcl:

    if { [ad_secure_conn_p] && ![ad_login_page] } {
set s_token_cookie [ns_urldecode [ad_get_cookie "ad_secure_token"]]

if { [empty_string_p $s_token_cookie] || [string compare $s_token_cookie [lindex [sec_get_session_info $session_id] 2]] != 0 } {
# token is incorrect or nonexistent, so we force relogin.
ad_returnredirect "/register/index?return_url=[ns_urlencode [ad_conn url]?[ad_conn query]]"
}
}

The source code must also be edited if the user login pages have been moved out of an OpenACS system. This information is contained by the ad_login_page procedure in security-procs.tcl:

ad_proc -private ad_login_page {} {

Returns 1 if the page is used for logging in, 0 otherwise.

} {

set url [ad_conn url]
if { [string match "*register/*" $url] || [string match "/index*" $url] } {
return 1
}

return 0
}

The set of string match expressions in the procedure above should be extended appropriately for other registration pages. This procedure does not use parameter::get or regular expressions for performance reasons, as it is called by the request processor.

Security Design

Created by Gustaf Neumann, last modified by Gustaf Neumann 13 Feb 2009, at 09:28 AM

By Richard Li and Archit Shah

OpenACS docs are written by the named authors, and may be edited by OpenACS documentation staff.

This document explains security model design for OpenACS 4. The security system with the OpenACS core must authenticate users in both secure and insecure environments. In addition, this subsystem provides sessions on top of the stateless HTTP protocol. This system also provides session level properties as a generic service to the rest of the OpenACS.

The atoms used in the implementation:

  • Cookies: RFC 2109, HTTP State Management Mechanism

    Cookies provide client side state. They are used to identify the user. Expiration of cookies is used to demark the end of a session.

  • SHA: SHA-1

    This secure hash algorithm enables us to digitally sign cookies which guarantee that they have not been tampered with. It is also used to hash passwords.

  • SSL with server authentication: SSL v3

    SSL provides the client with a guarantee that the server is actually the server it is advertised as being. It also provides a secure transport.

A session is defined as a series of clicks in which no two clicks are separated by more than some constant. This constant is the parameter SessionTimeout. Using the expiration time on the signatures of the signed cookies, we can verify when the cookie was issued and determine if two requests are part of the same session. It is important to note that the expiration time set in the cookie protocol is not trusted. Only the time inserted by the signed cookie mechanism is trusted.

Two levels of access can be granted: insecure and secure. This grant lasts for the remainder of the particular session. Secure authentication tokens are only issued over secured connections.

One consequence of this security design is that secure tokens are not automatically issued to users who authenticate themselves over insecure connections. This means that users will need to reauthenticate themselves over SSL when performing some action that requires secure authentication.

Although this makes the site less user friendly, this design significantly increases the security of the system because this insures that the authentication tokens presented to a secure section of the web site were not sniffed. The system is not entirely secure, since the actual authentication password can be sniffed from the system, after which the sniffer can apply for a secure authentication token. However, the basic architecture here lays the foundation for a secure system and can be easily adapted to a more secure authentication system by forcing all logins to occur over HTTPS.

The authentication system issues up to four signed cookies (see below), with each cookie serving a different purpose. These cookies are:

name value max-age secure?
ad_session_id session_id,user_id SessionTimeout no
ad_user_login user_id Infinity no
ad_user_login_secure user_id,random Infinity yes
ad_secure_token session_id,user_id,random SessionLifetime yes
  • ad_session_id

    • reissued on any hit separated by more than SessionRenew seconds from the previous hit that received a cookie

    • is valid only for SessionTimeout seconds

    • is the canonical source for the session ID in ad_conn

  • ad_user_login

    • is used for permanent logins

  • ad_user_login_secure

    • is used for permanent secure logins

    • contains random garbage (ns_time) to prevent attack against the secure hash

  • ad_secure_token

    • is a session-level cookie from the browser's standpoint

    • its signature expires in SessionLifetime seconds

    • contains random garbage (ns_time) to prevent attack against the secure hash

    • user_id is extraneous

The Tcl function (sec_handler) is called by the request processor to authenticate the user. It first checks the ad_session_id cookie. If there is no valid session in progress, a new session is created with sec_setup_session. If the user has permanent login cookies (ad_user_login and ad_user_login_secure), then they are looked at to determine what user the session should be authorized as. Which cookie is examined is determined by whether or not the request is on a secure connection. If neither cookie is present, then a session is created without any authentication. If the ad_session_id cookie is valid, the user_id and session_id are pulled from it and put into ad_conn.

Secure connections are authenticated slightly differently. The function ad_secure_conn_p is used to determine whether or not the URL being accessed is requires a secure login. The function simply checks if the location begins with "https". (This is safe because the location is set during the server initialization.)

If secure authentication is required, the ad_secure_token cookie is checked to make sure its data matches the data stored in ad_session_id. This is true for all pages except those that are part of the login process. On these pages, the user can not yet have received the appropriate ad_secure_token cookie, so no check against it is performed. The set of pages that skip that processing are determined by determined by ad_login_page. Since the ad_secure_token cookie is a session cookie, it is deleted by the browser when the browser exits. Since an attacker could conceivably store the secure cookie in a replay attack (since expiration date is not validated), the data in the secure cookie is never used to set any data in ad_conn; user_id and session_id is set from the ad_session_id cookie.

It is important to note that the integrity of secure authentication rests on the two Tcl function ad_secure_conn_p and ad_login_page. If ad_secure_conn_p is false, secure authentication is not required. If ad_login_page is false, secure authentication is not required.

The Tcl function ad_user_login does two things. First it performs the appropriate manipulation of the permanent login cookies, and then it updates the current session to reflect the new user_id. The manipulation of the permanent login cookies is based on 3 factors:

  • previous login: other user, same user

  • permanent: was a permanent login requested?

  • secure: is this a secure connection?

Both the secure and insecure permanent login cookie can have one of three actions taken on it:

  • set: cookie with no expiration is set

  • delete: set to "" with max age of 0, so it is expired immediately

  • nothing: if the cookie is present, it remains

The current state of the permanent login cookies is not taken into account when determining the appropriate action.

previous login state permanent login requested secure connection action on insecure action on secure
other y y set set
same y y set set
other y n set delete
same y n set nothing
same n y nothing delete
other n y delete delete
other n n delete delete
same n n delete delete

ad_user_login callssec_setup_session which actually calls sec_generate_session_id_cookie to generate the new cookie with refer to the appropriate user_id. If the connection is secure the ad_secure_token cookie is generated by a call to sec_generate_secure_token_cookie. This function is only called from sec_setup_session. Only sec_handler and sec_setup_session call sec_generate_session_id_cookie.

ad_user_logout logs the user out by deleting all 4 cookies that are used by the authentication system.

The creation and setup of sessions is handled in sec_setup_session, which is called either to create a new session from sec_handler or from ad_user_login when there is a change in authorization level. The session management code must do two things: insure that session-level data does not float between users, and update the users table which has columns for n_sessions, last_visit, and second_to_last_visit.

If there is no session already setup on this hit, a new session is created. This happens when sec_setup_session is called from sec_handler. If the login is from a user to another user, a new session is created, otherwise, the current session is continued, simply with a higher authorization state. This allows for data associated with a session to be carried over when a user logs in.

The users table is updated by sec_update_user_session_info which is called when an existing session is assigned a non-zero user_id, or when a session is created with a non-zero user_id.

ad_user_login assumes a password check has already been performed (this will change in the future). The actual check is done by ad_check_password. The database stores a salt and a hash of the password concatenated with the salt. Updating the password (ad_change_password) simply requires getting a new salt (ns_time) concatenating and rehashing. Both the salt and the hashed password field are updated.

A session is labeled by a session_id sequence. Creating a session merely requires incrementing the session_id sequence. We do two things to improve the performance of this process. First, sequence values are precomputed and cached in the Oracle SGA. In addition, sequence values are incremented by 100 with each call to nextval. These sequences values are cached on a per-thread basis. The cost of allocating a new session thus becomes the cost of executing an incr Tcl command per thread. This minimizes lock contention for the session ID sequence and also minimizes the number of DB requests, since each thread can allocate 100 sessions before requiring another DB hit. This cache works by keeping two counters: tcl_max_value and tcl_current_sequence_id. When tcl_current_sequence_id is greater than tcl_max_value a new value is requested from the db and tcl_max_value is incremented by 100. This is done on a per-thread basis so that no locking is required.

In addition, two procedures are dynamically generated at startup in security-init.tcl. These two procedures use parameter::get to obtain the constant value of a given parameter; these values are used to dynamically generate a procedure that returns a constant. This approach avoids (relatively) expensive calls to parameter::get in sec_handler. The impact of this approach is that these parameters cannot be dynamically changed at runtime and require a server restart.

Session properties are stored in a single table that maps session IDs to named session properties and values. This table is periodically purged. For maximum performance, the table is created with nologging turned on and new extents are allocated in 50MB increments to reduce fragmentation. This table is swept periodically by sec_sweep_session which removes sessions whose first hit was more than SessionLifetime seconds (1 week by default) ago. Session properties are removed through that same process with cascading delete.

Session properties can be set as secure. In this case, ad_set_client_property will fail if the connection is not secure. ad_get_client_property will behave as if the property had not been set if the property was not set securely.

Signed cookies are implemented using the generic secure digital signature mechanism. This mechanism guarantees that the user can not tamper with (or construct a value of his choice) without detection. In addition, it provides the optional facility of timing out the signature so it is valid for only a certain period of time. This works by simply including an expiration time as part of the value that is signed.

The signature produced by ad_sign is the Tcl list of token_id,expire_time,hash, where hash = SHA1(value,token_id,expire_time,secret_token). The secret_token is a forty character randomly generated string that is never sent to any user agent. The scheme consists of one table:

create table secret_tokens (
token_id integer
constraint secret_tokens_token_id_pk primary key,
token char(40),
token_timestamp sysdate
);

ad_verify_signature takes a value and a signature and verifies that the signature was generated using that value. It works simply by taking the token_id and expire_time from the signature, and regenerating the hash using the supplied value and the secret_token corresponding to the token_id. This regenerated hash is compared to the hash extracted from the supplied signature. The expire_time is also verified to be greater than the current time. An expire_time of 0 is also allowed, as it indicates no time out on the signature.

Signed cookies include in their RFC2109 VALUE field a Tcl list of the value and the signature. In addition to the expiration of the digital signature, RFC 2109 specifies an optional max age that is returned to the client. For most cookies, this max age matches the expiration date of the cookie's signature. The standard specifies that when the max age is not included, the cookie should be "discarded when the user agent exits." Because we can not trust the client to do this, we must specify a timeout for the signature. The SessionLifetime parameter is used for this purpose, as it represents the maximum possible lifetime of a single session.

RFC 2109 specifies this optional "secure" parameter which mandates that the user-agent use "secure means" to contact the server when transmitting the cookie. If a secure cookie is returned to the client over https, then the cookie will never be transmitted over insecure means.

Performance is a key goal of this implementation of signed cookies. To maximize performance, we will use the following architecture. At the lowest level, we will use the secret_tokens table as the canonical set of secret tokens. This table is necessary for multiple servers to maintain the same set of secret tokens. At server startup, a random subset of these secret tokens will be loaded into an ns_cache called secret_tokens. When a new signed cookie is requested, a random token_id is returned out of the entire set of cached token_ids. In addition, a thread-persistent cache called tcl_secret_tokens is maintained on a per-thread basis.

Thus, the L2 ns_cache functions as a server-wide LRU cache that has a minimum of 100 tokens in it. The cache has a dual purpose:

  • LRU cache Note that cache misses will only occur in the multiple server case, where a user agent may have a signature guaranteed by a secret token issued by another server in the cluster.

  • signature cache Since the cache always maintains a minimum of 100 (set by a parameter) tokens populated at startup, it can be used to provide a random token for signature purposes.

The per-thread cache functions as an L1 cache that indiscriminately caches all secret tokens. Note that this is not an LRU cache because there is no cache eviction policy per se -- the cache is cleared when the thread is destroyed by AOLserver.

Storing information on a client always presents an additional security risk.

Since we are only validating the information and not trying to protect it as a secret, we don't use salt. Cryptographic salt is useful if you are trying to protect information from being read (e.g., hashing passwords).

External SSL mechanisms (firewall, dedicated hardware, etc.) can be used by creating two pools of AOLservers. In one pool the servers should be configured with the location parameter of nssock module set to "https://yourservername". The servers in the other pool are configured as normal. The external SSL agent should direct SSL queries to the pool of secure servers, and it should direct non-SSL queries to the insecure servers.

The pseudorandom number generator depends primarily on ns_rand, but is also seeded with ns_time and the number of page requests served since the server was started. The PRNG takes the SHA1(seed,ns_rand,ns_time,requests,clicks), and saves the first 40 bits as the seed for the next call to the PRNG in a thread-persistent global variable. The remaining 120 bits are rehashed to produce 160 bits of output.

ad_user_login user_id Logs the user in as user user_id. Optional forever flag determines whether or not permanent cookies are issued.

ad_user_logout Logs the user out.

ad_check_password user_idpassword returns 0 or 1.

ad_change_password user_idnew password

ad_sign value Returns the digital signature of this value. Optional parameters allow for the specification of the secret used, the token_id used and the max_age for the signature. ad_verify_signature valuesignature Returns 1 or 0 indicating whether or not the signature matches the value specified. The secret parameter allows for specification of a different secret token to be used.

ad_set_signed_cookie namedata Sets a signed cookie name with value data.

ad_get_signed_cookie name Gets the signed cookie name. It raises an error if the cookie has been tampered with, or if its expiration time has passed.

ad_set_client_property modulenamedata Sets a session property with name to value data for the module module. The optional secure flag specifies the property should only be set if the client is authorized for secure access (ad_secure_conn_p is true). There is also an optional session_id flag to access data from sessions other than the current one.

ad_get_client_property modulenamedata Gets a session property with name to for the module module. The optional secure flag specifies the property should only be retrieved if the client is authorized for secure access (ad_secure_conn_p is true). There is also an optional session_id flag to access data from sessions other than the current one.

SessionTimeout the maximum time in seconds (default 1200) between requests that are part of the same session

SessionRenew the time in seconds (default 300) between reissue of the session cookie. The minimum time that can pass after a session cookie is issued and before it is rejected is (SessionTimeout - SessionRenew). This parameter is used so that only one session_id cookie is set on a single page even if there are multiple images that are being downloaded.

SessionLifetime the maximum possible lifetime of a session in seconds (default 604800 = 7 days)

NumberOfCachedSecretTokens the number of secret tokens to cache. (default 100)

The pseudorandom number generator used in the OpenACS is cryptographically weak, and depends primarily on the randomness of the ns_rand function for its randomness. The implementation of the PRNG could be substantially improved.

Add a password argument. It is non-optimal to make the default behavior to assume that the password was provided.

The secret tokens pool is currently static. Ideally, this pool should be changed on a random but regular basis, and the number of secret_tokens increased as the number of users come to the web site.

Since the security of the entire system depends on the secret tokens pool, access to the secret tokens table should be restricted and accessible via a strict PL/SQL API. This can be done by revoking standard SQL permissions on the table for the AOLserver user and giving those permissions to a PL/SQL package.

Deferring session to creation until the second hit from a browser seems to be a good way of preventing a lot of overhead processing for robots. If we do this, send cookie on first hit to test if cookies are accepted, then actually allocate on second hit. To preserve a record of the first hit of the session, just include any info about that first hit in the probe cookie sent. Look at how usca_p (user session cookie attempted) is used in OpenACS 3.x ecommerce.

Currently there are only session properties. Because sessions have a maximum life, properties have a maximum life. It would be nice to expand the interface to allow for more persistent properties. In the past, there was a sec_browser_properties table that held permanent properties about each unique visitor (for logged in users, these are just user properties). This was unscalable because there was no way to delete these properties, and the table tended to grow to millions of rows. It would be nice to view browser and session properties as two types of client properties, but with different deletion patterns (there are other differences as well, browser properties can be shared between concurrent sessions). The applications should have control over the deletion patterns, but should not be able to ignore the amount of data stored.

It would be nice to keep some info about sessions: first hit, last hit, and URLs visited come to mind. Both logging and API for accessing this info would be nice. WimpyPoint is an application that already wants to use this information to show how long the current presentation has been viewed. The right way may be to put the session_id into the access log and use log analyzers (leaving it in server memory for applications to access). Putting it into the database at all is probably too big a hammer. Certainly putting it into the database on every hit is too big a hammer.

Two trends drive the requirement for removing cookie dependence. WAP browsers that do not have cookies, and publc perceptions of cookies as an invasion of privacy. The rely on the cookies mechanism in HTTP to distinguish one request from the next, and we trust it to force requests from the same client to carry the same cookie headers. The same thing can be accomplished by personalizing the URLs sent back to each browser. If we can store an identifier in the URL and get it back on the next hit, the sessions system would continue to work.

Problems that arise:

  • URL sharing could be dangerous. If I happen to be browsing Amazon while logged in and I email a friend, he could conceivably receive it and follow it before my session has expired, gaining all of the privileges I had.

  • User-entered URLs are harder to handler. If a user is in the middle of a session and then types in the URL of some page, he could be kicked out of his session.

Both of these problems can be mitigated by doing detection of cookie support (see the section on robot detection). To help deal with the first problem, One could also make the restriction that secure sessions are only allowed over cookied HTTP.

This section is not meant to be a comprehensive analysis of the vulnerabilities of the security system. Listed below are possible attack points for the system; these vulnerabilities are currently theoretical in nature. The major cryptographic vulnerability of the system stems from the pseudorandom nature of the random number generators used in the system.

  • Cryptographically weak PRNG see above.

  • Dependence on sample SQL command The list of random token that are placed in the secret tokens cache is randomly chosen by the Oracle sample command. This command may not be entirely random, so predicting the contents of the secret tokens cache may not be as difficult as someone may anticipate.

  • Dependence on ns_rand The actual token that is chosen from the cache to be used is chosen by a call to ns_rand.

  • ad_secure_conn_p As discussed above, the security of the secure sessions authentication system is dependent upon this function.

Control Structure

Created by Anett Szabo, last modified by Gustaf Neumann 13 Feb 2009, at 09:27 AM

Control structures let you say "run this fragment of code if X is true" or "do this a few times" or "do this until something is no longer true". The available control structures in Tcl may be grouped into the following categories:
  • conditional
  • looping (iteration)
  • error-handling
  • miscellaneous (non-local exit)

 

The Fundamental Conditional Command: if

The most basic Tcl control structure is the if command:

if boolean ?then? body1 ?else? ?body2?
Note that the words "then" and "else" are optional, as is the entire else clause. The most basic if statement looks like this:
if {condition} {
body }
In the ArsDigita Community System, we always leave out the "then", but if we include an else or elseif clause, we put in those optional words. Consistency is the hobgoblin of little minds...
if {condition} {
body } elseif {other_condition} {
alternate_body } else { another_body }
Note how the curly braces and keywords are artfully positioned so that the entire if statement is on one line as far as the interpreter is concerned, i.e., all the newlines are grouped within curly braces. An easy way to break your program is to rewrite the above statement as follows:
if {condition} {
body } elseif {other_condition} {
alternate_body } else { another_body }
The Tcl interpreter will think that the if statement has ended after the first body and will next try to evaluate "elseif" as a procedure.

Let's look at an example from http://software.arsdigita.com/www/register/user-login.tcl. At this point in the ArsDigita Community System flow, a user has already typed his or her email address.

# Get the user ID
set selection [ns_db 0or1row $db "select user_id, user_state, converted_p from users \
where upper(email)=upper('$QQemail')"]

if {$selection == ""} {
# Oracle didn't find a row; this email addres is not in the database
# redirect this person to the new user registration page
ns_returnredirect "user-new.tcl?[export_url_vars return_url email]"
return
}
The same page provides an example both of nested if and if then else:
if {[parameter::get -parameter AllowPersistentLoginP -default 1]} {
# publisher has elected to provide an option to issue
# a persistent cookie with user_id and crypted password
if {[parameter::get -parameter PersistentLoginDefaultP -default 1]} {
# persistent cookie shoudl be the default
set checked_option "CHECKED"
} else {
set checked_option ""
}
ns_write "<input type=checkbox name=persistent_cookie_p value=t $checked_option>
Remember this address and password?
(<a href=\"explain-persistent-cookies.adp\">help</a>)"
}
Notice that the conventional programming style in Tcl is to call if for effect rather than value. It would work just as well to write the inner if in a more Lisp-y style:
set checked_option [if {[parameter::get ...]} {
subst "CHECKED"
} else {
subst ""
}]
This works because if returns the value of the last expression evaluated. However, being correct and being comprehensible to the community of Tcl programmers are different things. It is best to write code adhering to indentation and other stylistic conventions. You don't want to be the only person in the world capable of maintaining a service that has to be up 24x7.

 

Another Conditional Command: switch

The switch dispatches on the value of its first argument: particular variable as follows:

switch flags value {
pattern1 body1 pattern2 body2 ... }
If http://software.arsdigita.com/www/register/user-login.tcl finds a user in the database, it uses a switch on the user's state to decide what to do next:
switch $user_state {
"authorized" { # just move on }
"banned" {
ns_returnredirect "banned-user.tcl?user_id=$user_id"
return
}
"deleted" {
ns_returnredirect "deleted-user.tcl?user_id=$user_id"
return
}
"need_email_verification_and_admin_approv" {
ns_returnredirect "awaiting-email-verification.tcl?user_id=$user_id"
return
}
"need_admin_approv" {
ns_returnredirect "awaiting-approval.tcl?user_id=$user_id"
return
}
"need_email_verification" {
ns_returnredirect "awaiting-email-verification.tcl?user_id=$user_id"
return
}
"rejected" {
ns_returnredirect "awaiting-approval.tcl?user_id=$user_id"
return
}
default {
ns_log Warning "Problem with registration state machine on user-login.tcl"
ad_return_error "Problem with login" "There was a problem authenticating the account: $user_id. Most likely, the database contains users with no user_state."
return
}
}
In this case, we're using the standard switch behavior of matching strings exactly. We're also provide a "default" keyword at the end that indicates some code to run if nothing else matched.

It is possible to use more sophisticated patterns in switch. Here's a fragment that sends different email depending on the pattern of the address:

switch -glob $email {
{*mit.edu} { ns_sendmail $email $from $subject $body }
{*cmu.edu} { ns_sendmail $email $from $subject "$body\n\nP.S. Consider applying to MIT. Boston is much nicer than Pittsburgh"}
{*harvard.edu} { ns_sendmail $email $from $subject "$body\n\nP.S. Please ask your parents to invest in our tech startup."}
}

The third behavior for switch is invoked using the "-regexp" flag. See the pattern matching chapter for more on how these patterns work.

More: http://www.tcl.tk/man/tcl8.4/TclCmd/switch.htm

 

Looping commands while, foreach, and for

The while command in Tcl operates as follows:

while { conditional_statement } {
loop_body_statements }
The conditional statement is evaluated; if it is true, the loop body statement is executed, and then the conditional statement is reevaluated and the process repeats. If the conditional statement is ever false, the interpreter does not execute the loop body statements, and continues to the next line after the conditional.

Here is a while statement used to display the last name, first name of each MIT nerd using a Web service. The conditional is the result of calling AOLserver's ns_db getrow API procedure. This procedure returns 1 if it can fetch the next row from the SQL cursor, 0 if there aren't any more rows to fetch.

set selection [ns_db select $db "select first_names, last_name from users \
where lower(email) like '%mit.edu'"]

while { [ns_db getrow $db $selection] } {
# set local variables to the SQL column names
set_variables_after_query
ns_write "<LI>$last_name, $first_names"
}

More: http://www.tcl.tk/man/tcl8.4/TclCmd/while.htm

The Tcl foreach command loops through the elements of a list, setting a loop variable to each element in term:

foreach variable_name list {
body }
Here's an example from http://software.arsdigita.com/www/monitor.tcl, a page that displays current server activity:
# ask AOLserver to return a list of lists, one for each current connection
set connections [ns_server active]

foreach connection $connections {
# separate the sublist elements with "

" tags
ns_write $conn "



[join $connection "

"]"
}

The program http://sofware.arsdigita.com/www/admin/static/link-check.tcl checks every HTML file in an ArsDigita Community System for dead links. Here's a helper procedure that works on one file:
proc check_file {f} {
# alert the administrator that we're working on this file
ns_write "<li>$f\n<ul>\n"
# read the contents into $content
set stream [open $f]
set content [read $stream]
close $stream
# loop through each reference, relying on API call ns_hrefs
# to parse the HTML and tell us where this file points
foreach url [ns_hrefs $content] { # do all the hard work ... } ns_write "</ul>\n" }

Notice how easy this procedure was to write thanks to the AOLserver developers thoughtfully providing us with ns_hrefs, which takes an HTML string and returns a list of every HREF target.

More: http://www.tcl.tk/man/tcl8.4/TclCmd/foreach.htm

The last looping command, for, is good for traditional "for i from 1 to 10" kind of iteration. Here's the syntax:

for start test next body
We use this control structure in the winner picking admin page of the ArsDigita Comunity System's contest module: http://software.arsdigita.com/www/admin/contest/pick-winners.tcl. The input to this page specifies a time period, a contest, and how many winners are to be picked. Here the result of executing the for loop is a list of N elements, where N is the number of desired winners:
for {set i 1} {$i <= $n_winners} {incr i} {
# we'll have winner_numbers between 0 and $n_contestants - 1
# because randomRange returns a random integer between 0
# and its argument
lappend winner_numbers [randomRange $n_contestants]
}


More: http://www.tcl.tk/man/tcl8.4/TclCmd/for.htm


Error-handling command: catch

If a Tcl command throws an error in a CGI script or an AOLserver API page, by default the user will be presented with an error page. If you don't want that to happen, fix your bugs! Sometimes it isn't possible to fix your bugs. For example, the ns_httpget API procedure fetches a Web page from the wider Internet. Under certain network-dependent conditions, it may throw an error. If you don't want your users to be exposed to that as an error, put in a catch:

catch script ?variable_name?
catch returns 1 if script threw an error, 0 otherwise. If you supply the second argument (variable_name), catch will set that variable to the result of executing script, whether or not the script threw an error.

Our classic example always involves ns_httpget. Here's one from http://www.webho.com/WealthClock:

# define a procedure that computes the entire page
proc wealth_ReturnWholePage {} {
# do a couple of ns_httpgets and some arithmetic
# to produce the user-visible HTML
...
}

# this is the procedure registered to http://www.webho.com/WealthClock
proc wealth_Top {ignore} {
if [catch {set moby_string [Memoize wealth_ReturnWholePage]} errmsg] {
# something went wrong with our sources
... return an apology message to the users
} else {
# we computed the result (or Memoize got it from the cache)
ns_return 200 text/html $moby_string
}
}

Sending email is another time that a Web server has to go outside its carefully controlled world and might experience an error. Here is the entire http://software.arsdigita.com/tcl/ad-monitor.tcl, which implements a central facility for other sections in the ArsDigita Community System. The idea is that programmers can put in "email the administrator if broken" instructions on pages that won't result in a nightmare for the administrator if the page is getting hit every few seconds.

# the overall goal here is that the ad_host_administrator gets
# notified if something is horribly wrong, but not more than once
# every 15 minutes

# we store the last [ns_time] (seconds since 1970) notification time
# in ad_host_administrator_last_notified

ns_share -init { set ad_host_administrator_last_notified 0 } ad_host_administrator_last_notified

proc ad_notify_host_administrator {subject body {log_p 0}} {
ns_share ad_host_administrator_last_notified
if $log_p {
# usually the error will be in the error log anyway
ns_log Notice "ad_notify_host_administrator: $subject\n\n$body\n\n"
}
if { [ns_time] > [expr $ad_host_administrator_last_notified + 900] } {
# more than 15 minutes have elapsed since last note
set ad_notify_host_administrator [ns_time]
if [catch { ns_sendmail [ad_host_administrator] [ad_system_owner] $subject $body } errmsg] {
ns_log Error "failed sending email note to [ad_host_administrator]"
}
}
}

Make sure that you don't overuse catch. The last thing that you want is a page failing silently. Genuine errors should always be brought to a user's attention and ideally to the site administrator's. Users should not think that a server has done something on their behalf when in fact the task was not accomplished.

More: http://www.tcl.tk/man/tcl8.4/TclCmd/catch.htm

 

Miscellaneous commands: break, continue, return, and error

When inside a looping command, it is sometimes desirable to get the command to stop looping or to stop executing the current iteration but to continue on the next one. The break command is used to permanently escape the loop; the continue command is used to escape the current iteration of the loop but to start again at the next iteration. The syntax for each consists only of the appropriate word written on a line by itself within a loop.

We often use the break command when we want to limit the number of rows to display from the database. Here's an example from the photo.net neighbor-to-neighbor system. By default, we only want to show a "reasonable" number of postings on one page:

set selection [ns_db select $db ... big SQL query ... ]

set list_items ""
# see what the publisher thinks is a reasonable number (default to 100)
set n_reasonable [parameter::get -parameter NReasonablePostings -default 100]

# initialize a counter of the number of rows displayed so far
set counter 0
while {[ns_db getrow $db $selection]} {
set_variables_after_query
incr counter
if { $counter > $n_reasonable) } {
# append ellipses
append list_items "<p>\n..."
# flush the database cursor (tell Oracle that we don't need the
# rest of the rows)
ns_db flush $db
# break out of the loop
break } append list_items "<li><a href=\"view-one.tcl ..." }


More: http://www.tcl.tk/man/tcl8.4/TclCmd/break.htm

The return command has been shown before. It quits the proc it's in and returns the supplied value. Remember that any procedure lines after return aren't executed. Too many times we've seen code of the following form:

proc a_new_programmers_proc {} {
set db [ns_db gethandle]
# do a bunch of stuff with the database
return $result
# release the database handle
ns_db releasehandle $db
}
The most interesting thing that you can do with return is write procedures that force their callers to return as well. Here's an example from http://software.arsdigita.com/tcl/ad-security.tcl:
proc ad_maybe_redirect_for_registration {} {
if { [ad_verify_and_get_user_id] != 0 } {
# user is in fact logged in, return happiness
return
} else {
ns_returnredirect "/register/index.tcl?return_url=[ns_urlencode [ns_conn url]$url_args]"
# blow out of 2 levels
return -code return
}
}
A .tcl page can simply call this in-line
ad_maybe_redirect_for_registration 

# the code below will never get executed if the user isn't registered
# ... update the database or whatever ...



More: http://www.tcl.tk/man/tcl8.4/TclCmd/return.htm

The error command returns from a proc and and raises an error that, if not caught by a catch statement, will result in the user seeing a server error page. The first argument to error is displayed in the debugging backtrace:

proc divide {x y} {
if {$y == 0} {
error "Can't divide by zero."
} else {
return [expr {$x / $y}]
}
}

 

More: http://www.tcl.tk/man/tcl8.4/TclCmd/error.htm

---

based on Tcl for Web Nerds 

Package Manager Requirements

Created by Gustaf Neumann, last modified by Gustaf Neumann 13 Feb 2009, at 09:20 AM

By Bryan Quinn and Todd Nightingale

OpenACS docs are written by the named authors, and may be edited by OpenACS documentation staff.

The following is a requirements document for the OpenACS Package Manager (APM), version 4.0 (APM4). APM4 offers a superset of APM v3.3 functionality with the following specific enhancements:

  • A public procedural API. (v 3.3 only has web-based UI)

  • Support for dependency checking.

  • Support for compound packages (to support installation chaining).

  • Support for on-line parameter setting.

  • Support for sub-site level configuration (requires revised parameter and /admin pages at sub-site level; deprecation of site-wide parameter file).

To differentiate these new requirements from the requirements of version 3.3, all requirements new in v4 are prefaced with the number 4.

We gratefully acknowledge the authors of APM 3 for their original design documentation which suggested these features, as well as the influence of the design and open-source implementation of the Red Hat Package manager, the Debian packaging system, and PERL's CPAN in the development of the ideas behind this document.

A typical website will tend to offer its users a number of web-based services or applications, e.g. a bulletin board, calendaring, classified ads, etc. A website may also have underlying subsystems, such as a permissions system, content management system, etc. For such applications and subsystem components, modularity - or the degree to which a component can be encapsulated and decoupled from the rest of the system - is of great value. Thus the OpenACS Package Manager (APM) was created to allow website components, or packages, to be added, removed, and upgraded easily, with minimum disturbance to the rest of the system. This allows site owners to steadily offer users new and improved services, and also allows programmers to quickly and easily distribute their OpenACS components in a standardized manner to other OpenACS sites.

In general terms, a package is a unit of software that serves a single well-defined purpose. The OpenACS Package Manager (APM) provides a mechanism for packaging, installing, and configuring OpenACS software in a consistent, user-friendly, and subsite-aware manner.

The OpenACS Package Manager (APM) consists of:

  • A standard format for APM packages including:

    • Version numbering, independent of any other package and the OpenACS as a whole

    • Specification of the package interface

    • Specification of dependencies on other packages (if any)

    • Attribution (who wrote it) and ownership (who maintains it)

  • Web-based tools for package management:

    • Obtaining packages from a remote distribution point

    • Installing packages, if and only if:

      1. All prerequisite packages are installed

      2. No conflicts will be created by the installation

    • Configuring packages (obsoleting the monolithic OpenACS configuration file)

    • Upgrading packages, without clobbering local modifications

    • Uninstalling unwanted packages

  • A registry of installed packages, database-backed and integrated with file system-based version control

  • Web-based tools for package development:

    • Creating new packages locally

    • Releasing new versions of locally-created packages

    • Uploading packages to a global package repository on the web

    • Use of these tools should be safe, i.e. installing or removing a package should never break an OpenACS installation

  • Web-based tools for package configuration:

    • The ability to change package parameter values on-line through a simple web interface.

    • A new ad_parameter which does not require a monolithic site-wide parameter's file or server restarts for changes to take effect.

    • The ability to manage multiple package instances at the sub-site level.

The APM is intended for the following classes of users, which may or may not overlap:

  1. Developers (referred to as 'the developer') use the APM to create a software package for distribution and use the procedural API for direct control of the APM system.

  2. Site-wide administrators (referred to as 'the administrator') use the APM to install packages for their OpenACS instance, and optionally make them available to sub-sites.

  3. Sub-site administrators (referred to as 'the sub-admin') use an administration interface to configure and enable packages for their sub-site.

Initial Package Development

David Developer writes a piece of software used to do knowledge management (km) for the OpenACS. He distributes his data model, procedure code, UI pages, and his documentation according to the APM specification. He splits the documentation and the code into sub-packages, and creates a KM installation-chain to install both with the APM developer UI. Noting that his software was built with Patricia Programmer's Super Widget toolkit, he specifies that as a dependency. Moreover, since this package is capable of being used at the sub-site level, David configures this option in the package. When the package development is complete, David uses the APM developer UI to construct a distribution file. He assigns it a version number, 1.0, and makes the package available for download at the OpenACS package repository.

Initial Package Installation

Annie Admin learns of David's KM system by browsing the OpenACS package repository. Annie Admin uses the APM administrator UI on her system. She selects to install a package from a URL and types the URL displayed on the system. The APM automatically downloads the package. The dependency checker notices that Patricia's Super Widget toolkit is required, so it warns Annie of this. Annie selects an option to find a package that satisfies the dependency at the OpenACS APM repository. The APM informs Annie that it can download and install Jim's Super Widget toolkit. Annie confirms this option. After successfully installing Jim's toolkit, Annie proceeds to install David's KM system. The data model is loaded and all of the files necessary for the software are installed. Because installation was successful, the package is available for use.

Since the package is available for use, its initialization routines are set to run automatically on server startup. Annie is warned that since there are initialization routines, she must restart the server for the package to be ready for use. Annie restarts the server.

Initial Subsite Use of Package

Annie Admin decides to make the KM module available only to a particular sub-site type on her OpenACS system, and not others. She specifies this option using the Sub-site type UI (not part of APM).

Annie Admin notifies Sally SubAdmin by e-mail that a new package is now available for use. Sally goes to her sub-site /admin page and sees that a new entry, KM, is available. Sally clicks on it and finds links to the installed KM documentation and to the web based configuration utility. Then, Sally configures the package using an automatically generated web interface and enables KM for use on her sub-site. After some initial use of the package, Sally decides to change some parameters using the SubAdmin UI. These changes take effect immediately, without any server restarts.

Upgrade Process

Sally SubAdmin finds a bug in the KM system and sends a report to David Developer. David reads the bug report and verifies that the bugs are present in the current version. Because the bugs are present in the shared procedure file, David assigns a watch to the file. David makes the necessary modifications to the source code and saves the file. Because a watch was assigned to the file, the APM automatically reloads the updated code. David tests the program and confirms that the bug is fixed. He increments the minor version number and makes km v 1.1 available for download at the repository.

Sally SubAdmin asks Annie Administrator to upgrade the package using the APM UI. This upgrade supersedes the old version of KM at the site-wide level. Once Annie upgrades the package, the new version starts working immediately in Sally's sub-site.

Procedural API

Danielle Developer wants her software to perform different actions depending on what version of another package is installed. She uses the APM procedural API to check if KM version 1.0 is installed or version 1.1. Based on the results of this procedural call, the software exhibits different behavior.

  • 4.500.0 Package Identification (All of these items are entered by the developer using the developer UI.)

    4.500.1 A human readable package key that is guaranteed to be unique to the local OpenACS site must be maintained by the APM. For example, "apm."

    4.500.5 A package id (primary key) that is guaranteed to be unique to the local site must be maintained by the APM. For example, "25."

    4.500.10 A package URL that is guaranteed to be unique across all sites must be maintained by the APM. The package URL should point to a server that allows download of the latest version of the package. For example, "http://openacs.org/software."

  • 4.505.0 Version Identification (All of these items are entered by the developer using the developer UI.)

    4.505.1 A version id (primary key) that is guaranteed to be unique to the local site must be maintained by the APM.

    4.505.5 A version URL that is guaranteed to be unique across all sites must be maintained by the APM. The version URL should point to a server that allows download of a specific version of the package.

The API for APM v3 is explicitly a private API. However, it would be useful to obtain information from the APM through a procedural API. Implementing the API specified below is quite easy given that there are pages that already do all of the below in raw SQL.

  • 4.400.0 Packages Status Predicates

    4.400.1 Given defining information such as a package URL, the APM API can return the status of the package on the local OpenACS instance.

  • 4.405.0 Package Information Procedures

    4.405.1 The APM API can return information for any locally installed packages, including the version number, paths and files, and package key.

  • 4.410.0 Sub-site Procedures

    4.410.1 After a package has been installed at the site-wide level, the system API will provide means to check for package presence, creation, enabling, disabling, and destruction on a subsite.

  • 4.415.0 Parameter Values (replaces ad_parameter)

    4.415.1 The system API shall allow subsite parameters for an installed package to be set by either site-wide administrators or sub-site admins. The subsite parameter can be set to be non-persistent (but default is to survive server restarts). The subsite parameter can also be set to only take effect after a server restart (default is immediate).

    4.415.5 Parameters for a given subsite and package can be returned by the system API.

Provisions will be made to assure that packages are securely identified.

  • 4.600.1 Each package will have a PGP signature and there will be MD5 time stamps for each file within the package.

  • 4.600.5 The APM will provide a facility to validate both the PGP signature and MD5 stamps information before a package install.

The user interface is a set of HTML pages that are used to drive the underlying API. It is restricted to site-wide administrators because the actions taken here can dramatically affect the state of the running OpenACS.

The intent of the developer's interface is to enable the developer to construct and maintain APM packages. It will be possible to disable the developer's interface for production sites to help reduce the chance of site failure; much of the functionality here can have cascading effects throughout the OpenACS and should not be used on a production site.

  • 10.0 Define a package.

    The developer must be able to create a new package by specifying some identifying information for the package. This includes a package name, a package key, version information, owner information, and a canonical URL.

    10.1 The APM must maintain the state of all locally generated packages.

    10.50 If the developer fails to provide the required information, the package cannot be created.

    10.55 All of the package information should be editable after creation, except for the package key.

    4.10.60 The package creator must specify whether the package is capable of being used in sub-sites, or if only a single, global instance of the package is permitted.

    4.10.65 If the developer fails to provide unique information for unique fields specified in the data model requirements, the package cannot be created.

  • 20.0 Add files to a package

    20.1 The developer must be able to add files to the package. This is done by copying the files into the package directory in the host OS's file system. Files can be added at any point after package creation.

    20.3 Once a package has been versioned and distributed, no new files should be added to the package without incrementing the version number.

    20.5 The APM's UI should facilitate the process of adding new files, by scanning the file system for new files automatically, and allowing the developer to confirm adding them.

    20.10 The developer cannot add files to a given package via the UI that do not exist in the file system already.

    20.15 Package file structure must follow a specified convention. Please see the design document for what we do currently.

  • 30.0 Remove files from a package

    The developer must be able to remove files from a package. This can be done in two ways.

    • 30.1 Access the APM UI, browse the file list, and remove files.

      30.1.1If a file is removed from the package list, but not from the file system, an error should be generated at package load time.

    • 30.5 Remove the file from file system.

      30.5.1 The APM UI should take note of the fact that the file is gone and offer the developer an option to confirm the file's deletion.

  • 40.0 Modify files in a package.

    40.1 The developer should be able to modify files in the file system. The APM UI should not interfere with this.

    40.5 However, if the developer modifies files containing procedural definitions, APM UI should allow a means to watch those files and automatically reload them if changed. See requirement 50.0 for more detail.

    40.10 Also, although a change in files implies that the package distribution file is out of date, it is the developer's responsibility to update it.

  • 4.45.0 Manage Package Dependency Information.

    4.45.1 The developer should be able to specify which interfaces the package requires.

    4.45.5 The developer should be able to specify which interfaces the package provides.

    4.45.10 Circular dependencies are not allowed.

  • 50.0 Watch a file

    4.50.1 The developer should be able to assign a watch to any Tcl procedure file, whether in /packages or /tcl.

    50.5 If a watched file is locally modified, then it will be automatically reloaded, thus allowing for any changes made to take affect immediately.

    4.50.10 The setting of a watch should be persistent across server restarts.

  • 60.0 Display an XML package specification

    60.1 The developer should be able to view the XML package specification that encodes all package information.

  • 70.0 Write an XML package specification to the file system

    70.1 The developer should be able to write an up-to-date XML specification to disk.

    70.5 The developer should be able to request the current XML specification for all installed, locally generated packages.

  • 130.0 Distribution file generation

    130.1 The developer should be able to generate a .APM distribution file for the package with just one click.

    130.5 Generating a distribution file implies doing an "up-to-date" check on all of the files. If any of the files have changed since package installation, then a new version of the package is created.

  • 140.0 Access CVS information

    140.1 The developer should be able to determine the CVS status of a package, or all packages, with a single click.

  • 4.400.0 Compound Package Construction

    4.400.1 The developer can include .APM packages (sub-packages) within a package (the compound package) like any other file.

    4.400.5 The recommended usage for this feature is to allow for separation of optional and required components from the installation as well as better organization of files once installed. For example, all documentation for the community-core can be packages as community-core-doc.apm. It is legal to include sub-packages with dependencies that are not satisfied by the packages in the compound package, but this is discouraged. In such a case, the sub-package should really be a separate package that is required by the compound package.

    4.400.10 If a sub-package is required for the installation of the compound package, the compound package should have a registered dependency on the sub-package.

The requirement of the administrator's interface is to enable the administrator to install, enable, upgrade, disable, deinstall, and delete packages.

  • 80.0 Package Enable/Disable

    4.80.1 The administrator should be able mark an installed package as enabled. This means that the package is activated and its functionality is delivered through the Request Processor. As of OpenACS 4, this is done through the sub-site system.

    4.80.5 Moreover, the administrator must be able to disable a package, thereby removing the functionality provided to a sub-site. As of OpenACS 4, this is done through the sub-site system.

  • 90.0 Package Install

    90.1 The administrator must be able to install new packages either from locally maintained .APM files or from URLs.

    90.5 In the case of an URL, the APM transparently downloads the APM file off the web, proceeds with a file based installation, and then optionally removes the .APM file just downloaded.

    90.10.1 If .APM files are present in a package, then it is considered a compound package (use 4.410.0).

    90.15.0 Installation requires these steps:

    1. 90.15.1The package dependencies are scanned. If some dependencies are not present, the system warns the administrator that installation cannot proceed until those packages are installed.

    2. 90.15.2 Assuming all dependencies are present, APM extracts the contents of the APM file into the /packages directory.

    3. 90.15.3 The administrator is offered the option of importing directly into CVS.

    4. 90.15.4 The administrator is given a list of data model scripts found in the package and can select which ones to be executed.

    5. 90.15.5 If no errors are recorded during this process, the package is enabled.

  • 4.410.0 Compound package Install

    4.410.1 If .APM files are present in a package, then it is considered a compound package.

    4.410.5.0 Installation of a compound package proceeds according to the following sequence:

    1. 4.410.5.1 Identify the set of all sub-packages within the compound package by scanning for all files with .APM.

    2. 4.410.5.2 Identify which sub-packages are required by checking the dependencies of the compound package. If there dependencies not satisfied by the current system or the packages included with the compound package, halt installation and inform user to install these packages first.

    3. 4.410.5.3 Present Administrator with the ability to choose which sub-packages to install. Required sub-packages must be installed.

    4. 4.410.5.4 Proceed with the installation of each sub-package, starting with required packages. If the sub-package is already installed, then do nothing. Else, If the sub-package is a normal package, proceed according to 90.15.0, otherwise if it is a compound package, proceed according to 4.410.5.0.

    5. 4.410.5.5 If all required sub-packages are installed, proceed to install non-required sub-packages. If there was a failure during the installation of a required sub-package, then the installation of the compound package is also a failure.

    6. 4.410.5.6 Any attempt to install a compound package in the future involves a choice presented to the admin of installing any uninstalled sub-packages.

  • 4.420.0 Recovering from failed package installation

    4.420.1 If any error is generated during package installation, the package is not considered installed. To recover from this failure, the package should be selected for installation again.

  • 100.0 Version Upgrade

    100.1 The administrator can upgrade to a new version of a package. This entails

    1. 100.1.1 Running any necessary and included upgrade scripts.

    2. 100.1.5 Replacing any old files with new versions.

    3. 100.1.10 Marking the old version of the package as 'superseded' and disabling it.

    4. 100.1.15 Assuming no errors from above, the new package is enabled.

  • 110.0 Package Deinstall

    110.1 The administrator must be able to deinstall a package that has already been installed. Deinstallation entails:

    1. 110.1.1 Running any data model scripts necessary to drop the package.

    2. 110.1.5 Moving all of the files into a separate location in the file system from the installed packages.

    3. 4.110.1.10 If the package is a compound package, then the administrator must confirm removing all sub-packages. Optionally, some sub-packages can be kept.

    110.5 Deinstalled packages can be re-installed at a later date.

    4.110.10 If deinstalling a package or any of its sub-packages breaks a dependency, then deinstallation cannot proceed until the package registering the dependency is removed.

  • 120.0 Package Deletion

    120.1 The administrator should be able to completely erase all records of the package. This involves removing all instances of the package, all related database tables and content.

    120.5 This option can only be used if all package instances are deleted or marked as disabled. This is purposefully cumbersome because deleting all instances of a package can have far-sweeping consequences throughout a site and should almost never be done.

  • 150.0 Scan for new or modified packages

    150.1 The administrator should be able to scan the file system for any changes made in any of the installed package files.

    150.5 The administrator should be able to scan the file system for any newly installed packages.

If the developer is in charge of creating packages and the administrator for installing them, then the sub-site administrator is responsible for configuring and enabling packages. In order for a package to be available for a sub-site it must be associated with the sub-site's type specification. This interface is part of the sub-site /admin interface.

  • 4.300 Creating a package instance.

    4.300.1 From the sub-site /admin interface, there should be an option to view all packages available in the system as well as an option to add a package to the subsite.

    4.300.5 From the "add" option, the sub-admin can select from a list of packages registered as available in the sub-site type to which the sub-site belongs.

    4.300.19 Once a package instance is added, it is available on the list of the subsite's available packages.

  • 4.305 Configuring a package instance.

    4.305.1 An automatic web interface that lists all parameters with current values must be available.

    4.305.5 Changing the values for the parameters is accomplished simply by submitting an HTML form.

  • 4.310 Enabling a package instance.

    4.310.1 The sub-admin should be able to enable a package with a single click. Enabling a package means that the OpenACS will serve its URLs properly.

  • 4.315 Disabling a package instance.

    4.315.1 The sub-admin should be able to disable a package with a single click. Disabling a package means that the OpenACS will no longer serve those URLs.

  • 4.320 Deleting a package instance.

    4.320.1 Deleting a package instance involves deleting not only the package instance, but any and all content associated with it. It is questionable whether this option should even be available due to its drastic consequences. Reviewer comments appreciated.

Despite the fact that requirements are meant to be design/implementation neutral, the following thoughts were in our head when specifying these requirements. You must be familiar with the new object design for this to be comprehensible.

When a package is installed system-wide, a corresponding acs_object_type is created for it. All parameters registered for the package are registered for that acs_object_type.

When a package instance is created, it is an acs_object. Its parameters are set using the acs_attribute_values table. The automatic web interface for setting package parameters should be one and the same with the interface for setting acs object attribute values. Consequently, the implementation of these features should be quite straightforward.

Document Revision # Action Taken, Notes When? By Whom?
0.1 Creation 8/10/2000 Bryan Quinn, Todd Nightingale

Reviewed 8/11/2000 John Prevost, Mark Thomas, and Pete Su
0.2 Revised and updated 8/12/2000 Bryan Quinn
0.3 Reviewed, revised, and updated - conforms to requirements template. 8/18/2000 Kai Wu
0.4 Minor edits before ACS 4 Beta. 9/30/2000 Kai Wu