[ Table Of Contents | Keyword Index ]

OSSWEB(n) 2.0 "OSSWEB Framework"

NAME

OSSWEB - Framework for developers

TABLE OF CONTENTS

    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
    KEYWORDS
    COPYRIGHT

DESCRIPTION

Web development framework based on Tcl language and Naviserver application server.

History

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.

Why Naviserver?

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.

Concepts

Components

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:

So at the minimum, just installed Naviserver can serve static HTML files as Apache or other Web servers. In addition to that, Naviserver has special server-side templates that are called ADP (AOLserver Dynamic Page), this is similar to ASP or JSP. If file has an extension .adp or other extension that is registered as ADP, server will parse it and process all known tags. At the minimum well known <% %> tags are supported where Tcl scripts can be placed and they will be processed at the time of the page render. Naviserver allows to register custom tags and attach Tcl handlers to them, so when server parses the page and sees those tags it will call registered Tcl handler and it is up to the programmer how to render that tag.

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

  1. preauth filters first,

  2. if no errors, it performs HTTP authorization if exists, usually everybody uses cookies nowadays, so basic HTTP authorization is rare,

  3. then the server calls postauth filters, once the connection is closed, all trace filters are called.

The execution chain looks like this:

  1. accept socket

  2. read headers

  3. queue

  4. preauth filters

  5. basic authorization if required

  6. postauth filters

  7. Tcl proc if registered

  8. close conection

  9. trace filters
For example, default access logging is performed by trace filter, it is written in C as a separate module, but uses the same infrastucture, so it is easy to write your own trace filter that will do any kind of stuff with closed connection, like logging, statistics or replication.

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

First command registers Tcl proc template_handler for all requests that end with .oss, once this callback is processed, connection is closed

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 Model

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

There may be some additional headers that inform Web server about languages and encodings the browser supports, cookies and other information. All these headers except cookies are suplementary information, the resource is identified by first request line. For general purpose Web server which serves all kinds of requests including HTML files, CGI scripts, request line basically contains file path and name within Web server's filesystem. In order to prevent unauthorized control to some files or scripts, Web server has various security mechanisms. They restrict access to the whole resource by name or directory having no knowledge about this resource and resource's properties. When building Web application with access restrictions, or even simple application which requires autorized access, i had to build this security layer again for every application. Mostly because different Web servers have different capabilities and API, even different Apache modules has different API, so when i programmed with Python i implemented this in Python, when i switched to PHP i had to use PHP functions and re-write authorization again. But every time i write this every time i do the same job.

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

Users are combined into groups with the same set of access permissions. This is very convenient, it is possible to define set of permissions for each type of users and put these access rights into different group. When user is created, it may be linked to corresponding group, so there is no need to define the same permissions to each particular user. We have here some kind of inheritance, if there is no specific access permission for user, we will look into user's group(s).

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

Other columns reflect our security model which we disscuss later in this document.

Each user will have two cookies:

All security cookies are digitaly cryptographically signed, so tampering of cookies is not possible. Session_id for each user is also stored in the session table 'ossweb_user_sessions' and periodically checked for expiration.

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:

To verify a user during logon: session_id cookie is assigned with every login and live for some configured period of time. Every time a user logs into the system, new session id is generated and assigned to a user using cookie mechanism. Supporting session and identifying user action within session is essential for interactive Web application. This is used for example in shopping carts for e-commerce sites. We store session id for each user in the same 'ossweb_user_sesions' table. Each request which contains session_id cookie is verified with the ID of current session for each user. If the values are different, request just refused. Also session_id cookie is set to expire after some period of time. The server will re-new session cookie just before they are going to expire if user is working with the system. If user is away from his computer and after he returned and cookie is already expired, the system will prompt for the password in order to verify who is working. OSSWEB instroduces the following conventions that are independent from any application logic and those conventions are not required in full: The security model defined the following security elements: Each request will consist from 4 or 5 components:

 

   /project_name/app_name/page_name.oss[?cmd=command[.context]]

   or

   /project_name/app_name/page_name.oss[?cmd=command][&ctx=context]

where: Examples:

 

/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

When a request comes, security filter parses request path and splits it into elements. We assume that request should conform to our naming convention but if not OSSWEB will set default values for missing elements. So, if we cannot parse command, default is set to view, which is logically true, basically HTTP request is an attempt to view interested us document from web server. if we have just page name, then project, app_name and context will be set to unknown. The only part which is required is the page name and this is basically makes us regualr Web server which serves pages. It is important to have such default values, it simplifies support and maintenance of access priviliges because there should not be any unexpected behaviour or unknown states in which the security system can be. We should know at any time what kind of request came and what kind of resources it requested.

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

Let's assume that we have request for the page /portal/main/apps/?cmd=view. Scanning our permission list we will find first match at second record from the top. First three tokens are exact match and cmd_name is '*' which means any commands. This record will give us 'Y' which means access is allowed to this resourse. If we will request for /portal/main/apps?cmd=delete we will match this request to the fourth record in the permission list which will give us 'N' as a result. Access is denied, because this user is not allowed to delete anything except links from the application page. In order to delete link from some application, the url should look like /portal/main/apps?cmd=delete.link. In this case search for permission record will stop at third record which gives us 'Y'.

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.

Page Templates

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

will be resolved into /demo direcotry, in that directory should exist order/ subdirectory and in order/ subdirectory there should be at least world.adp file. This will be our presentation layer, optional world.tcl may exists which will provide the logic and the data for the 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

Create file world.adp

 

<BODY> Hello World </BODY>

Now assuming that Naviserver and OSSWEB are installed and running on port 8080 as per installation instructions, just point your browser to

 

http://localhost:8080/demo/hello/world.oss

You should see Hello World, nothing fancy but simple. Now let's add some login and scripting to this example. Let's show current date and time with every request. We will use OSSWEB tempalting features, not internal Naviserver ADP tags.

File world.adp

 

<BODY>
  Hello World, current time is @current_time@
</BODY>

File world.tcl

 

set current_time [ns_fmttime [ns_time]]

Now when to point your browser to the same page you should see the message with current time, every time you refresh the page, time should change. In this example we just show value of the variable, the variable is set in Tcl script file which is called before rendering world.adp file. This is how OSSWEB templating works, when the engine resolves the request into page and it sees that pair of world.adp and world.tcl files exist, world.tcl file is called first, it does the logic, then world.adp page is rendered in the same address space, i.e. in the world.adp file you can access all variables that were created in the world.tcl script.

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]

and re-create world.adp file

 

<BODY>
  Hello World, current time is @current_time@, page is shown @counter@ times
</BODY>

nsv_incr command will increment variable in the memory every time the page is requested, this command keeps the value in the shared memory and any thread can access it, all internal locking is done automatically. It returns incremented value and we save it in local Tcl variable, so world.adp page can access it and show in the html.

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]

File world.adp we check variable hour and output time of day depending on the current hour. At the end we provide link to refresh page.

 

<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:

Request Routing

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 example above defines 2 callbacks for each command it supports, ossweb::conn::process has -eval block which is similar to switch statement whereby for each command there is another block that defines what to do. For example, update command shoudl execute callback "update". If there is no error, next action will be "view". In case of error, next action will be "edit".

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"

If you take a look at the new example, basically it works the same way as previous one, but OSSWEB calls callback by command name and we defined global error handler to switch to command "view" on any error in any callback.

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


tells OSSWEB to create variables "id" and "name" with empty string as default value, so when page is called those variables are available and no need to verify their existence. If query parameters "id" or "name" were given in the url, then values will be stored in corresponding variables. Type "int" will enforce value of parameter id to be integer, if not, value will be ignored.

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"

This is page edit.tcl

 

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
}

This is page view.tcl

 

# View all records
ossweb::conn::callback view {} {
   ...
}

ossweb::conn::process -on_error index

In the example above we have 3 files, update.tcl, edit.tcl and view.tcl that perform some specific actions. Routing between them now has additional parameter that tells name of the page which to call and that means that OSSWEB will locate and call that page and execute specified command in that page. edit.tcl does not even have ossweb::conn::process call because it reads the record, stores all columns in the local Tcl variables and exit. In case of error, it passes control to index page and sets system error message that will be shown by index page for example.

Database Access

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:

SQL Query Abstraction

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>

By default all .xql files are stored under /usr/local/ns/xql, just drop new file there and OSSWEB server will detect and pick it up.

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
}

Same query is used in all parts of code, in some ways this amy look less readable, but if using proper names for queries they will tell you exactly what they are doing and if SQL is not what needs to modified or developed, this abstraction makes Tcl code even more readable.

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>

If xql file has been changed, OSSWEB will detect modification and re-read that file, so new query will immediately be used in the application. In real production it is possible to disable runtime checks for modified file to gain more performance but in development and no so busy sites, this ability to tune SQL without touching code is very usefull and leads to faster and better maintanance. Of course, production system are not supposed to be touched at all but in real life the opposite happens quite often.

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>

Now, if no Tcl variables user_id,user_name,user_email exist, this query will return all records. But when used in search page and user submitted values for any of those fields, SQL query will be constructed with additional conditions. It is possible to use different Tcl variable names and then map then, this will be explained in more details in API sections.

Installing OSSWEB

PostgresQL Installation

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 install_pgsql

This command will download and install PostgreSQL into /usr/local/pgsql and start the database server. Now time for OSSWEB installation.

OSSWEB Installation

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 install_world dbuser=`whoami`

or if PostgreSQL is provided by your Linux distribution and is already running with privileges of user postgres:

 

make install_world

The command above will download, compile and install Naviserver into /usr/local/ns. It provides default config file which accesses local PostgreSQL database named ossweb and listens on port 8080 for HTTP requests. To start the server in the shell just type:

 

/usr/local/ns/bin/nsd -f

-f flag tells to start in foreground so the server can be stopped by pressin Control-C. If background execution is preferred, just do not specify -f flag, server will detach itself from the terminal. Pointing your browser to http://localhost:8080 should display default Naviserver index page.

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.

Development Mode

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:

  1. ns_param server:development t Enable development mode. Also the server searches local direcotory where current page is being served for .xql files. In production mode all .xql files should be put into /usr/local/ns/xql directory but in develpment mode modules can be developed initially in their local folders.

  2. ns_param server:debug t This parameter enables global debug output for incoming requests, nsd.log will contain the complete trace of what is called during every request which makes debugging and tracing much easier.

Tutorial: First Application

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.

Database Model

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

Execute those statements in the psql, just copy them and paste in the PostgreSQL shell which can be invoced as:

 

/usr/bin/psql -U postgres ossweb

or

/usr/local/pgsql/bin/psql ossweb

Now we have 2 options, we can write all code manually or we can use OSWEB Page Generator to create some initial templates and SQL queries for us. To generate code, point your browser to http://localhost:8080/ossweb/admin/generator.oss

For the sake of explanation what we are doing this tutorial will show how to create applications manually.

Albums List

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

Now use your favorite text editor and create file album.tcl.

 

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

and then we create file album.adp

 

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

The code above defines callback command view which will be called by default. When request comes and OSSWEB does not see any specific command given, it will call command view. This callback just retrieves all records from album table and puts them into datasource named albums.

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.

Creating Albums

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

Let's explain what we've just added. New callback create will be called when form with album info will be submitted. It will insert new record into table album.

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>

In new album.adp file now we check what is current command and in case of edit we will show album form.

Updating Albums

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 view now has -eval parameter which is Tcl script that will be called for every row. Inside that script Tcl array row is automatically created with all columns set for the current record. In our case we build url so album name will be clickable and set link title to album description. when mouse over on album name and hold it, yellow tip will appear with description. On click, command edit will be called with given album id.

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.

Uploading Photos

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>

Now album.tcl needs to be extended. this will be our final version of the application, so below is the full source code for the album.tcl and after it will follow explanation of what was changed or added.

 

# 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

We start from the bottom, -forms parameter now include 3 forms to be created on page invocation. There are ways to distiguish when to create any particular form but for the first tutorial we will make this simple. We create all forms and but in only one case, when we need to create new album we destroy form_tab form so tabs will not show with empty form.

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

Namespace Hierarchy

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:

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

KEYWORDS

<border> , <calendar> , <case> , <continue> , <decode> , <else> , <exit> , <foreach> , <formerror> , <formgroup> , <formhelp> , <formlabel> , <formtab> , <formtemplate> , <formwidget> , <formwidgets> , <grid> , <group> , <helpbutton> , <helpimage> , <helptitle> , <if> , <include> , <join> , <list> , <master> , <multilist> , <multipage> , <multirow> , <ossweb:footer> , <ossweb:header> , <ossweb:link> , <ossweb:msg> , <ossweb:title> , <ossweb:url> , <return> , <row> , <rowfirst> , <rowlast> , <rowsection> , <rowtitle> , <slave> , <tcl> , <template> , <when> , Admin Procs , Albums List , Cache Procs , Components , Concepts , Connection Procs , Conversion Procs , Core Procs , Creating Albums , Custom Tags , Database Access , Database Model , Database Procs , Development Mode , File Storage Procs , Forms and Widget Procs , HTML Procs , History , Installing OSSWEB , Lookup Procs , Namespace Hierarchy , OSSWEB API , OSSWEB Installation , Page Templates , PostgresQL Installation , Request Routing , SQL Query Abstraction , SQL Support Procs , Scheduling Procs , Security Model , Template Procs , Tutorial: First Application , Type Validation Procs , Updating Albums , Uploading Photos , Utility Procs , Why Naviserver? , ossweb::acl , ossweb::admin::belong_group , ossweb::admin::create_user , ossweb::admin::delete_user , ossweb::admin::flush_user , ossweb::admin::login , ossweb::admin::logout , ossweb::admin::password , ossweb::admin::prefs , ossweb::admin::read_popup , ossweb::admin::reboot , ossweb::admin::send_popup , ossweb::admin::update_session , ossweb::admin::update_user , ossweb::adp::AppendCode , ossweb::adp::AppendData , ossweb::adp::Buffer , ossweb::adp::Cache , ossweb::adp::Compile , ossweb::adp::ContentType , ossweb::adp::Cookie , ossweb::adp::DirName , ossweb::adp::Evaluate , ossweb::adp::Execute , ossweb::adp::Exit , ossweb::adp::File , ossweb::adp::Include , ossweb::adp::Length , ossweb::adp::Level , ossweb::adp::Parse , ossweb::adp::Reset , ossweb::adp::Trim , ossweb::adp::Write , ossweb::cache , ossweb::cache::append , ossweb::cache::create , ossweb::cache::exists , ossweb::cache::flush , ossweb::cache::get , ossweb::cache::incr , ossweb::cache::keys , ossweb::cache::lappend , ossweb::cache::names , ossweb::cache::put , ossweb::cache::run , ossweb::cluster , ossweb::cluster::master , ossweb::cluster::send , ossweb::coalesce , ossweb::config , ossweb::conn , ossweb::conn::callback , ossweb::conn::check_acl , ossweb::conn::clear_property , ossweb::conn::create_acl , ossweb::conn::export_form , ossweb::conn::get_property , ossweb::conn::header , ossweb::conn::hostname , ossweb::conn::init_reftable , ossweb::conn::init_vars , ossweb::conn::localnetwork , ossweb::conn::log , ossweb::conn::next , ossweb::conn::parse_request , ossweb::conn::parse_session , ossweb::conn::parse_url , ossweb::conn::parse_user , ossweb::conn::process , ossweb::conn::query , ossweb::conn::read_acl , ossweb::conn::read_user , ossweb::conn::redirect , ossweb::conn::redirect_access_denied , ossweb::conn::redirect_for_login , ossweb::conn::response , ossweb::conn::secret , ossweb::conn::sessionvars , ossweb::conn::set_msg , ossweb::conn::set_property , ossweb::conn::set_session_id , ossweb::conn::set_user_id , ossweb::conn::shared_secret , ossweb::conn::sign , ossweb::conn::sign_cookie , ossweb::conn::signed_cookie , ossweb::control::prefs , ossweb::control::user , ossweb::control::user::group , ossweb::convert::array_to_js , ossweb::convert::array_to_list , ossweb::convert::array_to_set , ossweb::convert::array_to_string , ossweb::convert::list_to_array , ossweb::convert::list_to_attributes , ossweb::convert::list_to_query , ossweb::convert::list_to_vars , ossweb::convert::plain_list , ossweb::convert::query_to_value , ossweb::convert::query_to_vars , ossweb::convert::set_to_array , ossweb::convert::set_to_attributes , ossweb::convert::set_to_list , ossweb::convert::set_to_vars , ossweb::convert::string_to_list , ossweb::convert::value_to_query , ossweb::convert::vars_to_list , ossweb::crc32 , ossweb::datatype::boolean , ossweb::datatype::email , ossweb::datatype::file , ossweb::datatype::float , ossweb::datatype::ilist , ossweb::datatype::int , ossweb::datatype::integer , ossweb::datatype::ipaddr , ossweb::datatype::macaddr , ossweb::datatype::money , ossweb::datatype::name , ossweb::datatype::phone , ossweb::datatype::text , ossweb::datatype::textarea , ossweb::datatype::url , ossweb::datatype::validate , ossweb::datatype::value , ossweb::date , ossweb::db::begin , ossweb::db::block , ossweb::db::cache , ossweb::db::columns , ossweb::db::commit , ossweb::db::currval , ossweb::db::delete , ossweb::db::exec , ossweb::db::foreach , ossweb::db::handle , ossweb::db::insert , ossweb::db::list , ossweb::db::multilist , ossweb::db::multipage , ossweb::db::multirow , ossweb::db::multivalue , ossweb::db::nextval , ossweb::db::parse_error , ossweb::db::read , ossweb::db::release , ossweb::db::rollback , ossweb::db::rowcount , ossweb::db::select , ossweb::db::sethandle , ossweb::db::tables , ossweb::db::update , ossweb::db::value , ossweb::db::vars , ossweb::db::xql , ossweb::decode , ossweb::decrypt , ossweb::dehexify , ossweb::dirname , ossweb::encrypt , ossweb::exists , ossweb::file::check_acl , ossweb::file::delete , ossweb::file::exists , ossweb::file::getname , ossweb::file::image_exists , ossweb::file::link , ossweb::file::list , ossweb::file::mtime , ossweb::file::name , ossweb::file::open , ossweb::file::register_download_check , ossweb::file::register_download_proc , ossweb::file::rename , ossweb::file::return , ossweb::file::root , ossweb::file::save , ossweb::file::size , ossweb::file::stat , ossweb::file::upload , ossweb::file::url , ossweb::file_list , ossweb::form , ossweb::hexify , ossweb::html::combobox_menu , ossweb::html::dropdown_menu , ossweb::html::escape , ossweb::html::files , ossweb::html::font , ossweb::html::form_title , ossweb::html::help_link , ossweb::html::image , ossweb::html::images , ossweb::html::include , ossweb::html::js_url , ossweb::html::link , ossweb::html::menu , ossweb::html::menu::admin , ossweb::html::menu::js , ossweb::html::play_sound , ossweb::html::popup_handlers , ossweb::html::popup_menu , ossweb::html::popup_object , ossweb::html::proxy_url , ossweb::html::quote , ossweb::html::refresh , ossweb::html::select , ossweb::html::sounds , ossweb::html::textwidth , ossweb::html::title , ossweb::html::url , ossweb::iftrue , ossweb::image_exists , ossweb::image_name , ossweb::lexists , ossweb::list_config , ossweb::lookup::exec , ossweb::lookup::form , ossweb::lookup::link , ossweb::lookup::master , ossweb::lookup::mode , ossweb::lookup::onereturn , ossweb::lookup::property , ossweb::lookup::row , ossweb::lookup::start , ossweb::lookup::stop , ossweb::lookup::url , ossweb::message , ossweb::multirow , ossweb::negative , ossweb::number_list , ossweb::nvl , ossweb::oproc , ossweb::option_list , ossweb::ovar , ossweb::pad0 , ossweb::project , ossweb::random , ossweb::random_range , ossweb::read_file , ossweb::register_init , ossweb::reset_config , ossweb::resource::check , ossweb::resource::lock , ossweb::resource::trylock , ossweb::resource::unlock , ossweb::schedule::init , ossweb::schedule::job , ossweb::schedule::run , ossweb::schedule::tsearch , ossweb::sendmail , ossweb::set_config , ossweb::sql::columns , ossweb::sql::dbargs , ossweb::sql::filter , ossweb::sql::ignore , ossweb::sql::insert_values , ossweb::sql::list , ossweb::sql::multipage , ossweb::sql::quote , ossweb::sql::update_values , ossweb::tag::create , ossweb::tag::info , ossweb::template , ossweb::trim0 , ossweb::true , ossweb::tsearch , ossweb::util::chart , ossweb::util::clock , ossweb::util::email_template , ossweb::util::fmt , ossweb::util::gethome , ossweb::util::gets , ossweb::util::lmax , ossweb::util::lmin , ossweb::util::lshuffle , ossweb::util::money , ossweb::util::number , ossweb::util::open , ossweb::util::puts , ossweb::util::size , ossweb::util::wrap_text , ossweb::version , ossweb::widget , ossweb::widget::boolean , ossweb::widget::button , ossweb::widget::calendar , ossweb::widget::categoryselect , ossweb::widget::checkbox , ossweb::widget::colorselect , ossweb::widget::combobox , ossweb::widget::date , ossweb::widget::email_lookup , ossweb::widget::employee_lookup , ossweb::widget::file , ossweb::widget::filelink , ossweb::widget::groupselect , ossweb::widget::helpbutton , ossweb::widget::hidden , ossweb::widget::howto , ossweb::widget::image , ossweb::widget::imageselect , ossweb::widget::inform , ossweb::widget::intervalselect , ossweb::widget::label , ossweb::widget::labelselect , ossweb::widget::link , ossweb::widget::lookup , ossweb::widget::multiselect , ossweb::widget::none , ossweb::widget::numberselect , ossweb::widget::password , ossweb::widget::popupbutton , ossweb::widget::popuptext , ossweb::widget::radio , ossweb::widget::readonly , ossweb::widget::reminder , ossweb::widget::reset , ossweb::widget::select , ossweb::widget::sorting , ossweb::widget::soundselect , ossweb::widget::submit , ossweb::widget::text , ossweb::widget::textarea , ossweb::widget::user_lookup , ossweb::widget::userselect , ossweb::widget::webtracking , ossweb::write_file

COPYRIGHT

Copyright © Vlad Seryakov vseryakov@gmail.com December 2006