The Anubis Project. Making interactive Web sites. Copyright (c) Alain Prouté 2004-2005. Authors: Alain Prouté Last revision: January 2005. In this file we propose simple tools for making well structured interactive and secured web sites. ----------------------------------- Table of Contents --------------------------------- *** (1) Structure of a web site. *** (1.1) Overview. *** (1.2) What web pages are made of. *** (1.3) Actions. *** (1.4) States. *** (2) Carrying on. *** (2.1) Describing your web sites. *** (2.2) Directories on the server's disk. *** (2.3) Starting your web sites. *** (3) The HTML interface. *** (3.1) Types used by the HTML interface. *** (3.2) ``in form'' versus ``off form''. *** (3.3) Defining your own style. *** (3.4) Actioners and forms. *** (3.5) Local popup. --------------------------------------------------------------------------------------- read tools/basis.anubis read web/common.anubis read web/multihost_http_server.anubis *** (1) Structure of a web site. First of all we need to explain what a web site should be made of. Ideally, the client should see the web site working as any other interactive computer software. So, it should be clear that a 'session' (i.e. a visit to the web site, including the consultation of several pages) is some kind of conversation between the client and the web site, and that the web site should maintain a 'current state' of this conversation. At each new request (click) from the client, this state must be updated. *** (1.1) Overview. The data which are stored into the state should be considered as 'session data'. They do not encompass 'permanent data', which are typically stored into a data base. We want to separate the following two functionalities (which are used at each request (click) during a single session): - computing the new state from the previous state and from the client request, - computing the page to be sent to the client from the new current state. The next picture shows the structure we have in mind: request +---------+ HTML page (with a hidden state name) .-------------------| client |<--------------. | .-----------------| | | | | previous state +---------+ | | | name | | | | client side ............................................................................ | | | server side | | | | | .-------------------. | | | | previous state | | V V V | | +---------------+ +---------------+ +--------------+ | compute state | | server's disk | | compute page | +---------------+ +---------------+ +--------------+ ^ | | ^ ^ ^ ^ | | | | | | | | | `--------------------+--------------------' | | | new state | | read | `------------------------+--------------------' write | new state name update V +-----------+ | data base | +-----------+ When the client begins a session, there is no previous state. In this case, a default 'initial state' is used instead. It is important that computing the HTML page is performed only from the new state (and its name) not using the data base. This is because the data base may have been updated by another client since the computation of the new state. Doing another way could create an inconsistency between the page sent to the client and its current state recorded on the server's disk. In this file, all the above stuff is defined. You just have to provide the function for computing a new state (compute state) and the function for computing the page (compute page) from the new state. You don't have to worry about state names, saving and retrieving states and the like. *** (1.2) What web pages are made of. What the client can see in his browser's window may be called a 'page'. Within a page, we have several sorts of components: - 'local' components, i.e. all components which do not open a connection, like texts, images, etc... possibly using JavaScript programmation, - 'actioners', which, when clicked upon, open a connection with our web site; they may appear as links or buttons, etc... - 'foreign links', which when clicked upon, open a connection with another web site. Of course, what an actioner does is just ask our web site to perform an action. To that end, the actioner essentially sends the name of the action to be performed. However, it may be necessary to provide additional informations which may be seen as 'operands' of the action. In order to attach operands to an action, HTML provides the notion of 'form'. Indeed, a form contains essentially a set of input fields into which the client may put values for the required operands of the action, and a submit button, which is the actioner itself. Notice that a single form may contain several submit buttons, which simply means that there are several distincts actions taking the same set of operands. Restrictions must be put on the use of all theses gadgets. Indeed, for example, putting a form within another form is meaningless. HTML does not explicitly forbid such things, but the client's browser may be seriously disturbed. In this file, we propose an interface to the HTML language, which forbids such meaningless things, simply by imposing a strict typing of HTML concepts. Each web site may be accessible through two communication channels: - a non secured channel (HTTP), - a secured channel (HTTPS). Nevertheless, the whole thing should be considered as a single web site. For example, you may have a secured page, obtained through HTTPS, containing public images obtained through HTTP. An actioner in a non secured page may open a secured connection, and conversely. Summarizing, a web page is made of local elements, foreign links and actioners. Actioners receive operands from forms, and they also choose to communicate through the non secured or through the secured channel. +-------------------+ | page | | | +---------------+ | +--------------+ | | next page | | | form | | | (non secured) | | | +----------+ | | HTTP | | | | | actioner |---------------------------->| | | | +----------+ | | +---------------+ | | | | | | +----------+ | | +---------------+ | | | actioner |---------------------------->| next page | | | +----------+ | | HTTPS | (secured) | | | | | | | | +--------------+ | | | | | +---------------+ | | +-------------------+ *** (1.3) Actions. The client opens a new connection with our web site whenever he clicks on an actioner. The result is that a request is sent, essentially made of a list of 'web arguments'. Each web argument is a pair (name,value). One of these web arguments, the 'action' web argument (whose name is "a"), determines the action to be performed. The other web arguments (not including "s") are the operands for this action. Hence, the 'compute state' box in the picture above, splits naturally into as many sub-boxes as there are actions. For this reason, we define the following type for representing actions (where '$State' is the type representing session informations): public type ActionTicket:... // a definitely opaque type public type Web_Action($State): http_action (String name, // name of action (ActionTicket, $State) -> Bool allow, // true if action allowed (ActionTicket ticket, HTTP_Info http_info, List(Web_arg) web_args, // actually only 'operands' web arguments $State state) -> $State do_it), https_action (String name, // name of action (ActionTicket, $State) -> Bool allow, // true if action allowed (ActionTicket ticket, HTTP_Info http_info, List(Web_arg) web_args, // actually only 'operands' web arguments $State state) -> $State do_it), http_https_action (String name, (ActionTicket, $State) -> Bool allow, (ActionTicket ticket, HTTP_Info http_info, List(Web_arg) web_args, $State state) -> $State do_it). 'HTTP actions' are executed only under HTTP, and 'HTTPS actions' are executed only under HTTPS. Each action has a name, which is used to identify the action. Each action also has a function 'allow' whose job is to verify that the action is allowed in the current state, and a function 'do_it' for performing the action. The function 'do_it' receives a lot of informations: - 'HTTP informations': - the IP address of the client, - the URI requested by the client (after redirection), - the list of HTTP headers generated by the client's browser, - the list of web arguments sent by the client (except "s" and "a"), - the previous state (or the 'initial' or 'ticket expired' state if no previous state can be found). In most cases, HTTP informations are not used. This is the reason why they are gathered for simplicity into a unique datum of type 'HTTP_Info'. For your convenience, we introduce the following simpler variants: public define Web_Action($State) http_action ( String name, $State -> Bool allow, (List(Web_arg),$State) -> $State do_it ) = http_action(name, (ActionTicket t, $State s) |-> allow(s), (ActionTicket t, HTTP_Info h, List(Web_arg) l, $State s) |-> do_it(l,s)). public define Web_Action($State) https_action ( String name, $State -> Bool allow, (List(Web_arg),$State) -> $State do_it ) = https_action(name, (ActionTicket t, $State s) |-> allow(s), (ActionTicket t, HTTP_Info h, List(Web_arg) l, $State s) |-> do_it(l,s)). public define Web_Action($State) http_https_action ( String name, $State -> Bool allow, (List(Web_arg),$State) -> $State do_it ) = http_https_action(name, (ActionTicket t, $State s) |-> allow(s), (ActionTicket t, HTTP_Info h, List(Web_arg) l, $State s) |-> do_it(l,s)). When you define your web site, you must provide the list of all the actions of the site. When a new state has been computed, a graphical representation of this state must be sent to the client. To that end, you must provide a function (named below 'compute_page') of type: $State -> HTML_Page where the type 'HTML_Page' (defined below in this file) abstractly represents HTML pages. public type HTML_Page:... It should be clear that states and pages are deeply linked together. Indeed, we really understand the page shown to the client as a representation of the current state of the conversation between the client and the web site. *** (1.4) States. Now, we explain how to define the type (say 'State') to be used as an instance of the type parameter '$State'. Each state determines a page (since 'compute_page' computes a page from a state). However, some components of the state may be independant of the page. It may be the case for example for the indication of the natural language used by the client. Hence, a state should be made of (at least) two parts: - informations which are the same for all pages, - informations which are particular to each page. For example, you could define: type Page: // one alternative per page, with particular informations login(...), // in the components main_page(...), ...etc... Now, the type 'State' could be defined as follows: type State: state(Language, // informations valid for all pages ..., Page). // informations particular to a page However, if you are making a secured web site within which clients should be identified (by id and password), it may be a good idea to have two sorts of states, one for non identified clients and one for identified clients. In this case, define the type 'State' as follows (this is just a suggestion): type State: non_identified(Language), identified(String id, Language, Page). When a request arrives, check if the previous state is 'identified(...)' or 'non_identified(...)', and don't provide access to certain pages to non identified clients. This is required for security. Some more words on security. If your site needs to identify clients, define the initial state as 'non_identified(...)'. Construct a 'login' page, and check the id and password of the client. If the id and password are correct, then change the state of the client to 'identified(...)'. No other action should be able to do that. Now, be confident that clients cannot forge states. The only information they have is the name of a state, not the state itself which is never sent over the network, but only stored on the server's disk. The name of the state is constructed using strong cryptographical methods (sha1). If everything (since the 'login' page) is performed under HTTPS, even state names cannot be seen by a third party. So, if the system retrieves a previous state of the form 'identified(...)', you can be confident that your client is well identified, and you can send him confidential informations. States have a limited life time. It may happen that a client clicks on a button at a time its state is out of date. In this case, this system considers that the new state is a special state named 'ticket expired'. You must provide a function producing this state when you describe your web site. The page corresponding to this state must just inform the client that he/she waited a too long time before clicking on a button, and has to restart (a new conversation) from the begining. *** (2) Carrying on. *** (2.1) Describing your web sites. Before you may start your web site, you must describe it, i.e. produce a datum of the opaque type 'Web_Site'. public type Web_Site:... Producing such a datum may be performed by: public define Web_Site make_web_site_description ( String common_name, // for example: "www.our-business.com" String site_directory, // where 'public' and other directories are // located (should NOT end with '/') ActionTicket -> One init, (ActionTicket, HTTP_Info) -> $State initial_state, (ActionTicket, $State expired, HTTP_Info, List(Web_arg), Bool is_https) -> $State ticket_expired_state, (ActionTicket, HTTP_Info, List(Web_arg), Bool is_https) -> $State ticket_lost_state, List(Web_Action($State)) actions, $State -> HTML_Page compute_page, Int32 timeout, // seconds (todo: minutes) List(Redirection) redirections, List(String) journal_extensions, List(String) journal_headers, String authorization_secret, List(MIME) known_mime_types, (String action_name, List(Web_arg) args)-> One before_send_file ). Explanations: 'common_name' is the name of the site (the name the browser must send as the value of the 'Host' HTTP header in order to access the site). Such a name generally looks like this: www.somewhere.com If you are using HTTPS, you also have an 'X.509 SSL server certificate'. The name of the site must be exactly the same as the name on the certificate (which is precisely called the 'common name' in the X.509 jargon). If the two names do not match, the site will still work, but the transaction will not be transparent to the client. His browser will complain that the name of the certificate does not match the name of the site, and he will have to accept the certificate manually. 'site_directory' is the absolute path to the directory where the files needed by the site are located. Usually this directory looks like: my_anubis/web_sites/www.somewhere.com However, this information is not computed from 'common_name', so that you can change the common name (for example temporarily, for networking reasons) without loosing access to the files. 'ticket_expired_state(expired_state,http_info,lwa,is_https)' must produce the state whose graphical representation is a page explaining to the user that its 'ticket' (or 'session information') has expired, and that he/she must close all popup windows and start a new session. The arguments of the function contain the previous (expired) state and all current informations concerning the user. This arguments may be useful for example for producing the expiration message in the language chosen by the user. You can also (and this may be much smarter) send a 'ticket prolongation page' (including a new login for example), and resume the same conversation, since you have all the pertinent informations at hand. In the case the ticket is definitely lost, the second fonction 'ticket_lost_state' is used. Notice that despite the fact that the parameter $State is involved in the arguments of the above function, the type 'Web_Site' does not depend on this parameter. This allows to produce lists of web site descriptions, where each description may be constructed with a different instance of $State. This is required because distinct sites must have distinct types of session informations. This is made possible by the fact that the type is obscure, and the constructor replaced by a function which assembles 'ticket_expired_state', ticket_lost_state', 'actions' and 'compute_page' into a single entity not depending on $State. You should have a look to the private part of this file if you want more precisions about this programming technique. 'before_send_file' is a function which is executed just before the HTTP server sends a file. It gets an action name and the web arguments received with the request for that file. Notice that this action name and these web arguments may be put into a 'private_download' element, and will come back to the server at the time of the download. *** (2.2) Directories on the server's disk. The description of you site contains the name of the directory within which the required files are located. This may be for example: my_anubis/web_sites/www.our-business.com/ This is called the 'site directory' (for the given site). Within the site directory, the following directories are created by this program: states public journal private_download upload_temporary The directory 'states' is used for storing states (session informations). Out of date states are automatically removed after some time. The tree rooted at 'public' contains files that the server is allowed to send to the clients. For security reasons, the server never sends a file which is not within the tree whose root is this 'public' directory (except for the 'private download' mecanism; see 'web/multihost_http_server.anubis'). Also, the MIME type (see 'web/mime.anubis') must have been recognized before the file may be sent. The directory 'journal' contains the jounal files. The roles of the remaining directories 'private_download' and 'upload_temporary' is explained in 'multihost_http_server.anubis', where you will also find further informations on 'public' and 'journal'. *** (2.3) Starting your web sites. When you have described all your web sites (you may want to have several web sites, and they are distinguished by their 'common name'), you may start them all together using 'start_web_sites' below. This function returns a result of the following type: public type Start_Web_Sites_Result: cannot_bind_to_port(Int32), cannot_bind_to_port(Int32,Int32), ok(Server http_server, Server https_server). Indeed, it may happen that the system cannot bind (begin to listen) to one of the two ports (or to both). The main reason is that another server is already listening on that port. Another reason may be that 'anbexec' has not been correctly installed, i.e. that the 's' bit has not been set for 'user' and 'group' (there is not such problem under Windows). Also notice that the Linux kernel may need a rather long time (up to several minutes) before liberating a listening port. Now, if the system can bind to the two ports, the pair of the two servers is returned. Two tools are useful for manipulating servers: shutdown of type Server -> One is_down of type Server -> Bool They are defined in 'predefined.anubis' (together with the type 'Server'). public define Start_Web_Sites_Result start_web_sites ( Int32 ip_address, // the IP address shared by the web sites Int32 http_port, // usually: 80 Int32 https_port, // usually: 443 String ssl_certificate_common_name, List(Web_Site) web_sites // web sites to be started ). 'ip_address' is the IP address on which the two servers listen. If you put 0, the servers listen on all the IP addresses of the machine. This may be useful if the machine has several network interfaces. 'ssl_certificate_common_name' is the common name of the SSL certificate that 'anbexec' loads when it starts. One instance of 'anbexec' cannot handle more than one SSL server certificate. This is due to a problem of conception of SSL itself. See the book 'SSL and TLS' by Eric Rescorla (at Addison Wesley) for more explanations. Notice that the number of servers is always 2, regardless of the number of web sites you are starting. *** (3) The HTML interface. We propose an interface to dynamic HTML. Dynamic HTML includes HTML, and a combination of CSS (Cascading Style Sheet) and JavaScript techniques for making HTML elements more reactive and attractive on the client side. *** (3.1) Types used by the HTML interface. For easy reference, we gather below the definitions of all the types used by the HTML interface, and we comment them immediately. public type Text_Option: size(Int32), // size of character font to use font(String), // name of character font to use (such as "helvetica",...) color(RGB), // color to be used for characters italic, oblique, small_capitals, bold, underlined, left_justified, right_justified, justified, // justified on both sides line_through. A list of 'Text_Option' must be given with each text you want to put in your page. public type Table_Option: background_color(RGB), // applied to all cells in the table background_image(String url), border(Int32 width_of_outer_edge, // if not present, all values are 0 Int32 width_of_top_of_relief, Int32 width_of_inner_edge, RGB border_color), width(Int32), // sets a minimal width for the table percentage_width(Int32). public define Table_Option nude = border(0,0,0,rgb(0,0,0)). A list of 'Table_Option' must be given with each table. public type BackgroundOption: repeat, // repeat the background in both directions repeat_horizontal, // repeat the background only horizontally repeat_vertical, // repeat the background only verticall no_repeat. // don't repeat the background public type Cell_Option: left, // put the content of the cell on the left h_center, // center the content of the cell horizontally right, // put the content of the cell on the right top, // put the content of the cell upwards v_center, // center the content of tye cell vertically, bottom, // put the content of the cell downwards base_line, // align the content vertically according to base lines background_color(RGB), background_image(String url, BackgroundOption), width(Int32), // sets a minimal width for the cell percentage_width(Int32), height(Int32), // sets a minimal height for the cell columns(Int32), // lets the cell span over several columns rows(Int32), // lets the cell span over several rows nowrap. // do not allow text wrapping within the cell A list of 'Cell_Option' must be given with each cell and each row in a table. Options given with a row apply to all the cells in the row, but are superseded by options given with cells, which apply only to the cell they are given with. public type HTML_Cell($T): cell(List(Cell_Option) options, $T content). The parameter $T is later instantiated either to 'HTML_In_Form' or to 'HTML_Off_Form', depending on where you put your table (within a form or not within a form). For your convenience, we define the following particular case: public define HTML_Cell($T) cell ( $T content ) = cell([],content). public type HTML_Row($T): row(List(Cell_Option) options, List(HTML_Cell($T)) cells). Same remark as for 'HTML_Cell($T)'. We define several convenience functions: public define HTML_Row($T) row ( List(HTML_Cell($T)) cells ) = row([],cells). public define HTML_Row($T) row ( HTML_Cell($T) cell ) = row([],[cell]). public type Actioner_Connection: same, // use same type of connection as current page http, // use non secured connection https. // use secured connection public type Other_Window_Option: resizable, // the new window may be resized by the client scrollbars, // the new window has scrollbars width(Int32), // the new window has the specified width height(Int32). // the new window has the specified height public type Actioner_Target: same, other(String window_name, List(Other_Window_Option)). public type Actioner_Aspect: link (List(Text_Option),String text), // hypertext link button (String url_off, String url_on), // rollover button button (String text, Int32 width, RGB color), // rollover button immediate_selector (String name, Int32 size, List(String) choices). public define Actioner_Aspect link ( String text ) = link([],text). Actioners are explained in details below. public type TextAreaOption: disabled. public type HTML_In_Form: literal (Printable_tree), sequence (List(HTML_In_Form) items), text (List(Text_Option), String the_text), preformated (List(Text_Option), String), paragraph (List(Text_Option), String the_text), image (String url), table (List(Table_Option), List(HTML_Row(HTML_In_Form))), center (HTML_In_Form), mail_to (String email, HTML_In_Form element), scroller (Int32 width, Int32 height, Int32 content_width, Int32 content_height, HTML_In_Form content), actioner (Actioner_Connection, Actioner_Target, Actioner_Aspect, String action_name, List((String,String)) extra_ops), local_popup (Actioner_Aspect, HTML_In_Form content, Int32 x, Int32 y, String title, RGB color, Int32 width), foreign_link (String url, String name), private_download (String abs_path, String name, String extra_ext, Maybe((String,List((String,String)))) action), text_input (String name, String init, Int32 width), password_input (String name, Int32 width), text_area (List(TextAreaOption), String name, String init, Int32 width, Int32 height), file_upload (String name, Int32 width), selector (String name, Int32 size, List(String) choices), selector (String name, Int32 size, List(String) choices, String selected), radio_button (String name, String value, Bool checked), check_box (String name, Bool checked). 'HTML_In_Form' defines all the elements you may put within a form. We define a convenience function: public define HTML_In_Form text_area ( String name, String init, Int32 width, Int32 height ) = text_area([],name,init,width,height). public define HTML_In_Form table ( List(HTML_Row(HTML_In_Form)) rows ) = table([],rows). public define HTML_In_Form private_download ( String abs_path, String name, String extra_ext ) = private_download(abs_path,name,extra_ext,failure). public define HTML_In_Form private_download ( String abs_path, String name, String extra_ext, String action_name, List((String,String)) args ) = private_download(abs_path,name,extra_ext,success((action_name,args))). public type HTML_Off_Form: literal (Printable_tree), sequence (List(HTML_Off_Form) items), text (List(Text_Option), String the_text), preformated (List(Text_Option), String), paragraph (List(Text_Option), String the_text), image (String url), table (List(Table_Option), List(HTML_Row(HTML_Off_Form))), center (HTML_Off_Form), mail_to (String email, HTML_Off_Form element), scroller (Int32 width, Int32 height, Int32 content_width, Int32 content_height, HTML_Off_Form content), fixed_size (Int32 width, Int32 height, HTML_Off_Form content), fixed_size (Int32 width, Int32 height, String name_of_HTML_file), actioner (Actioner_Connection, Actioner_Target, Actioner_Aspect, String action_name, List((String,String)) extra_ops), actioner (Actioner_Connection, Actioner_Target, Actioner_Aspect, String action_name, List((String,String)) extra_ops, String form_name), local_popup (Actioner_Aspect, HTML_Off_Form content, Int32 x, Int32 y, String title, RGB color, Int32 width), foreign_link (String url, String name), private_download (String abs_path, String name, String extra_ext, Maybe((String,List((String,String)))) action), form (String form_name, HTML_In_Form content). 'HTML_Off_Form' defines all the elements you may put outside any form. public define HTML_Off_Form table ( List(HTML_Row(HTML_Off_Form)) rows ) = table([],rows). We add two convenience functions for 'row'. The reason why we add two functions, one for 'HTML_In_Form' and one for 'HTML_Off_Form', is that adding a schema with an arbitrary '$T' creates too many ambiguities. This is due to the fact that, if we do so, the arguments of the function do not refer to any of the types defined here. public define HTML_Row(HTML_In_Form) row ( HTML_In_Form content ) = row([],[cell([],content)]). public define HTML_Row(HTML_Off_Form) row ( HTML_Off_Form content ) = row([],[cell([],content)]). public define HTML_Off_Form private_download ( String abs_path, String name, String extra_ext ) = private_download(abs_path,name,extra_ext,failure). public define HTML_Off_Form private_download ( String abs_path, String name, String extra_ext, String action_name, List((String,String)) args ) = private_download(abs_path,name,extra_ext,success((action_name,args))). Notice that the two types have alternatives in common (same name, same arguments types, up to the value of the parameter $T), which correspond to elements which may be put anywhere in the page. public type HTML_Meta: keywords (List(String)), refresh (Actioner_Connection connection, Actioner_Target target, String action_name, Int32 delay), // in seconds meta (String name, String content), http_equiv (String name, String content), generic_meta (List((String,String))). Meta tags are put in the 'head' of the HTML page. public type Body_Option: background_color (RGB), background_image (String url). public type HTML_Body: body(List(Body_Option) options, HTML_Off_Form content). public type HTML_Page: html_page(String title, List(HTML_Meta) meta_tags, HTML_Body body). 'HTML_Page' represents the final product of the construction of a web page. *** (3.2) ``in form'' versus ``off form''. There is a variety of HTML elements: texts, buttons, links, forms, inputs, etc... Some of them may have a content, which is yet another HTML element (or several). Hence, it is meaningful to say that an element is 'within' another one. Now, putting any element within any other one may be meaningless. For example, a input element must be put within a form (otherwise, it is useless), and a from within another form has no precise meaning. Actually, the main criterium is ``within a form or not within a form''. So, HTML elements in a given page are separated into two categories: those who are within a form, and the others. Nevertheless, there are elements which may belong to both categories, like images and texts. We want to make use of the strong typing mecanism of Anubis in order to forbid non meaningful placement of elements. The type 'HTML_In_Form' defines elements to be put within forms. Similarly, 'HTML_Off_Form' defines elements not to be put within forms. Both types are recursive, and 'HTML_Off_Form' refers to 'HTML_In_Form' (via the 'form' alternative, of course), but the two types are not cross recursive. This is the reason why it is impossible to put a form within a form. In order to construct a web page, you essentially have to produce a datum of type 'HTML_Off_Form' (maybe containing data of type 'HTML_In_Form'). In practice, you don't have to worry so much about these two types, because elements which may be put anywhere are constructed for both types by functions with the same name and the same arguments. Hence, for both types, you just write the same thing. You are warned by the compiler only when you try to put an element at a place it is not allowed. *** (3.3) Defining your own style. We provide generic tools for constructing HTML elements. However, your web site needs to have a ``style''. To that end, you need to write down a set of ``styling functions'', using the tools defined here. These styling functions allow the introduction of your colors and other visual characteristics into the constructed elements once and for all. For example, you may want all your texts to be rendered in the ``Helvetica'' font, in size 14 and using some 'text_color'. You may write something like this: define RGB text_color = rgb(10,40,40). define HTML_Off_Form text ( String the_text ) = text([font("helvetica"),size(14),color(text_color)], the_text). (and the same one for type 'HTML_In_Form') so that in order to put a piece of text in a page, you just write: text("... some text ...") and you don't have to provide the font, size and color for each text. If you want to have several styles of text presentation, you just write several sets of such convenience functions. This also suggests a trick. You may want for example different colors for 'in form' texts and 'off form' texts. This may be achieved automatically by defining two functions as above, with the same name and same argument type, but returning either a 'HTML_In_Form' or a 'HTML_Off_Form'. If this preliminary work is well done, you will not waste your time later when you concentrate on the actual informational content of your pages. This general principle should be applied to all sorts of elements. This is the best thing to do in order to separate the functions defining the visual style from the functions defining the informational content itself, so that changing the style without changing the content becomes easy. This is also the best way for having a clean and easily readable source for your web site. *** (3.4) Actioners and forms. We have gathered several notions from HTML into that of an 'actioner'. An actioner is an HTML element which opens a connection to our server when clicked upon. Actioners may have different visual aspects. They may look like hypertext links or like buttons (rollovers), or even like selectors (with immediate action). In any case, their behavior is the same: they open a connection to our server, and send a set of 'web arguments', i.e. pairs 'name=value'. Among these web arguments, one of them denotes the action to be performed, and the others should be considered as operands for this action. Actually, the precise behavior of the actioner has several variants. The connection with the server may be secured (HTTPS) or non secured (HTTP). See the type 'Actioner_Connection' above. You must also choose where the answer must be rendered. This may be in the same window or in another window (or frame). If it is in another window, the name of that window must be given. If the window does not exist, the browser will create it. Optionally, you may give the dimensions of the new window and other characteristics. See the type 'Actioner_Target' above. The actioner also has a visual aspect. See the type 'Actioner_Aspect' above. In the case of a rollover button, you provide the URLs of two images (of the same size) representing the button: url_off: to be used when the mouse is not over the button, url_on: to be used when the mouse is over the button. You can also create rollover buttons without creating images. Just use the second alternative named 'button'. The server creates the images automatically. The purpose of forms is just to give operands to actioners. If the actioner is placed within a form, all the input elements which are within this form provide operands to the actioner (except sometimes when they are not set by the client). If it is not put within a form, the actioner gets no operand, except if the name of a form is explicitly given, in which case the actioner gets all the inputs from that form as operands. Furthermore, you may want to give extra operands to the actioner. This may be useful for separating families of actioners with the same action name. Extra operands 'name=value' must be given in the form of pairs '(name,value)'. Notice that the name of a form may be used by an actioner which is off the form, so as to get the operands provided by this form. Also notice that several actioners may refer to the same form, being either in the form, or referring to the form from the outside. These actioners simply get the same set of operands, even if they correspond to distinct actions. Input elements may be put only within a form. *** (3.5) Local popup. This element looks like a link or a rollover button. When this button is clicked upon, a 'popup window' appears. Actually, this popup window is just a layer in the same HTML page, which becomes suddenly visible. It is realized with a '
' HTML tag. In particular, clicking on the button does not open any connection. This is why it is called 'local'. The arguments have the following roles: Actioner_Aspect aspect of the button (same semantics as for actioners) content content of the popup window x, y, position of the popup window on the HTML page (not relative to the button but to the page itself) title title of the popup window color color of the title bar and close button in the popup window. A lightened version of this color is used for the background of the popup window. width width of the title bar --- That's all for the public part ! -------------------------------------------------- ----------------------------------- Table of Contents --------------------------------- *** [1] States. *** [1.1] Saving and retrieving states. *** [1.2] Deleting out of date states. *** [2] Tools. *** [2.1] Directories. *** [2.2] Secondary documents. *** [3] Managing web arguments. *** [3.1] Prefixing web arguments names. *** [3.2] Separating web arguments. *** [3.3] Applying an action. *** [4] Web site descriptions and the 'awp handlers'. *** [4.1] The type 'Web_Site'. *** [4.2] Making a web site description. *** [4.3] Starting the servers. *** [5] HTML Formating. *** [5.1] The type 'HTML_Any($T)'. *** [5.2] Formating a color. *** [5.3] Creating buttons. *** [5.4] Formating an actioner. *** [5.5] Formating a private download link. *** [5.6] Formating rows and cells in a table. *** [5.7] Formating elements which may be put anywhere. *** [5.8] Formating 'in form' elements. *** [5.9] Formating 'off form' elements. *** [5.10] Formating meta-tags. --------------------------------------------------------------------------------------- *** [1] States. We have to define functions for saving a state, retrieving a state, deleting out of date states. We need one such function per web site. The types of the first two functions depend on the parameter $State. This is not the case of the third one. The fact that the instance of $State is variable from one web sites to the other implies rather subtle manipulations using full functionality. *** [1.1] Saving and retrieving states. Each state is saved into a file on the server's disk (in the directory represented by the symbol 'state_directory', which is 'my_anubis/web_sites/common_name/states'). The state is saved together with a time stamp whose value is obtained by adding the current time to the given timeout for states. The state receives a name obtained by hashing (using sha1) the content of the file itself, and then encoding the hash with 'web_arg_encode'. The name of the file into which the state is saved is the concatenation of "s" and the name of the state. read web/web_arg_encode.anubis The tool below constructs the function which is able to save a state on the server's disk. define ($State s) -> String // the function constructed returns the name of the state make_save_state_function ( Int32 timeout, String state_directory ) = ($State s) |-> with time_stamp = now+timeout, to_be_saved = (time_stamp,s), state_name = web_arg_encode(sha1(to_be_saved)), if save(to_be_saved,state_directory+"/s"+state_name) is ok then state_name else (print("Cannot create state file in '"+state_directory+"'.\n"); ""). When a request arrives, we need to retrieve the previous state from the server's disk. We receive the name of that state. If the state is out of date, the state file is kept 3 days, and then deleted. type PreviousState($State): not_found, // cannot retrieve the previous state out_of_date($State), // the previous state is out of date still_valid($State). // the previous state is still valid define (String state_name) -> PreviousState($State) make_retrieve_state_function ( String state_directory ) = (String state_name) |-> with file_path = state_directory+"/s"+state_name, if (RetrieveResult((Int32,$State)))retrieve(file_path) is ok(d) then ( if d is (time_stamp,s) then if time_stamp < now then ( forget(remove(file_path)); out_of_date(s) ) else still_valid(s) // state has been successfully retrieved ) else not_found. *** [1.2] Deleting out of date states. We also need to delete states which are out of date and which will never be deleted by the above method. This may be performed by a machine doing this periodically (say once per states life time period). define (List(String) file_names) -> One make_delete_out_of_date_states_function ( Maybe($State) dummy, String state_directory ) = (List(String) file_names) |-df-> if file_names is { [ ] then unique, [h . t] then with file_path = state_directory+"/"+h, if (RetrieveResult((Int32,$State)))retrieve(file_path) is ok(d) then ( if d is (time_stamp,data) then if time_stamp < now then (forget(remove(file_path)); df(t)) else df(t) ) else (forget(remove(file_path)); df(t)) }. The 'labelled arrow' |-df-> is documented in 'documentation/en/anubis_doc.txt'. Note: The argument 'dummy' (of type $State) is not used in the body of the function (hence its name). Nevertheless, it is required. Indeed, the Anubis compiler does not accept a parameter in the body of a function (here the parameter is required by the use of 'retrieve') if this parameter does not appear in the type of the function. This is because this would create ambiguities that no explicit typing may ever resolve. If you put a double slash in front of the declaration of 'dummy' above, and if you compile this file, you will get a message like this one: Error in 'making_a_web_site.anubis', line 805, column 7: A definition may not contain parameters which are not present in the declaration part (hidden parameters): $State The type of the function constructed by 'make_delete_out_of_date_states_function' is independant of the parameter $State. This is important because this allows to create the list of such functions for all web sites. From this list, it is possible to call the functions one after the other, so deleting out of date states for all web sites. Actually, the next function receives a list of pairs (state_directory,function), one for each web site. define One delete_out_of_date_states // for all web sites ( List((String, List(String) -> One)) directories_and_functions ) = if directories_and_functions is { [ ] then unique, [h . t] then if h is (state_directory,function) then function(directory_list(state_directory,"s*")); delete_out_of_date_states(t) }. The above function must be called periodically in a separate virtual machine. The period we have choosen is (rather logically) the life time of states itself. This may be achieved by an 'infinite' loop, using a 'sleep(timeout)'. However, the loop must not be really infinite, because the servers may be shutdown. Hence, our loop must test (rather frequently; say every second) if the servers are down. If they are, the loop must be exited. define One delete_states_loop ( List((String,List(String) -> One)) directories_and_functions, Int32 timeout, Int32 next_time, Server http_server, Server https_server ) = if (is_down(http_server) & is_down(https_server)) then unique else if now > next_time then ( delete_out_of_date_states(directories_and_functions); delete_states_loop(directories_and_functions, timeout, now+timeout, http_server, https_server) ) else ( sleep(1000); // sleep just one second and try again delete_states_loop(directories_and_functions, timeout, next_time, http_server, https_server) ). The above loop must be run in a separate virtual machine. This will be done just after the two servers are started. *** [2] Tools. *** [2.1] Directories. We need a tool for creating directories (if needed). (This tool has been moved to 'tools/basis.anubis'). *** [2.2] Secondary documents. Some HTML elements (like '', '') cannot receive their content directly from the current document, but only through an URL. For this reason, we implement a mecanism for creating secondary documents on the fly. To that end we use the 'private download' mecanism. A secondary document is formated by the same functions as the main document itself. The next function takes an 'off form' element, creates the file containing the secondary document in HTML format, and returns the URL at which the document will be available. define String create_secondary_document ( String sd, // site directory String as, // authorization_secret String sn, // state name $T -> Printable_tree format_element, $T content, Int32 width ) = with private_download_directory = sd+"/private_download", hash = web_arg_encode(sha1(content)), file_content = (Printable_tree) ["
", format_element(content), "
" ], file_name = "sd"+hash+".html", file_path = private_download_directory+"/"+file_name, if write_to_file(file_path,file_content) is { cannot_open_file then print("Cannot open file '"+file_path+"'.\n"); "", write_error(n) then print("Error writing file '"+file_path+"'.\n"); "", ok then file_name+"?zauth="+ make_authorization(sd,as,private_download_directory+"/"+file_name) }. *** [2.3] Generating unique ids. In order to uniquely name object for JavaScript we generate unique ids from a counter. define Int32 new_idnum ( Var(Int32) ic_v // 'idnum' counter variable ) = protect with result = *ic_v+1, ic_v <- result; result. *** [3] Managing web arguments. Web arguments are those pairs 'name=value' which are transmitted through the HTTP protocol. We need precise naming conventions for these web arguments. *** [3.1] Prefixing web arguments names. We want to assign different roles to web arguments, and we also want to be able to recognize its role directly from the name of a web argument. The name "s" is reserved for the web argument whose value is the name of the current state. The name "a" is reserved for the web argument whose value is the name of the action to be performed. Other web arguments receive arbitrary names, and in order to avoid clashes, these names are prefixed by: "p" for names of password inputs, "o" for other web arguments The reason why password input names have a distinct prefix is that this allows the HTTP server to hide the passwords on the console of the server and in the journal. *** [3.2] Separating web arguments. When a new request arrives, we need to separate the web arguments, that is to say: - find the value of "s", and recover the corresponding state, - find the value of "a", which is the name of the action to be performed, - get the list of all the remaining web arguments (operands of the action). We must also determine if the previous state may be recovered. If it is not the case (either because the previous state name is invalid, or the previous state is out of date), we must check if there is an action name. Indeed, the presence of an action name indicates that the user has clicked on one of our buttons or links. If on the contrary there is no action name the user has just entered our address in his browser. In this last case, we must send the first page of our site (maybe a 'login' page), but if there is an action, we must send a page just saying that the session ticket has expired. If the previous state is recovered and there is no action, the new state is the same as the previous state. The result of the separation of the web arguments is of type: type Separated_Web_Args($State): swa(PreviousState($State) previous_state, Maybe(String) action_name, List(Web_arg) operands). The next function constructs the function which separates the web arguments. define (List(Web_arg) lwa) -> Separated_Web_Args($State) make_separate_web_args_function ( String state_directory, String -> PreviousState($State) retrieve_state ) = (List(Web_arg) lwa) |-swaf-> if lwa is { [ ] then // // no web arg found => no previous state and no action // swa(not_found,failure,[]), [wa_1 . wa_others] then // // at least one web arg => // separate other web args, and insert the first one as needed // if (Separated_Web_Args($State))swaf(wa_others) is { swa(ps1, // possible previous state an1, // maybe an action name op1) // operands so far then if wa_1 is { web_arg(n,v) then with prefix = substr(n,0,1), if prefix = "s" then swa(retrieve_state(v),an1,op1) else if prefix = "a" then swa(ps1,success(v),op1) else if prefix = "t" then swa(ps1,an1,[web_arg("&target",v) . op1]) else if prefix = "p" then swa(ps1,an1,[web_arg(substr(n,1,length(n)-1),v) . op1]) else if prefix = "o" then swa(ps1,an1,[web_arg(substr(n,1,length(n)-1),v) . op1]) else swa(ps1,an1,op1), upload(n,v,t) then swa(ps1,an1,[upload(substr(n,1,length(n)-1),v,t) . op1]) }} }. *** [3.3] Applying an action. When the web arguments are separated (and their names cleaned up from prefixes), we may apply the action to the operands and the current state. We search for the action to be applied in the list of actions. If no action is found, the new state is the same as the previous state. Also, we deny the application of an HTTP action if the request arrives through the HTTPS channel and conversely. public type ActionTicket: hf87jdh23sqPjfYUGF7865GHTjf7. Preferably, do never use this alternative name explicitely. See 'tools/sdbms.anubis' for explanations. define ($State previous, String action_name, HTTP_Info http_info, List(Web_arg) lwa, Bool is_https) -> $State make_apply_action_function ( String common_name, List(Web_Action($State)) actions ) = with f = ($State previous, String action_name, HTTP_Info http_info, List(Web_arg) lwa, Bool is_https, List(Web_Action($State)) actions) |-f-> if actions is { [ ] then (print(common_name+": action '"+action_name+ "' not found.\n"); previous), [ac1 . others] then if ac1 is { http_action(an,allow,do_it) then if an = action_name then if is_https then (print(common_name+": HTTP action '"+an+ "' called through HTTPS (denied).\n"); previous) else if allow(hf87jdh23sqPjfYUGF7865GHTjf7,previous) then do_it(hf87jdh23sqPjfYUGF7865GHTjf7,http_info,lwa,previous) else previous else f(previous,action_name,http_info,lwa,is_https,others), https_action(an,allow,do_it) then if an = action_name then if is_https then if allow(hf87jdh23sqPjfYUGF7865GHTjf7,previous) then do_it(hf87jdh23sqPjfYUGF7865GHTjf7,http_info,lwa,previous) else previous else (print(common_name+": HTTPS action '"+an+ "' called through HTTP (denied).\n"); previous) else f(previous,action_name,http_info,lwa,is_https,others), http_https_action(an,allow,do_it) then if an = action_name then if allow(hf87jdh23sqPjfYUGF7865GHTjf7,previous) then do_it(hf87jdh23sqPjfYUGF7865GHTjf7,http_info,lwa,previous) else previous else f(previous,action_name,http_info,lwa,is_https,others), } }, ($State previous, String action_name, HTTP_Info http_info, List(Web_arg) lwa, Bool is_https) |-> f(previous,action_name,http_info,lwa,is_https,actions). *** [4] Web site descriptions and the 'awp handlers'. *** [4.1] The type 'Web_Site'. The type 'Web_Site_Description' is defined in 'web/common.anubis'. We need another one, because, we have some extra informations to record for each site. public type Web_Site: web_site((Int32,Int32) -> Web_Site_Description description, List(String) -> One delete_out_of_date). *** [4.2] Making a web site description. Below is the function which creates a web site description. It first creates (if needed) the directories for the site, then constructs the tool functions for the site, and the site handler. Finally, it constructs the web site description. We gather common (constant) informations in the following type: type CommonInfo: info(String common_name, Int32 http_port, Int32 https_port, String site_directory, String authorization_secret, SystemFont font). We need a forward declaration. public define Printable_tree format ( CommonInfo cinfo, String state_name, HTML_Page page, Bool is_https ). define Printable_tree top_redirection_page ( String common_name ) = [ "", "", "" ]. public define Web_Site make_web_site_description ( String common_name, // for example: "www.our-business.com" String site_directory, ActionTicket -> One init, (ActionTicket, HTTP_Info) -> $State initial_state, (ActionTicket, $State expired, HTTP_Info, List(Web_arg), Bool is_https) -> $State ticket_expired_state, (ActionTicket, HTTP_Info, List(Web_arg), Bool is_https) -> $State ticket_lost_state, List(Web_Action($State)) actions, $State -> HTML_Page compute_page, Int32 timeout, List(Redirection) redirections, List(String) journal_extensions, List(String) journal_headers, String secret, List(MIME) known_mime_types, (String action_name, List(Web_arg) args) -> One before_send_file ) = init(hf87jdh23sqPjfYUGF7865GHTjf7); // // load a font for button texts // if load_system_font("times_medium_r_12") is { failure then print("Cannot load system font 'times_medium_r_12'.\n"); alert, success(font) then with // // make required directories (if needed) // web_sites_directory = make_directory(my_anubis_directory+"/web_sites"), state_directory = make_directory(site_directory+"/states"), // // manage automatic buttons: create the buttons directory (if needed), // and remove old buttons. // buttons_directory = site_directory+"/public/buttons", forget(make_directory(site_directory+"/public")); forget(make_directory(buttons_directory)); with button_files = directory_list(buttons_directory,"b*.jpg"), forget(map((String fn) |-> remove(buttons_directory+"/"+fn), button_files)); // // construct tool functions // with save_state = make_save_state_function(timeout,state_directory), retrieve_state = make_retrieve_state_function(state_directory), separate_web_args = make_separate_web_args_function(state_directory,retrieve_state), apply_action = make_apply_action_function(common_name,actions), // // construct the site handler // site_handler = (Int32 http_port, Int32 https_port) |-> ((HTTP_Info http_info, List(Web_arg) lwa, Bool is_https) |-> (Printable_tree) if separate_web_args(lwa) is { swa(mb_previous_state,mb_action_name,operands) then with new_state = if mb_previous_state is { not_found then if mb_action_name is { failure then ($State) initial_state(hf87jdh23sqPjfYUGF7865GHTjf7,http_info), success(_) then ticket_lost_state(hf87jdh23sqPjfYUGF7865GHTjf7,http_info,lwa,is_https) }, out_of_date(state) then ticket_expired_state(hf87jdh23sqPjfYUGF7865GHTjf7,state,http_info,lwa,is_https), still_valid(state) then if mb_action_name is { failure then state, success(action_name) then apply_action(state,action_name,http_info,operands,is_https) } }, state_name = save_state(new_state), format(info(common_name,http_port,https_port,site_directory,secret,font), state_name,compute_page(new_state),is_https) }), // // make the delete_out_of_date function // delete_out_of_date = make_delete_out_of_date_states_function((Maybe($State))failure, site_directory+"/states"), // // construct the web site description // web_site((Int32 http_port, Int32 https_port) |-> web_site_description(common_name, site_directory, redirections, journal_extensions, journal_headers, secret, known_mime_types, site_handler(http_port,https_port), (List(Web_arg) lwa) |-> if separate_web_args(lwa) is swa(mb_previous_state,mb_action_name,operands) then if mb_action_name is { failure then unique success(an) then before_send_file(an,operands) }), delete_out_of_date)}. *** [4.3] Starting the servers. public define Start_Web_Sites_Result start_web_sites ( Int32 ip_address, // the IP address shared by the web sites Int32 http_port, // usually: 80 Int32 https_port, // usually: 443 String ssl_certificate_common_name, List(Web_Site) web_sites // web sites to be started ) = with get_description = (Web_Site ws) |-> description(ws)(http_port,https_port), with http_server_r = start_http_server (ip_address,http_port, map(get_description,web_sites)), with https_server_r = start_https_server(ip_address,https_port,ssl_certificate_common_name,map(get_description,web_sites)), if http_server_r is ok(http_server) then ( if https_server_r is ok(https_server) then ( start_http_servers_tasks(map(get_description,web_sites)); delegate delete_states_loop( map((Web_Site ws) |-> (site_directory(description(ws)(http_port,https_port))+ "/states",delete_out_of_date(ws)), web_sites), 3600*24*3, // keep out of date states 3 days now, http_server, https_server), ok(http_server,https_server) ) else cannot_bind_to_port(https_port) ) else ( if https_server_r is ok(https_server) then cannot_bind_to_port(http_port) else cannot_bind_to_port(http_port,https_port) ). public define One start_web_sites ( Int32 ip_address, // the IP address shared by the web sites Int32 http_port, // usually: 80 Int32 https_port, // usually: 443 String ssl_certificate_common_name, List(Web_Site) web_sites // web sites to be started ) = if (Start_Web_Sites_Result)start_web_sites(ip_address, http_port, https_port, ssl_certificate_common_name, web_sites) is { cannot_bind_to_port(n) then print("Cannot bind to port: "+n+"\n"), cannot_bind_to_port(n,m) then print("Cannot bind to ports: "+n+", "+m+"\n"), ok(s1,s2) then print("Servers started.\n") }. *** [5] HTML Formating. We need to translate HTML elements as defined above into actual HTML text. Actioners require special informations, which must be transmitted when needed by the 'format' functions: - the 'common name', which is used for URLs, - the HTTP/HTTPS port number, - the 'state name', which must be transmitted when the actioner is clicked upon, - the 'form name' (if any) to which the actioner refers. If the actioner is off form, and if it refers to a form, the name of that form is already known by the actioner. On the contrary, if the actioner is 'in form', it refers implicitly to the form containing it. The name of that form is transmitted to the 'format' functions called from within the formating of that form. *** [5.1] The type 'HTML_Any($T)'. The type 'HTML_Any($T)' gathers elements which may be put anywhere in the page. The parameter $T becomes either 'HTML_Off_Form' or 'HTML_In_Form'. type HTML_Any($T): any_text (List(Text_Option), String the_text), any_preformated (List(Text_Option), String), any_paragraph (List(Text_Option), String the_text), any_image (String url), any_table (List(Table_Option), List(HTML_Row($T))), any_center ($T), any_mail_to (String email, $T element), any_scroller (Int32 width, Int32 height, Int32 content_width, Int32 content_height, $T content), any_fixed_size (Int32 width, Int32 height, $T content), any_fixed_size (Int32 width, Int32 height, String name_of_HTML_file), any_actioner (Actioner_Connection, Actioner_Target, Actioner_Aspect, String action_name, List((String,String)) extra_ops, Maybe(String) form_name), any_local_popup (Actioner_Aspect, $T content, Int32 x, Int32 y, String title, RGB color, Int32 width), any_foreign_link (String url, String name), any_private_download (String abs_path, String name, String extra_ext, Maybe((String,List((String,String))))). *** [5.2] Formating a color. RGB colors are formatted as '#rrggbb' where rr, gg and bb are two characters hexadecimal values. define String format ( RGB color ) = if color is rgb(r,g,b) then "#" + hexadecimal(int8_to_int32(r),2) + hexadecimal(int8_to_int32(g),2) + hexadecimal(int8_to_int32(b),2). The following is a very arbitrary definition of the opposite color. The thing which is important is that it is far from the original, so that characters in 'opposite' color are clearly visible over the original. define RGB opposite ( RGB color ) = if color is rgb(r,g,b) then with r1 = int8_to_int32(r), with g1 = int8_to_int32(g), with b1 = int8_to_int32(b), rgb(truncate_to_int8(255-r1), truncate_to_int8(255-g1), truncate_to_int8(255-b1)). *** [5.3] Creating buttons. We want to be able to create buttons in the form of a pair of images (rollovers) automatically. We use the JPEG interface, because for the time being Anubis cannot handle other kinds of images. Computing printed text length. define Int32 printed_text_width ( Int8 -> Int32 char_size, List(Int8) l ) = if l is { [] then (Int32) 0, [h . t] then char_size(h) + 1+ printed_text_width(char_size,t) }. define Int32 printed_text_width ( SystemFont font, String s ) = printed_text_width((Int8 c) |-> int8_to_int32(width(get_char_info(font,c))), explode(s)). Converting RGB to RGBA. define RGBA to_rgba ( RGB color ) = if color is rgb(r,g,b) then rgba(r,g,b,255). Drawing a 'relief'. define One draw_relief ( RGBAImage dest, RGBA color, Int32 contrast, Int32 x, Int32 y, Int32 width, Int32 height ) = with l = lighten(color,contrast), d = darken(color,contrast), draw_rectangle(dest,rect(x,y,x+width,y+1),l); draw_rectangle(dest,rect(x,y+1,x+1,y+height),l); draw_rectangle(dest,rect(x+width-1,y+1,x+width,y+height),d); draw_rectangle(dest,rect(x+1,y+height-1,x+width-1,y+height),d). Creating a button background. define RGBAImage create_button_background ( RGBA color, Int32 width, Int32 height ) = with result = create_rgba_image(width,height,color), draw_relief(result,color,100,0,0,width,height); draw_relief(result,color,70,1,1,width-2,height-2); draw_relief(result,color,55,2,2,width-4,height-4); draw_relief(result,color,35,3,3,width-6,height-6); draw_relief(result,color,20,4,4,width-8,height-8); draw_relief(result,color,10,5,5,width-10,height-10); draw_relief(result,color,5,6,6,width-12,height-12); result. Drawing the text over the background. define One draw_button_text ( RGBAImage image, String text, Int32 text_index, Int32 pixel_x, Int32 y, Rectangle clip, RGBA color, SystemFont font, ) = if nth(text_index,text) is { failure then unique, success(c) then with cw = draw_system_character(image,clip,pixel_x,y,font,int8_to_int32(c),color), draw_button_text(image,text,text_index+1,pixel_x+cw+1,y,clip,color,font) }. define One draw_button_text ( RGBAImage image, String text, Int32 text_width, RGBA light_color, RGBA dark_color, SystemFont font ) = with image_width = width(image), image_height = height(image), x_pos = (image_width-text_width)>>1, clip = rect(0,0,image_width,image_height), new_light_color = lighten(light_color,150), new_dark_color = darken(dark_color,40), draw_button_text(image, text, 0, x_pos+2, 16, clip, new_dark_color, font); draw_button_text(image, text, 0, x_pos, 14, clip, new_light_color, font). The next function creates the two images for a button. The information given is the main color of the button, the text of the button and the minimal width (in pixels) of the button. The function does not create the button if the images already exist. The two images are stored in the directory 'site_directory/buttons'. The names of the files are of the form: bxxxx_off.jpg bxxxx_on.jpg where the prefix 'b' is to avoid leading '-' which may perturb UNIX commands (like 'rm'), and where 'xxxx' is created from the given informations by the formula: xxxx = web_arg_encode(sha1((color,text,width))) Hence, distinct informations give distinct file names. define String // returns xxxx create_button_images ( String site_directory, RGBA color, String text, Int32 width, SystemFont font ) = with xxxx = web_arg_encode(sha1((color,text,width))), buttons_dir = site_directory+"/public/buttons", off_filepath = buttons_dir+"/b"+xxxx+"_off.jpg", on_filepath = buttons_dir+"/b"+xxxx+"_on.jpg", if file_exists(on_filepath) then xxxx else with text_width = printed_text_width(font,text), button_width = max(width,text_width+12), button_height = (Int32)20, light_color = lighten(color,60), very_light_color = lighten(light_color,30), dark_color = darken(color,40), background_off = create_button_background(color,button_width,button_height), background_on = create_button_background(light_color,button_width,button_height), draw_button_text(background_off,text,text_width,very_light_color,dark_color,font); draw_button_text(background_on, text,text_width,very_light_color,dark_color,font); forget(write_image_to_JPEG_file(to_JPEG(background_off), off_filepath, 100)); forget(write_image_to_JPEG_file(to_JPEG(background_on), on_filepath, 100)); xxxx. *** [5.4] Formating an actioner. An actioner works as follows. Assume first that it refers to a form. When it is clicked upon, the actioner puts (via 'onMouseDown') the URL into the 'action' attribute of the form, and submits the form, using the JavaScript command 'form_name.submit()'. If the actioner does not refer to a form, it fires the URL directly via 'href', because in that case, the actioner is always an tag. The URL itself is composed using the connection sort (same, http or https), the common name and port number (if needed), the state name, the action name, and the extra operands, which are put into a query string. It may look like this: http://common_name:port/?s=state_name&a=action_name&oname=value... Each extra operand is a pair of strings: (name,value). It is formated as: &oname=value define String format_extra_operands ( List((String,String)) l ) = if l is { [ ] then "", [h . t] then if h is (n,v) then "&o"+n+"="+v+format_extra_operands(t) }. In case the target is another window, we need to format the options for this window. define String format ( List(Other_Window_Option) l ) = if l is { [ ] then "", [h . t] then if h is { resizable then "resizable", scrollbars then "scrollbars", width(w) then "width="+w, height(h) then "height="+h } + if t is [ ] then "" else (","+format(t)) }. public define Printable_tree format_choices ( List(String) l ) = if l is { [ ] then [ ], [h . t] then ["' tag, with a 'href' attribute. If it refers to a form, it is still realized by a '' tag, but with no href attribute. In this case, we use the 'onMouseDown' or 'onChange' event handler. The handler calls a JavaScript function which puts the URL as the value of the 'action' attribute of the form, and submits the form. type URL_or_JavaScript: url (String), javascript (Printable_tree script, Printable_tree handler). define URL_or_JavaScript format_action ( String the_url, Maybe(String) mb_form_name ) = if mb_form_name is { failure then url(the_url), success(form_name) then with n = new_count, javascript( [ "" ], ["pfu",form_name,n,"();"] ) }. Now, we format the actioner according to its aspect. define String format ( List(Text_Option) l ). define Printable_tree format_actioner ( CommonInfo cinfo, String state_name, Actioner_Connection connection, Actioner_Target target, Actioner_Aspect aspect, String action_name, List((String,String)) extra_ops, Maybe(String) mb_form_name, Bool is_https, ) = if cinfo is info(common_name,http_port,https_port,site_dir,secret,font) then with url = make_actioner_url(cinfo,connection,target, state_name,action_name,extra_ops,is_https), with action = format_action(url,mb_form_name), if aspect is { link(opt,text) then [ if action is { url(u) then [""], javascript(s,h) then [s,""] }, text, "" ], button(url_on,url_off) then [ if action is { url(u) then ["", "", "" ], button(text,width,color) then with xxxx = create_button_images(site_dir, to_rgba(color),text,width,font), url_on = "buttons/b"+xxxx+"_on.jpg", url_off = "buttons/b"+xxxx+"_off.jpg", [ if action is { url(u) then ["", "", "" ], immediate_selector(name,size,choices) then [ if action is { url(u) then ["" ] }. define Printable_tree format_local_popup_button ( CommonInfo cinfo, Actioner_Aspect aspect, Int32 n, ) = if cinfo is info(common_name,http_port,https_port,site_dir,secret,font) then [ "", "", if aspect is { link(opt,text) then [text], button(url_off,url_on) then [ "", ], button(text,width,color) then with xxxx = create_button_images(site_dir, to_rgba(color),text,width,font), url_on = "buttons/b"+xxxx+"_on.jpg", url_off = "buttons/b"+xxxx+"_off.jpg", [ "", ], immediate_selector(name,size,choices) then alert }, ""]. *** [5.5] Formating a private download link. We get the absolute path of the file to be downloaded, and the name under which it should appear to the client. The function 'format_private_download' creates an hypertext link for downloading the file. The secured mecanism of private download is used. This function is called by the function which formats HTML_Any($T) elements. define Printable_tree format_private_download ( CommonInfo cinfo, String sn, // state name String abs_path, // absolute file path on server String name, // name of file as it appears in the browser String extra, // extra extension Maybe((String,List((String,String)))) action ) = if cinfo is info(common_name,http_port,https_port,site_directory,secret,font) then with private_download_directory = site_directory+"/private_download", with auth = make_authorization(site_directory,secret,abs_path), [ "", name, "" ]. *** [5.6] Formating rows and cells in a table. define Int32 percent ( Int32 p ) = if p < 0 then 0 else if p > 100 then 100 else p. Formating cell options. define String format ( BackgroundOption o ) = if o is { repeat then "", repeat_horizontal then "; background-repeat: repeat-x", repeat_vertical then "; background-repeat: repeat-y", no_repeat then "; background-repeat: no-repeat" }. define String format ( List(Cell_Option) options ) = if options is { [ ] then "", [h . t] then if h is { left then " align=left", h_center then " align=center", right then " align=right", top then " valign=top", v_center then " valign=center", bottom then " valign=bottom", base_line then " valign=baseline", background_color(c) then " bgcolor="+format(c), background_image(n,o) then " style=\"background: url("+n+")"+format(o)+"\"", width(w) then " width="+w, percentage_width(n) then " width="+percent(n)+"%", height(h) then " height="+h, columns(n) then " colspan="+n, rows(n) then " rowspan="+n, nowrap then " nowrap" } + format(t) }. Formating cells in a row. define Printable_tree format ( List(HTML_Cell($T)) cells, $T -> Printable_tree format_element ) = if cells is { [ ] then [ ], [h . t] then if h is cell(options,element) then ["", format_element(element), "" . format(t,format_element)] }. Formating the rows in a table. define Printable_tree format ( List(HTML_Row($T)) rows, $T -> Printable_tree format_element, ) = if rows is { [ ] then [ ], [h . t] then if h is row(options,cells) then ["", format(cells,format_element), "" . format(t,format_element)] }. define Printable_tree format ( List(TextAreaOption) l ) = if l is { [] then [], [h . t] then if h is { disabled then ["disabled " . format(t)] } }. *** [5.7] Formating elements which may be put anywhere. The function below involves the parameter $T which is later instantiated as 'HTML_In_Form' or as 'HTML_Off_Form'. Now, since there are dictinct 'format' functions for these two types, and because formating of tables requires recursive calls of such functions, it is necessary to provide the 'format' function to be called recursively as an argument. Putting naively a call to 'format' will not work, because the compiler will look for a function able to format data of type $T (which is at that time distinct from any other type, including our two types). Such a function does not exist. Hence the function to be called for formating elements must be passed as a functional argument (called 'format_element' below). Actually, what we pass is a function taking a unique argument of type $T. Other informations (like the name of the state) are already in the function by way of full functionality. Formating text options. They are formated in CSS syntax, to be used within a 'style=...'. define String format ( List(Text_Option) l ) = if l is { [ ] then "", [h . t] then if h is { size(n) then "font-size:"+n+"pt", font(fn) then "font-family:"+fn, color(c) then if c is rgb(r,g,b) then "color:rgb("+int8_to_int32(r)+","+int8_to_int32(g)+","+int8_to_int32(b)+")", italic then "font-style:italic", oblique then "font-style:oblique", small_capitals then "font-variant:small-caps", bold then "font-weight:bold", underlined then "text-decoration:underline", left_justified then "text-align:left", right_justified then "text-align:right", justified then "text-align:justify", line_through then "text-decoration:line-through" } + if t is [ ] then "" else ("; "+format(t)) }. Formating table options. define String format ( List(Table_Option) l, Bool border_seen ) = if l is { [ ] then if border_seen then "" else " border=0 cellspacing=0 cellpadding=0", [h . t] then if h is { background_color(c) then " bgcolor="+format(c)+format(t,border_seen), background_image(url) then " background="+url+format(t,border_seen), border(o,top,i,c) then " border="+o+" cellspacing="+top+" cellpadding="+i+ " bordercolor="+format(c)+format(t,true), width(w) then " width="+w+format(t,border_seen), percentage_width(p) then " width="+percent(p)+"%" } }. define Printable_tree format_scroller ( String sn, Int32 width, Int32 height, Int32 content_width, Int32 content_height, Int32 idnum, // identifying the scroller $T content, $T -> Printable_tree format_element ) = [ "", "", "", "", "", "", (if content_width > width then [ "", "", "", ] else [ ]), "
", "
", "
", format_element(content), "
", "
", "
", "", "", "", "
", "
", "", "", "", "", "", "
", "
" ]. define Printable_tree popup_topbar ( CommonInfo cinfo, String title, RGB color, Int32 width, ) = if cinfo is info(common_name,http_port,https_port,site_directory,secret,font) then with xxxx = web_arg_encode(sha1((title,color,width))), path = site_directory+"/public/buttons/t"+xxxx+".jpg", result = (Printable_tree)[""], if file_exists(path) then result else with col = to_rgba(color), bg = create_button_background(col,width,20), very_light_color = lighten(col,70), dark_color = darken(col,40), title_width = printed_text_width(font,title), draw_button_text(bg,title,title_width,very_light_color,dark_color,font); forget(write_image_to_JPEG_file(to_JPEG(bg),path,100)); result. define Printable_tree popup_close_button ( CommonInfo cinfo, RGB color, String div_name, String state_var_name, ) = if cinfo is info(common_name,http_port,https_port,site_directory,secret,font) then with xxxx = web_arg_encode(sha1(color)), path_on = site_directory+"/public/buttons/c"+xxxx+"_on.jpg", path_off = site_directory+"/public/buttons/c"+xxxx+"_off.jpg", result = (Printable_tree)[""], if file_exists(path_on) then result else with col = to_rgba(color), title = "x", title_width = printed_text_width(font,title), bg_on = create_button_background(lighten(col,30),20,20), bg_off = create_button_background(col,20,20), very_light_color = lighten(col,70), dark_color = darken(col,40), draw_button_text(bg_on,title,title_width,very_light_color,dark_color,font); draw_button_text(bg_off,title,title_width,very_light_color,dark_color,font); forget(write_image_to_JPEG_file(to_JPEG(bg_on),path_on,100)); forget(write_image_to_JPEG_file(to_JPEG(bg_off),path_off,100)); result. The function below formats a datum of type 'HTML_Any($T)'. define Printable_tree format ( CommonInfo cinfo, String sn, // state_name Var(Int32) ic_v, HTML_Any($T) element, $T -> Printable_tree format_element, // able to format a datum of type $T Bool is_https, ) = if cinfo is info(common_name,http_port,https_port,site_directory,secret,font) then if element is { any_text(opts,t) then ["",t,""], any_preformated(opts,s) then ["
",s,"
"], //["
",s,"
"], any_paragraph(opts,t) then ["

",t,"

"], any_image(url) then [""], any_table(opts,rows) then ["",format(rows,format_element),"
"], any_center(e) then ["
",format_element(e),"
"], any_mail_to(email,elem) then ["",format_element(elem),""], any_scroller(w,h,cw,ch,c) then format_scroller(sn,w,h,cw,ch,new_idnum(ic_v),c,format_element), any_fixed_size(w,h,c) then with url = create_secondary_document(site_directory,secret,sn,format_element,c,w), ["", "secondary document", ""], any_fixed_size(w,h,fn) then with url = fn+"?zauth="+make_authorization(site_directory,secret, fn), ["", "secondary document", ""], any_actioner(c,t,a,an,eo,fn) then format_actioner(cinfo,sn,c,t,a,an,eo,fn,is_https), any_local_popup(a,c,x,y,t,clr,w) then with n = new_idnum(ic_v), b = format_local_popup_button(cinfo,a,n), f = ["
", "", "
", popup_topbar(cinfo,t,clr,w), "", popup_close_button(cinfo,clr,"lpu_"+n,"lpust_"+n), "
", format_element(c), "
", "
"], [ "
", b, "", f, "
" ], any_foreign_link(url,name) then ["",name,""], any_private_download(url,name,extra_ext,action) then format_private_download(cinfo,sn,url,name,extra_ext,action) }. *** [5.8] Formating 'in form' elements. define Printable_tree format ( CommonInfo cinfo, String fn, // form_name String sn, // state_name Var(Int32) ic_v, // idnum counter variable HTML_In_Form element, Bool is_https, ) = if cinfo is info(common_name,http_port,https_port,site_directory,secret,font) then with format_element = (HTML_In_Form e) |-> format(cinfo,fn,sn,ic_v,e,is_https), if element is { literal(t) then t, sequence(l) then flat(map(format_element,l)) text(opts,t) then format(cinfo,sn,ic_v,any_text(opts,t),format_element,is_https), preformated(o,s) then format(cinfo,sn,ic_v,any_preformated(o,s),format_element,is_https), paragraph(opts,t) then format(cinfo,sn,ic_v,any_paragraph(opts,t),format_element,is_https), image(url) then format(cinfo,sn,ic_v,any_image(url),format_element,is_https), table(opts,rows) then format(cinfo,sn,ic_v,any_table(opts,rows),format_element,is_https), center(e) then format(cinfo,sn,ic_v,any_center(e),format_element,is_https), mail_to(a,e) then format(cinfo,sn,ic_v,any_mail_to(a,e),format_element,is_https), scroller(w,h,cw,ch,c) then format(cinfo,sn,ic_v,any_scroller(w,h,cw,ch,c),format_element,is_https), actioner(c,t,a,an,eo) then format(cinfo,sn,ic_v,any_actioner(c,t,a,an,eo,success(fn)),format_element,is_https), local_popup(a,c,x,y,t,clr,w) then format(cinfo,sn,ic_v,any_local_popup(a,c,x,y,t,clr,w),format_element,is_https), foreign_link(url,name) then format(cinfo,sn,ic_v,any_foreign_link(url,name),format_element,is_https), private_download(url,name,extra,action) then format(cinfo,sn,ic_v,any_private_download(url,name,extra,action),format_element,is_https), text_input(n,i,w) then [""], //["  "], password_input(n,w) then [""], //["  "], text_area(opts,n,i,w,h) then [""], file_upload(n,w) then [""], selector(n,s,cs) then [""], selector(n,s,cs,sd) then [""], radio_button(n,v,c) then [""], check_box(n,c) then [""] }. *** [5.9] Formating 'off form' elements. The encryption type 'multipart/form-data' is required for a form containing an upload. define Bool contains_an_upload ( HTML_In_Form form_content ). define Bool contains_an_upload ( HTML_Row(HTML_In_Form) row ) = mapor(contains_an_upload, map(content,cells(row))). define Bool contains_an_upload ( HTML_In_Form form_content ) = if form_content is { literal(t) then false, sequence(l) then mapor(contains_an_upload,l) text(o,t) then false, preformated(o,s) then false, paragraph(o,t) then false, image(u) then false, table(o,rows) then mapor(contains_an_upload,rows), center(e) then contains_an_upload(e), mail_to(m,e) then false, // 'e' may but should not contain an upload scroller(w,h,cw,ch,e) then contains_an_upload(e), actioner(c,t,a,an,eo) then false, local_popup(a,c,x,y,t,clr,w) then contains_an_upload(c), foreign_link(u,n) then false, private_download(p,n,e,a) then false, text_input(n,i,w) then false, password_input(n,w) then false, text_area(o,n,i,w,h) then false, file_upload(n,w) then true, selector(n,s,c) then false, selector(n,s,c,p) then false, radio_button(n,v,c) then false, check_box(n,c) then false }. define String enctype ( HTML_In_Form form_content ) = if contains_an_upload(form_content) then " enctype=multipart/form-data" else "". define Printable_tree format ( CommonInfo cinfo, String sn, // state_name Var(Int32) ic_v, // 'idnum' counter variable HTML_Off_Form element, Bool is_https, ) = if cinfo is info(common_name,http_port,https_port,site_directory,secret,font) then with format_element = (HTML_Off_Form e) |-> format(cinfo,sn,ic_v,e,is_https), if element is { literal(t) then t, sequence(l) then flat(map(format_element,l)), text(opts,t) then format(cinfo,sn,ic_v,any_text(opts,t),format_element,is_https), preformated(o,s) then format(cinfo,sn,ic_v,any_preformated(o,s),format_element,is_https), paragraph(opts,t) then format(cinfo,sn,ic_v,any_paragraph(opts,t),format_element,is_https), image(url) then format(cinfo,sn,ic_v,any_image(url),format_element,is_https), table(opts,rows) then format(cinfo,sn,ic_v,any_table(opts,rows),format_element,is_https), center(e) then format(cinfo,sn,ic_v,any_center(e),format_element,is_https), mail_to(a,e) then format(cinfo,sn,ic_v,any_mail_to(a,e),format_element,is_https), scroller(w,h,cw,ch,c) then format(cinfo,sn,ic_v,any_scroller(w,h,cw,ch,c),format_element,is_https), fixed_size(w,h,c) then format(cinfo,sn,ic_v,any_fixed_size(w,h,c),format_element,is_https), fixed_size(w,h,fn) then format(cinfo,sn,ic_v,any_fixed_size(w,h,fn),format_element,is_https), actioner(c,t,a,an,eo) then format(cinfo,sn,ic_v,any_actioner(c,t,a,an,eo,failure),format_element,is_https), actioner(c,t,a,an,eo,fn) then format(cinfo,sn,ic_v,any_actioner(c,t,a,an,eo,success(fn)),format_element,is_https), local_popup(a,c,x,y,t,clr,w) then format(cinfo,sn,ic_v,any_local_popup(a,c,x,y,t,clr,w),format_element,is_https), foreign_link(url,name) then format(cinfo,sn,ic_v,any_foreign_link(url,name),format_element,is_https), private_download(url,name,extra,action) then format(cinfo,sn,ic_v,any_private_download(url,name,extra,action),format_element,is_https), form(fn,c) then [ "
", // action is set dynamically by // the actioner using JavaScript format(cinfo,fn,sn,ic_v,c,is_https), "
" ] }. *** [5.10] Formating meta-tags. define Printable_tree format_keywords ( List(String) l ) = if l is { [] then [ ], [h . t] then if t is [] then [h] else [h , ", " . format_keywords(t)] }. define Printable_tree format ( CommonInfo cinfo, String state_name, HTML_Meta m, Bool is_https ) = if m is { keywords(l) then [""], refresh(co,ta,an,delay) then [""], meta(n,c) then [""], http_equiv(n,c) then [""], generic_meta(l) then [" if p is (n,v) then [n,"=\"",v,"\" "], l)), ">"] }. define Printable_tree format ( CommonInfo cinfo, String state_name, List(HTML_Meta) metas, Bool is_https ) = if metas is { [] then [], [h . t] then [format(cinfo,state_name,h,is_https) . format(cinfo,state_name,t,is_https)] }. define Printable_tree format ( Body_Option o ) = if o is { background_color(c) then [" bgcolor=" , (String)format(c)], background_image(n) then [" background=", n] }. define Printable_tree format ( List(Body_Option) l ) = if l is { [ ] then [ ], [h . t] then [format(h) . format(t)] }. define Printable_tree format ( CommonInfo cinfo, String state_name, HTML_Page page, Bool is_https, ) = if cinfo is info(common_name,http_port,https_port,site_directory,secret,font) then with ic_v = var((Int32)0), if page is { html_page(title,metas,body) then if body is body(options,element) then [ "", "", "", "", "", "",title,"", // put title format(cinfo,state_name,metas,is_https), // format the metas "", "", // format body options //"
", format(cinfo,state_name,ic_v,element,is_https), //"
", "", "" ] }.