OSSWEB - Framework for developers
TABLE OF CONTENTS
DESCRIPTION
History
Why Naviserver?
Concepts
Components
Security Model
Page Templates
Request Routing
Database Access
SQL Query Abstraction
Installing OSSWEB
PostgresQL Installation
OSSWEB Installation
Development Mode
Tutorial: First Application
Database Model
Albums List
Creating Albums
Updating Albums
Uploading Photos
OSSWEB API
Namespace Hierarchy
Core Procs
Cache Procs
Type Validation Procs
Conversion Procs
Connection Procs
Database Procs
SQL Support Procs
Forms and Widget Procs
HTML Procs
Template Procs
Custom Tags
File Storage Procs
Utility Procs
Scheduling Procs
Admin Procs
Lookup Procs
COPYRIGHT
Web development framework based on Tcl language and Naviserver application server.
Why another Web application framework, how many exist already, can't you just pick one and use it? This is a good question and as i see how many frameworks exist today, not an easy one. Back at 2000 when i was building high performance http proxy, i needed small scripting language to embed into my server. At that time i liked Python and did all scripting tasks using it but the server i was working on had to be multi-threaded and Python did not support threads very well. It has one master lock which allows only one thread to execute Python code, so multiple interpreters in one address space will be serialize one after another which gives not very good performance if you need to serve hundreds or thousands requests. Perl i never liked, PHP was for Apache only, the only 2 languages i found were Lua and Tcl. For some reason i do not remember now i chose Tcl. It was very easy to embed, C API is very well documented and simple. So in no time my proxy server was able to call small scripts in multiple threads and it gave a lot of additional functionality because some things were much easier to cod ein scripting language than in C.
Then another big project came in and i had to choose Web application platform but the trick was, it was supposed to be really big system, and Web was only part of it. The system had to support job scheduling, different communication protocols, background tasks, different databases and other non-Web related stuff and at the same time it should be accessible and configured only through Web interface. I forgot to mention that open-source tools were preferable and we were not Microsoft shop, but we had some software running on Windows we needed to connect to. Having experience with Apache, PHP and Java i was still looking when i found at that time active web site of the company called ArsDigita. The forums on that site were so interesting and useful so i digged up deeper and found execelent book by Philip Greenspun, Philip and Alex's Guide to Web Publishing available at http://philip.greenspun.com/panda/. I used Tcl by that time, so i was basically hooked. I installed AOLserver and started playing with it to see if this is what i need for my new project. It turned out that combination of AOLserver and some parts of ArsDigita open-source software called ACS were almost perfectly fit but what was more important, the platform was giving almost unlimited posibilities as in Web space and also in other backend or server architecture related domains. The AOLserver's API, Tcl and C was so powerfull and rich, extending it was so easy, after almost 2 years of development and maintaining the whole system was built just using AOLServer, Tcl. C was used only for specific modules like SNMP, ICMP protocols, database driver and other low-level stuff. During the development, the system grew from pieces of former ACS system into completely different Web application framework with its own security model, API and data model. By the time ArsDigita disappeared we no longer actually needed it because we were completely independent from any outside companies, all tools we used were open-sourced.
We were using AOLserver since 2001, started with version 3.3. After several years i collected a bunch of patches and modules that i was trying to keep up-to-date with new versions of AOLserver. I tried to submit my patches, some have been accepted, some not but after some time it was obvious that AOLserver is intended to be Web server only while i wanted it to be more versatile application platform where Web is just one of the features. I wanted the server to supported different protocols, not just HTTP, more flexible API, more modules and functions. I am not saying i was right and others were wrong, but i saw the potential of the server to be much bigger than just a Web server and not embracing it was very frustrating for me. So, Naviserver was born as a fork of AOLserver version 4.0.10. Since then a lot of stuff have been added and changed in Naviserver that those two servers cannot be simply switched, Naviserver has much larger API set which is not available in AOLserver.
So, enough history and explanations, let's get closer to the OSSWEB/Naviserver framework and how they are bound to each other.
Let's start from the lowest level up to the top. The programming language is Tcl. Arguably it is not the best language but it is very old and still being actively developed, it is very stable, supports threads natively, has good library of useful applications and modules. And it is so simple it is nothing to learn actually, there is no syntax, but the specific syntax can be built using Tcl and we are using this feature. Created as a glue language it actually grew up into general purpose programming language. It is not popular as Java, Perl, Python or Ruby but still has large community and used in many applications. On other hand, popularity never bothered me much, if i like the language, it does the job i do not care how many people using it and i am not going to switch just because it does not make headlines anymore. So, next level is Naviserver, Tcl is embedded into it very deeply, the language is part of the core, that's why Naviserver is very high performance server even with scripting language, there is no overhead, Tcl interpreters are used when they are needed, then they are put back into the pools of availabld interpreters, so other threads can re-use existing interpreter at any time. Also, Tcl in Naviserver is the developement language, more than 200 Tcl functions are available for a developer ranging from accessing internal server state to inter-thread communications and Web page filters.
The biggest difference between Naviserver and other Web server like Apache or lighttpd or other pure Web server that it is capable of not just serving Web page but provide complete server infrastructure that can be used to build more complex server applications. For example, if some actions needs to be run every once in a while, there is internal scheduler than can run Tcl scripts the same way as cron does. Every script can access database using the same API, all additional library functions are available to every thread the same way as system calls from glibc are avaialbel to all user programs. Socket callbacks and client functions, access to file system and database, local cache with expiration for any kind of data, shared variables with automatic locking and more can be used to develop backend applications in addition to Web applications.
So, let's start with describing how Naviserver processes HTTP request. There are many ways to build request processors, i will describe how OSSWEB framework does it. Naviserver is getting much better documentation now, so many answers on specific programming issues can be found there.
Naviserver's HTTP driver is listening for example on port 80. It can accept and process multiple connections at once, it uses async I/O, therefore is able to accept and process many connections without spawning threads. Once a connection is accepted and HTTP request line and headers are read and parsed, driver passes this request structure to first available connection thread. There are can be different thread pools defined with minimum and maximum parameters, which prevents from overloadig the system with too many threads and at the same time keeps some number of idle threads ready to process new requests. That connection thread will be responsible now what to do with the request and for returning valid HTTP response, driver thread will continue to accept new connections and reading HTTP requests from other sockets.
In Naviserver there are different methods how to serve HTTP requests:
The difference between registered Tcl proc and filter is that once the Tcl proc is executed and finished, the connection is closed, it is responsibility of that Tcl proc to provide valid HTTP response.
The registerted filter works same way as Tcl proc but there can be multiple filters registered for the same method or URL and each filter can decide if the processing stops here or next filter can continue processing the request. The filters give more flexibility, there are several stages when different set of filters are called during request processing, those stages called preauth, postauth and trace which means during processing of any request the server calls
The execution chain looks like this:
To show this in action there are two examples how to register Tcl handlers in Naviserver:
ns_register_proc GET /*.oss template_handler ns_register_filter preauth GET /* security_handler ns_register_filter postauth GET *.oss template_handler |
Second command registers security handler to be called first for all requests. Third command registers template handler. Security filter can return stop return code that will tell the server not to call any further filters otherwise server will match next filter and execute it, if processing is till enabled the server will continue until all matched filters are called.
OSSWEB uses those features to install its own filters for security and template processing, security filter is registered as preauth, so it is called early as the first filter, then if request processing continues, template filter can be called if required.
Security filter can be registered for particular url patterns or as global filter for the whole site. It performs session support, user authentication and authorization. If session is not valid, user is redirected automatically to login page. If session is valid, security filter passes control to other filters or procs so it behaves completely transparent to the application. It even can be disabled completely in case of public site, but one nice feature it supports is public sessions, for every new connection it creates new session and assignes cookies so you can track the same user if you need it.
When HTTP request comes the only information we have is request line with query parameters and/or cookies.
These are examples of HTTP requests:
GET /index.html HTTP/1.0 GET /MyObjects/MyPortal.oss?account_id=123 |
Is it possible to create security layer that will serve security part and will be almost independent from application itself?
The reason many applications have it own security implementation because their security implementation too tightly bound to application logic. Why not to define security functions, put it into separate API and implement it as much independently from language or Web server as possible.
In this case application is the client to security sybsystem and should accept some rules or restrictions that this security subsytem introduces. The idea is not new, this is just slightly different approach for building Web application. Instead of creating application logic and then adding access restriction to it we can just use existing security implementation and just use its API. Our security model is based on naming convention for application requests. When you write your code, you should use this convention or API for building links between various parts of the application. Security layer is located between Web server and your application and takes these requests and applies them against it access database. Also we will use SQL database for storing sesison/user related information. All this will provide us with robust security system that can be used for many Web aplications.
The idea is that every request to our secure application web site should be processed through security handler. This handler verifies user credentials and allows request pass or rejects it according to access priviliges this user has in our security database. Actually we have two tasks which are connected, but at the same time separated from each other. First, we need to authenticate user, in other words we have know to who is trying to access our restricted web site. And second, we need to determine the user's access rights to the requested resource. Both these tasks should use the same database, at least they should have access to the same user information. First task can be accomplished by using cookies or native HTTP autentication method. Second task is itself our security model implementation.
For every request that matches this pattern the security handler is called and verifies request path, parameters and/or cookies. It tries to check these against security permissions that are stored in database for each particular user or group of users.
Our users and sessions tables look like this, not all columns are shown here for brievity:
TABLE ossweb_users user_id INTEGER user_name VARCHAR password VARCHAR salt VARCHAR salt2 VARCHAR TABLE ossweb_user_sessions user_id INTEGER session_id VARCHAR ipaddr VARCHAR login_time TIMESTAMP access_time TIMESTAMP |
And finally we create access permission table which will contain access rights for each user or group. Any user can have many records in this database, we can define as many access record as needed. There is no technical limits, just logic of secuirty for each particular application.
TABLE ossweb_acls acl_id INTEGER obj_id INTEGER obj_type CHAR project_name VARCHAR app_name VARCHAR page_name VARCHAR cmd_name VARCHAR ctx_name VARCHAR value CHAR |
Each user will have two cookies:
user_id cookie is assigned after a user successfully logged in into the system. It is unique identifier which is primary key in the 'ossweb_users' table. We can keep this cookie for a long time in the browser, because the same user always uses the same user_id. It is useful for example for public sites or public open part of secured sites. You can show some specific customized information for this particular user, like it is done on many Web portals. When a user provides his/her user name and password, we just look into the table and try to find record with provided user name. In case of success we verify provided password with existing one from database. We keep passwords encrypted in the database using SHA1 digest encryption algorithms. So for verification we have to calculate digest from provided user password with the salt which is kept in the database. This salt is just some randomly calculated string, the more unique the salt is the more secure the system and the more difficult it to break. Resulting encrypted string should be the same as the encrypted password.
To store a password we do:
/project_name/app_name/page_name.oss[?cmd=command[.context]] or /project_name/app_name/page_name.oss[?cmd=command][&ctx=context] |
/demo/knowledgebase/file.oss /demo/knowledgebase/edit.oss /demo/knowledgebase/file.app?cmd=move /demo/order/search.oss /demo/order/account.oss?cmd=show /demo/order/account.oss?cmd=add.service /demo/order/account.oss?cmd=add&ctx=package |
After we parsed request, OSSWEB will scan access database. Database access table 'ossweb_acls' contains all 5 tokens as columns. Each column may contain actual value or *, which means 'everything'.
All records are sorted in a such way, that more specific rows are always at the top and more generic at the bottom.
We retrieve permissions from 'ossweb_acls' table for the specified user and all gropus this user belongs to. Sorting this way allows us to use the first match because more specific matches are always ahead of more generic ones. We do not need to scan all records every time. Because it is possible to check access permissions inside application, we load all permissions into memory and call special routine that will scan this list for the match. This way we do not need to call SQL database every time we want to verify access to some parts of our application.
For example our user has access to 2 projects, 'portal' and 'doc'. Within project he has access to application called main. Within this application he can execute 'view' and 'search' for any pages. The page with user preferences 'prefs' can be updated by this user and any commands except 'delete' can be executed in the page 'apps'.
obj_id | obj_type | project_name | app_name | page_name | cmd_name | ctx_name | value
-----+----------+--------------+----------+-------------+----------+-------------+-------
0 | G | portal | main | apps | delete | link | Y
0 | G | portal | main | apps | delete | * | N
0 | G | portal | main | prefs | update | * | Y
0 | G | portal | main | * | search | * | Y
0 | G | portal | main | * | view | * | Y
0 | G | doc | * | * | view | * | Y
0 | G | portal | main | * | * | * | Y
0 | G | unknown | * | * | * | * | N
|
The last line in the list will make OSSWEB to deny all unknown requests regardless of the format, because by default all all security elements set to unknown and only command is set to view, it is very easy to define any kind fo filter that will restrict access to Web applications.
OSSWEB has Web administrative interface that allows to create security permissions per user or user group.
Now, when our session is authenticated and we got the green light to proceed to the application, OSSWEB core engine will try to route the request to the application page. As i said above, there are multiple way to create application pages but we start with simplest way, projects are directories, applications are directories inside the projects and application pages are .adp and .tcl files inside the application directories. Thus, the request
/demo/hello/world.oss?cmd=view |
All page directories and files are located under Naviserver pageroot which is /usr/local/ns/pages according to supplied with OSSWEB config file. To install OSSWEB if it is not isntalled yet, refer to README file in the OSSWEB distribution.
To create example page we should create project and aplication directories and world.adp file:
mkdir -p /usr/local/ns/pages/demo/hello cd /usr/local/ns/pages/demo/hello |
<BODY> Hello World </BODY> |
http://localhost:8080/demo/hello/world.oss |
File world.adp
<BODY> Hello World, current time is @current_time@ </BODY> |
set current_time [ns_fmttime [ns_time]] |
Simply put, in OSSWEB templating system .tcl file prepares the data to be shown on the page and .adp file formats and presents that data to the user.
Let's show one more example, we add page counter to hello world application to show how many time this page was shown. No database will be used, we keep the counter value in memory.
Just add one more command to world.tcl file File world.tcl
set current_time [ns_fmttime [ns_time]] set counter [nsv_incr HellowWorld PageCounter] |
<BODY> Hello World, current time is @current_time@, page is shown @counter@ times </BODY> |
Let's show how processing logic can be used inside templates. We will show greeting depending on time of day in our previous example. We will use OSSWEB registered tags. File world.tcl
set current_time [ns_fmttime [ns_time]] set hour [ns_fmttime [ns_time] "%H"] set counter [nsv_incr HellowWorld PageCounter] |
<BODY>
Good
<case>
<when @hour@ gt 11>
Afternoon
<when @hour@ gt 18>
Evening
<else>
Morning
</case>
<P>
Hello World, current time is @current_time@, page is shown @counter@ times
<P>
<ossweb::link -text Refresh>
</BODY>
|
Another useful feature to show data from the database on the page, there are several methods how to do this:
In .tcl file:
set list { one two three }
In .adp file:
<OL>
<list name=list>
<LI> @list:item@
</list>
</OL>
|
In .tcl file: set row [ossweb::db::multilist "SELECT user_name,user_email FROM ossweb_users"] In .adp file: <OL> <multilist name=row> <LI> @row:1@, @row:2> </multilist> </OL> |
In .tcl file:
ossweb::db::multirow rows "SELECT first_name,last_name,user_email FROM ossweb_ursers" -eval {
set row(full_name) "$row(first_name) $row(last_name)"
}
In .adp file:
<multirow name=rows>
@rows.full_name@
<if @row.user_email@ ne ""> Email: @rows.user_email@ </if>
<BR>
</multirow>
|
<foreach name=row query="SELECT user_name FROM ossweb_users"> @row.user_name@ </foreach> or <foreach name=row query=sql:ossweb.user.list> @row.user_name@ </foreach> |
Once the server found and called our page, it is up to us what to do next. We can retrieve all query parameter using ns_queryget command and perform our logic and return HTTP response back to client. But doing all that over and over again for every page would be tedious and time consuming. OSSWEB offers flexible request routing method that will simplify request processing and minimize amount of code that needs to written.
Because security handler parses and splits request into pre-defined elements, inside the page we know which command was requested. We just need to write so-called callback on that command that will be automatically called. Inside that callback you need to write only what is related to that particular command. In OSSWEB that means every page needs to call ossweb::conn::process command. It accepts list of commands and corresponding callbacks to call, also you can specify where to go after successfull execution of the callback and where to go in case of error. Of course every callback can do routing itself to the next point as well, it is up to you to decide how it is more convenient in this particular application.
Let's show how the code looks like:
# Update record
ossweb::conn::callback update {} {
...
}
# Read record from the DB and show form
ossweb::conn::callback edit {} {
...
}
# View all records
ossweb::conn::callback view {} {
...
}
ossweb::conn::process \
-eval {
update {
-exec update
-next "cmd view"
-on_error "cmd edit"
}
edit {
-exec edit
-on_error "cmd edit"
}
default {
-exec view
}
}
|
The way the "next" statement works, it re-evaluates the same page, i.e. re-executes ossweb::conn::process again with new command. But it re-evaluates itself in the same address space, so all Tcl variables and other objects are available. Deep inside OSSWEB actually does not parse the same Tcl file more than once, after the first time, Tcl code is compiled, wrapped into Tcl proc and cached by the Tcl interpreter, so next time OSSWEB actually just calls the Tcl command, no reading and parsing involved. But OSSWEB monitors the file and if it was modified, it is reloaded and recompiled.
For example let's assume update callback failed with some error, OSSWEB sees the "-on_error" statement, sets system command to "edit" and re-evaluates the page again. Now ossweb::conn::process is called with command edit, so OSSWEB calls callback "edit" which does something, for example shows the form to the user with error message.
If no errors occured in "update" callback, "-next" statement sets command to "view", OSSWEB re-evaluates the page and callback "view" is called which will show all records including just made changes.
Actually, the above form is kind of verbose, another way to do the same is to rely on OSSWEB ability to automatically call callbacks for each command. Only in this case we have to handle errors and routing in every callback.
# Update record
ossweb::conn::callback update {} {
...
ossweb::conn::next cmd view
}
# Read record from the DB and show form
ossweb::conn::callback edit {} {
...
ossweb::conn::next cmd view
}
# View all records
ossweb::conn::callback view {} {
}
ossweb::conn::process \
-on_error "cmd view"
|
Also, callbacks "update" and "view" now define what to do after they finish, they call special command ossweb::conn::next which changes execution context and sets new command to "view".
This looks simple but make the code and programming more like writing callbacks and routing between them. Another useful feature is that ossweb::conn::process converts incoming query parameters into local Tcl variables, so ther are accessable in every callback. For security reasons, not all parameters get converted, only those you are interested in.
The format is list of triples: varname type default_value ....
For example:
ossweb::conn::callback view {} {
if { $id != "" } {
...
}
}
ossweb::conn::process \
-columns { id int ""
name "" "" }
|
There is possible to keep different command in the different files and route requests flow between pages as well.
This is page update.tcl
# Update record
ossweb::conn::callback update {} {
...
ossweb::conn::next -page view -cmd view
}
ossweb::conn::process -on_error "-page view -cmd edit"
|
set id [ossweb::sql::quote [ns_queryget id]]
if { [ossweb::db::multivalue "SELECT * FROM account WHERE id=$id"] } {
ossweb::conn::set_msg "Unable to read account record"
ossweb::conn::next -cmd error -page index
return
}
|
# View all records
ossweb::conn::callback view {} {
...
}
ossweb::conn::process -on_error index
|
OSSWEB provides high level access to databases using native Naviserver database drivers and database API. Naviserver includes drivers for most databases like PostgreSQL, MySQL, Oracle, Informix, Interbase, Sybse, SQL Server, Berkeley DB. OSSWEB supports PostgreSQL only.
In order to access databse, it needs to be configured in the main Naviserver config file, it includes so called database pools, when access to each database has unique name which is used in the applications, never any application uses database username or passwords directly. This makes applications very portable and independent of specific database, just update config file and application will use different database server. Applications just request database handle from the named pool, if no connections to the database yet, Naviserver connects to databse, and returns the handle to the caller. Once application is done, it returns handle back to the pool and any other thread can reuse already opened database handle. This way not too many open connections to database are able to maintain very high number of users or working threads.
OSSWEB does not provide any mapping or object layer on top of SQL access. Database API provides one-to-one mapping between database columns and Tcl variables. Once read from the database program can access values from the SQL tables as Tcl variables. For updates Tcl variables will be used as well for corresponding SQL columns. It is up to the program to set them with appropriate values. There are helper procs that make working with database even more easier and there is object system module otcl that provides dynamic objects similar to Ruby or Python that can be used to build object-oriented layer. In most cases storing and manipulating Tcl variables or arrays with database access is enough.
There are several API calls available for the developer:
set date [ossweb::db::value "SELECT NOW()"] |
set names [ossweb::db::list "SELECT user_name FROM ossweb_users"]
foreach name $names {
ns_log notice Column: $name
}
|
if { [ossweb::db::multivalue "SELECT user_id,user_name FROM ossweb_users WHERE user_id=1"] } {
error "Record not found"
}
ns_log Notice User ID : $user_id
ns_log Notice User Name : $user_name
|
set users [ossweb::db::list "SELECT user_id,user_name FROM ossweb_users"]
foreach user $users {
foreach { user_id user_name } $user {}
ns_log notice User: $user_id $user_name
}
|
ossweb::db::multirow users "SELECT user_id,user_name FROM ossweb_users" |
<UL> <multirow name=users> <LI> @users.user_id@ @users.user_name@ </multirow> </OL> |
ossweb::db::multipage users \
"SELECT user_id FROM ossweb_users" \
"SELECT user_id,user_name,user_email FROM ossweb_users WHERE user_id IN (CURRENT_PAGE_SET)" \
-page $page
ossweb::conn::process -columns { page int 1 }
|
<multipage name=users> <UL> <multirow name=users> <LI> @users.user_id@ @users.user_name@ </multirow> </OL> <multipage name=users> |
ossweb::db::foreach "SELECT user_id,user_name FROM ossweb_users" {
ns_log notice $user_id $user_name
}
|
if { [ossweb::db::exec "DELETE FROM ossweb_users WHERE user_id=1"] } {
error "Unable to delete the record"
}
|
set msg_name Test
set description "Test message"
ossweb::db::insert ossweb_msgs
# Update record with msg_name=Test with new description Test2
ossweb::db::update ossweb_msgs -columns { description "" Test2 } msg_name Test
# Delete record by msg_name=Test and description=Test2
ossweb::db::delete ossweb_msgs msg_name Test description Test2
# Retrieve all records with msg_name=Test
set recs [ossweb::db::select ossweb_msgs msg_name Test]
# Same as above but store columns in local Tcl variables,
# this is equivalent of "SELECT * FROM ossweb_msgs WHERE msg_name='Test'"
ossweb::db::read ossweb_msgs msg_name Test
|
ossweb::sql::filter \
{ user_id ilist ""
user_name Text ""
user_email text "" }
|
ossweb::db::exec "INSERT INTO ossweb_users
[ossweb::sql::insert_values -full t \
{ user_id int ""
user_name "" ""
user_email "" ""
status "" active }]"
ossweb::db::exec "UPDATE ossweb_users
SET [ossweb::sql::update_values -skip_null t \
{ user_id int ""
user_name "" ""
user_email "" ""
status "" active }]"
|
Sometimes application may do similar tasks or similar database actions in different places. Duplicating the same SQL statements and in case of minor change going through all source code may be not fun to do. OSSWEB offers flexible solution, it is called SQL query abstraction.
In every place where SQL statement is expected, mostly in database API calls, instead of pure SQL code it is possible to use SQL ids. Those ids are uniquely identify SQL statements and used similar to procedure or API calls. It is stored in .xql files using very simple XML format with given query id, then applications can refer to that id and execute SQL statement as they are given them directly. But, one feature that makes it even more flexible and usefull, those stored statements can containt Tcl code and refer to Tcl variables, before passing to database for execution, that XQL definition will be processed by Tcl or better say to executed by Tcl and the resulting SQL statement will be passed to the database for execution.
Let's see this in example, we will use SQL statements from previous section. We create users.xql file and put there all our SQL statements.
<query name="ossweb.user.list"> <description> List of all users </description> <sql> SELECT user_id,user_name,user_email FROM ossweb_users </sql> </query> |
Now we take previos example they will look like this:
ossweb::db::multirow users sql:ossweb.user.list
set users [ossweb::db::list sql:ossweb.user.list]
foreach user $users {
foreach { user_id user_name } $user {}
ns_log notice User: $user_id $user_name
}
ossweb::db::foreach sql:ossweb.user.list {
ns_log notice $user_id $user_name
}
|
Also it is worth to mention that if i need to change that SQL and make it globally that users with status 'disabled' should not appear anywhere i just go and change one particular XQL file and query.
<query name="ossweb.user.list"> <description> List of all users </description> <sql> SELECT user_id,user_name,user_email FROM ossweb_users WHERE status <> 'disabled' </sql> </query> |
Another area where .xql files are useful is to make conditional SQL statements. Let's assume that we want to use only this SQL statement to all access to ossweb_users table. But we have search page which may take input of how somebody wants to locate particular user, let's say by user_name or/and by user_email. XQL definition allows to embed Tcl directly into SQL query and also allows to define required Tcl variables with default values.
<query name="ossweb.user.list">
<description>
List of all users
</description>
<sql>
SELECT user_id,user_name,user_email
FROM ossweb_users
WHERE status <> 'disabled'
[ossweb::sql::filter \
{ user_id ilist ""
user_name Text ""
user_email Text "" } \
-before AND]
</sql>
</query>
|
PostgreSQL should be installed and running before you do OSSWEB installation. If it is not installed, before installing OSSWEB you need to perform PostgreSQL setup;
make pgsql |
To prepare the environment just download OSSWEB from ftp://ftp.crystalballinc.com/pub/vlad/ossweb.tar.gz.
wget ftp://ftp.crystalballinc.com/pub/vlad/ossweb.tar.gz tar -xzf ossweb.tar.gz cd ossweb |
If PostgreSQL was installed manually and is running with your privileges and not as user postgres then:
make world dbuser=`whoami` |
make world |
/usr/local/ns/bin/nsd -f |
Typing http://localhost:8080/ossweb/ will bring OSSWEB login screen, just enter username admin with password admin. It will bring initial OSSWEB admin pages with menu on the left with installed applications.
By default OSSWEB config file enables development mode in which there is no need to restart the server even in case library files are changed. OSSWEB will detect changes to Tcl files and will reload them automatically.
The following parameters define development mode:
To show the whole process how to build applications for OSSWEB, let's create photo album application, from the beginning to complete web site. The application will not be feature complete but pretty functional and useful.
We will have 2 tables, albums and photos. albums table will hold list of albums with name and creator, photos will be for keeping list of photes for each album.
CREATE SEQUENCE album_seq;
CREATE TABLE album (
album_id INTEGER NOT NULL DEFAULT NEXTVAL('album_seq'),
album_name VARCHAR NOT NULL,
album_descr VARCHAR NULL,
user_id INTEGER NOT NULL DEFAULT 0 REFERENCES ossweb_users(user_id),
create_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
PRIMARY KEY(album_id)
);
CREATE TABLE photo (
album_id INTEGER NOT NULL REFERENCES album(album_id),
photo_name VARCHAR NOT NULL,
photo_size INTEGER NOT NULL,
create_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
PRIMARY KEY(album_id,photo_name)
);
|
/usr/bin/psql -U postgres ossweb or /usr/local/pgsql/bin/psql ossweb |
For the sake of explanation what we are doing this tutorial will show how to create applications manually.
After we created SQL tables we want to create new albums and be able to list, modify and delete them. Let's call our application photo.
mkdir -p /usr/local/ns/pages/photo cd /usr/local/ns/pages/photo |
ossweb::conn::callback view {} {
ossweb::db::multirow albums "SELECT *,ossweb_user_name(user_id) AS user_name FROM album ORDER BY 1"
}
ossweb::conn::process \
-default view
|
<ossweb:header>
<BODY>
<CENTER><ossweb:msg></CENTER><BR>
<ossweb:title>Photo Albums</ossweb:title>
<CENTER>
<ossweb:link -text "Create New Album" cmd edit>
</CENTER>
<border style=white>
<TR><TH>Album Name</TH><TH>Created</TH><TH>Owner</TH></TR>
<multirow name=albums>
<TR><TD>@albums.album_name@</TD>
<TD>@albums.create_date@</TD>
<TD>@albums.user_name@</TD>
</TR>
</multirow>
</border>
</BODY>
<ossweb:footer>
|
In album.adp we show datasource albums using <multirow> tag, parameter name specifies which named datasource to use. This tag repeats HTML block for every row in the datasource. Inside that tag, columns refered by their names. Other OSSWEB predefined tags are:
Now we can point our browser to http://localhost:8080/photo/album.oss.
To create new album we need to define form with fields and 2 callbacks, one will be called on form invokation and another on form submittion.
# Retrieve all albums
ossweb::conn::callback view {} {
ossweb::db::multirow albums "SELECT *,ossweb_user_name(user_id) AS user_name FROM album ORDER BY 1"
}
# Create new album, fire error on SQL error
ossweb::conn::callback create {} {
ossweb::db::insert album \
-error t
ossweb::conn::set_msg "Album has been created"
ossweb::conn::next cmd view
}
# Nothing yet
ossweb::conn::callback edit {} {
}
# Declare form for album details
ossweb::conn::callback create_form_album {} {
ossweb::form form_album -title "Album Details"
ossweb::widget form_album.album_name -label Name
ossweb::widget form_album.album_descr -type textarea -label Description \
-resize \
-cols 50 \
-rows 2 \
-optional
ossweb::widget form_album.cmd -type submit -label Create
ossweb::widget form_album.back -type button -label Back \
-url "cmd view"
}
# Request controller, declare Tcl variable album_id, create form form_album
# and perform request routing
ossweb::conn::process \
-columns { album_id int "" } \
-forms form_album \
-default view
|
Second argument is a Tcl list with column definitions, for now we have album name and user_id. Column definitions is a list with triples, column name, column type and default value. For each column name ossweb::db::insert will try to see if Tcl variable with such name exists and if so, its value will be used in construction SQL insert statement. Type "" means that column is VARCHAR, so it will be properly quotes and escaped. Next column is user_id, type userid means that value will be assigned with current session's user id. Once SQL statement is built it will be executed and we will return message about successful operation.
-error t flag tells ossweb::db::insert to raise exception in case any SQL runtime error, in this case transaction will be aborted if any and OSSWEB will stop processing callback and by default convention will re-route to command error. In our example we do not handle error command so control will be passed to view command which will show existing album list and <ossweb:msg> tag will render current error condition. Another defaut convention, in case of any form validation error, for example widget album_descr is described as optional but widget album_name is not, which means it is required. If you try to submit form with empty album_name, error condition will happen because form will not be validated, in this case control will be re-routed to command edit.
Callback edit will be called on clicking Create New Album link. Because we do not provide album_id we just show empty form. -forms form_albums clause in ossweb::conn::process tells OSSWEB that we need to prepare form named form_album. By convention, OSSWEB will call create_form_album callabck which should create the form. In out form we create 2 input widgets, name and textarea for description and 2 buttons, one for submit and another for returning back to the list.
Once the form filled, hit the Create button, new album should be created. But before that, we need to update album.adp file as well. Currently it does not know how to handle edit command.
<ossweb:header>
<BODY>
<CENTER><ossweb:msg></CENTER><BR>
<if @ossweb:cmd@ eq edit>
<formtemplate id=form_album></formtemplate>
<else>
<ossweb:title>Photo Albums</ossweb:title>
<CENTER>
<ossweb:link -text "Create New Album" cmd edit>
</CENTER>
<border style=white>
<TR><TH>Album Name</TH><TH>Created</TH><TH>Owner</TH></TR>
<multirow name=albums>
<TR><TD>@albums.album_name@</TD>
<TD>@albums.create_date@</TD>
<TD>@albums.user_name@</TD>
</TR>
</multirow>
</border>
</if>
</BODY>
<ossweb:footer>
|
For updating albums we need to be able to click on the existing album, update albums fields and save it back into the database. album.adp template will not change because we do not chnage the layout. New features will be added in the album.tcl.
# Show all albums
ossweb::conn::callback view {} {
ossweb::db::multirow albums "SELECT *,ossweb_user_name(user_id) AS user_name FROM album ORDER BY 1" -eval {
# Convert album_name field into link which points to album edit page
set row(album_name) [ossweb::html::link -text $row(album_name) -title $row(album_descr) cmd edit album_id $row(album_id)]
}
}
# Create new album record
ossweb::conn::callback create {} {
ossweb::db::insert album \
-error t
ossweb::conn::set_msg "Album has been created"
ossweb::conn::next cmd view
}
# Update existing album record by album_id
ossweb::conn::callback update {} {
ossweb::db::update album \
-error t \
album_id $album_id \
user_id $user_id
ossweb::conn::set_msg "Album has been updated"
ossweb::conn::next cmd view
}
# Delete existng album record by album_id and user_id
ossweb::conn::callback delete {} {
ossweb::db::delete album \
-error t \
album_id $album_id \
user_id $user_id
ossweb::conn::set_msg "Album has been deleted"
ossweb::conn::next cmd view
}
# Album record details
ossweb::conn::callback edit {} {
# New record, do nothing
if { $album_id == "" } {
return
}
# Read record by album_id
if { [ossweb::db::read album album_id] } {
error "OSSWEB: Invalid album id $album_id"
}
# Convert Create label into update command
ossweb::widget form_album.cmd -label Update
# Add delete button if this is our album
if { $user_id == [ossweb::conn user_id] } {
ossweb::widget form_album.delete -type button -label Delete \
-confirmtext "Album will be deleted, continue?" \
-url "cmd delete album_id $album_id"
}
# Update form with values fromTcl variables
ossweb::form form_album set_values
}
# Definition of form for album
ossweb::conn::callback create_form_album {} {
ossweb::form form_album -title "Album Details"
ossweb::widget form_album.album_id -type hidden \
-datatype int \
-optional
ossweb::widget form_album.album_name -label Name
ossweb::widget form_album.album_descr -type textarea -label Description \
-resize \
-cols 50 \
-rows 2 \
-optional
ossweb::widget form_album.cmd -type submit -label Create
ossweb::widget form_album.back -type button -label Back \
-url "cmd view"
}
# Controller that perform rquest processing and routing
ossweb::conn::process \
-columns { album_id int ""
user_id userid 0 } \
-forms form_album \
-default view
|
Callback update is similar to create, we just update given record with modified fields. user_id now just integer column, we assign user_id Tcl variable at the beginning with current user id or 0 if not logged in.
Delete callback will delete album by album_id and user_id, that means that i can delete only created by me albums
Callback edit is what handles edit command. If album_id is provided we will try to read the record. In case of wrong id we just fire error message which will be shown on the page using <ossweb:msg> tag. If we was able to locate the record, ossweb::db::read will create local Tcl variables with the same names as column names. In our case we will have album_id, album_name, album_descr, user_id, create_date variables.
Also widget cmd is changed with new label Update, on submit, it will make the command to be update (cmd and ctx parameters always converted into lowercase because they are reserved word and commands are lowercase only). New widget delete is added which will provide ability to delete existing album.
In the form definition we added new hidden widget album_id with datatype integer, this will have the form validate album_id on submitions and will guard against SQL-injection attacks.
The last change worth mentioning is -columns parameter for ossweb::conn::process. It now has 2 items, we added user_id which is described as const. That means it will be assigned once on page invocation. In case if in the url user_id=something will be sent, it will be ignored. [ossweb::conn user_id 0] means return current user id if logged in or 0 as default if no user session currently exists.
Now we are ready to upload some pictures into our albums. There are different ways to do this but we will choose simple way with some additional features to show what OSSWEB can do. The GUI will have tabs on top of the album form, default will be album details, then Upload and Photos. In Upload section we will provide several upload buttons to be able to send more than one picture, in the Photos section we will format all photos as thumbnails.
In album.adp file we added tabbed buttons using tag <formtab> and 2 new sections in edit mode. Tcl variable tab will be used for switching between different tabs. When we show photos, we use table with 3 pictures in each row.
We refer to @photos:rowcount@ which is predefined variable created automatically for each multirow datasource. It hods number of records in the record set. If no uploaded pictures, we show message, otherwise HTML table with 3 pictures in the row. For each row, <multirow> tag automatically assigned Tcl variable @photos:rownum@ with current row sequential number starting with 1.
<ossweb:header>
<BODY>
<CENTER><ossweb:msg></CENTER><BR>
<if @ossweb:cmd@ eq edit>
<formtab id=form_tab>
<case>
<when @tab@ eq upload>
<formtemplate id=form_upload></formtemplate>
<when @tab@ eq photos>
<if @photos:rowcount@ nil or @photos:rowcount@ eq 0>
There are no uploaded pictures in this album yet.
<else>
<TABLE BORDER=0 WIDTH=100% >
<TR>
<multirow name=photos>
<TD>@photos.photo_url@<BR>
@photos.photo_name@: @photos.photo_size@
</TD>
<if @photos:rownum@ not mod 4>
</TR><TR>
</if>
</multirow>
</TR>
</TABLE>
</if>
<else>
<formtemplate id=form_album></formtemplate>
</case>
<else>
<ossweb:title>Photo Albums</ossweb:title>
<CENTER>
<ossweb:link -text "Create New Album" cmd edit>
</CENTER>
<border style=white>
<TR><TH>Album Name</TH><TH>Created</TH><TH>Owner</TH></TR>
<multirow name=albums>
<TR><TD>@albums.album_name@</TD>
<TD>@albums.create_date@</TD>
<TD>@albums.user_name@</TD>
</TR>
</multirow>
</border>
</if>
</BODY>
<ossweb:footer>
|
# Read all album records
ossweb::conn::callback view {} {
ossweb::db::multirow albums "SELECT *,ossweb_user_name(user_id) AS user_name FROM album ORDER BY 1" -eval {
# Convert album_name into link that points to album edit page
set row(album_name) [ossweb::html::link -text $row(album_name) -title $row(album_descr) cmd edit album_id $row(album_id)]
}
}
# Create new album record, if successfull return to view mode
ossweb::conn::callback create {} {
ossweb::db::insert album \
-error t
ossweb::conn::set_msg "Album has been created"
ossweb::conn::next cmd view
}
# Update existing album record by album_id, if successfull return to view mode
ossweb::conn::callback update {} {
ossweb::db::update album \
-error t \
album_id $album_id \
user_id $user_id
ossweb::conn::set_msg "Album has been updated"
ossweb::conn::next cmd view
}
# Delete existing album by album_id and user_id
ossweb::conn::callback delete {} {
ossweb::db::delete album \
-error t \
album_id $album_id \
user_id $user_id
ossweb::conn::set_msg "Album has been deleted"
ossweb::conn::next cmd view
}
# Save uploaded pictures
ossweb::conn::callback upload {} {
# Root path where we store images
set path [ns_info pageroot]/photo
# For all 2 pictire input fields
for { set i 1 } { $i <= 5 } { incr i } {
# Store uploaded image into our directory
set photo_name [ossweb::file::upload file$i -path $path -mode path]
# Check if image exists
if { $photo_name == "" || ![file exists $path/$photo_name] } {
continue
}
# Save image file size
set photo_size [file size $path/$photo_name]
# Create new record in photo table
ossweb::db::insert photo \
-error t
}
ossweb::conn::set_msg "Pictures have been uploaded"
ossweb::conn::next cmd edit tab upload
}
# Album details callback
ossweb::conn::callback edit {} {
# New album, do nothing
if { $album_id == "" } {
return
}
# Read album record by album_id
if { [ossweb::db::read album album_id] } {
error "OSSWEB: Invalid album id $album_id"
}
# Perform tab-specific actions
switch -- $tab {
photos {
# Retrieve all pictures for given album
ossweb::db::read photos -type "multirow photos" album_id $album_id -eval {
# Make file size human readable
set row(photo_size) [ossweb::util::size $row(photo_size)]
# Assign with link to open new window with full picture
set row(photo_url) [ossweb::html::link -image /photo/$row(photo_name) -url $row(photo_name) \
-width 150 \
-height 150 \
-window Photo]
}
}
default {
# Update album mode, change command to update
ossweb::widget form_album.cmd -label Update
# If out album, define delete button
if { $user_id == [ossweb::conn user_id] } {
ossweb::widget form_album.delete -type button -label Delete \
-confirmtext "Album will be deleted, continue?" \
-url "cmd delete album_id $album_id"
}
# Update form with values fromTcl variables
ossweb::form form_album set_values
}
}
}
# Form definition for album
ossweb::conn::callback create_form_album {} {
ossweb::form form_album -title "Album Details"
ossweb::widget form_album.album_id -type hidden \
-datatype int \
-optional
ossweb::widget form_album.album_name -label Name
ossweb::widget form_album.album_descr -type textarea -label Description \
-resize \
-cols 50 \
-rows 2 \
-optional
ossweb::widget form_album.cmd -type submit -label Create
ossweb::widget form_album.back -type button -label Back \
-url "cmd view"
}
# Form definition for uploading pictures
ossweb::conn::callback create_form_upload {} {
ossweb::form form_upload -title "Upload Pictures"
ossweb::widget form_upload.album_id -type hidden -datatype int
ossweb::widget form_upload.file1 -type file -label Picture1
ossweb::widget form_upload.file2 -type file -label Picture2 \
-optional
ossweb::widget form_upload.file3 -type file -label Picture3 \
-optional
ossweb::widget form_upload.file4 -type file -label Picture4 \
-optional
ossweb::widget form_upload.file5 -type file -label Picture5 \
-optional
ossweb::widget form_upload.cmd -type submit -label Upload
}
# Form definition for tabs
ossweb::conn::callback create_form_tab {} {
ossweb::form form_tab
# For new albums do not show tabs
if { $album_id == "" } {
return
}
set url "cmd edit album_id $album_id"
ossweb::form form_tab -widgets {
{ details -type link -label Details -url $url }
{ upload -type link -label Upload -url $url }
{ photos -type link -label Photos -url $url }
}
}
# Call request controller, create specified forms, declare variables
# and perform request routing depending on command
ossweb::conn::process \
-columns { album_id int ""
tab "" ""
user_id userid 0 } \
-forms { form_album form_tab form_upload } \
-default view
|
form_tab for is used for generating tabs, there are different styles of tabs in the OSSWEB, for this example we will use default. To try and see how it look, try to set <formtab id=form_tab style=blue> or any other styles from the list: text ebay square oval oval2.
Another form is form_upload, we will use it for uploading puctures into our album. We defined 5 inputs, first one is required but other are optional. When this form is submitted, it will use upload command as defined by Upload button.
Callback edit has been changed as well, now we have to handle different tabs, so we use switch on Tcl variable tab to produce different data for different parts of ADP template. When tab photos was chosen, we retrieve all pictures for the selected album into multirow datasource or recordset.
photo_size field is updated with human readable format of the image size. We assign every image with link to popup separate window whcih will show image in full size. In the page we scale them to 150x150, some images will be distored but in this application we will not perform smart scaling using ImageMagick or with other conversion tool.
New callback upload has been created as well, it will handle upload command: save pictures and create records in photo table for each uploaded picture. Pictures will be saved in the same directory where our pages reside. There is another mechanism to save picturs out of web server root so it cannot be accessed directly. For public albums this is not very useful and a little bit more complicated. When we save picture name in the table we will get the size of the image file and store it in the table, just for convenience for not checking file size on every view.
OSSWEB API logically split into different Tcl namespace which provides clean way of separation and allows keeping related function close to each other instead of spreading the login across the whole system.
The hierachy tree looks like this: