/*
 * Presence Agent, subscribe handling
 *
 * $Id: subscribe.c 30 2005-06-16 12:42:16Z bogdan_iancu $
 *
 * Copyright (C) 2001-2003 FhG Fokus
 *
 * This file is part of openser, a free SIP server.
 *
 * openser is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version
 *
 * openser is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License 
 * along with this program; if not, write to the Free Software 
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * History:
 * ---------
 * 2003-02-29 scratchpad compatibility abandoned
 * 2003-01-27 next baby-step to removing ZT - PRESERVE_ZT (jiri)
 */

#include <string.h>
#include "../../str.h"
#include "../../dprint.h"
#include "../../mem/mem.h"
#include "../../parser/parse_uri.h"
#include "../../parser/parse_from.h"
#include "../../parser/parse_expires.h"
#include "../../parser/parse_event.h"
#include "../../parser/parse_content.h"
#include "presentity.h"
#include "watcher.h"
#include "pstate.h"
#include "notify.h"
#include "paerrno.h"
#include "pdomain.h"
#include "pa_mod.h"
#include "ptime.h"
#include "reply.h"
#include "subscribe.h"


#define DOCUMENT_TYPE "application/cpim-pidf+xml"
#define DOCUMENT_TYPE_L (sizeof(DOCUMENT_TYPE) - 1)

static struct {
	int event_type;
	int mimes[MAX_MIMES_NR];
} event_package_mimetypes[] = {
	{ EVENT_PRESENCE, { MIMETYPE(APPLICATION,PIDFXML), 
#ifdef SUBTYPE_XML_MSRTC_PIDF
			    MIMETYPE(APPLICATION,XML_MSRTC_PIDF),
#endif
			    MIMETYPE(APPLICATION,XPIDFXML), MIMETYPE(APPLICATION,LPIDFXML), 0 } },
	{ EVENT_PRESENCE_WINFO, { MIMETYPE(APPLICATION,WATCHERINFOXML), 0 }},
#ifdef EVENT_SIP_PROFILE
	{ EVENT_SIP_PROFILE, { MIMETYPE(MESSAGE,EXTERNAL_BODY), 0 }},
#endif
//	{ EVENT_XCAP_CHANGE, { MIMETYPE(APPLICATION,WINFO+XML), 0 } },
	{ -1, { 0 }},
};

/*
 * contact will be NULL if user is offline
 * fixme:locking
 */
void callback(str* _user, str *_contact, int state, void* data)
{
     presentity_t *presentity;

     get_act_time();

     presentity = (struct presentity*)data;

     if (presentity && callback_update_db) {
	  presence_tuple_t *tuple = NULL;
	  int orig;
	  LOG(L_ERR, "callback: uri=%.*s contact=%.*s state=%d\n",
	      presentity->uri.len, presentity->uri.s, (_contact ? _contact->len : 0), (_contact ? _contact->s : ""), state);
	  if (_contact) {
	       if (callback_lock_pdomain)
		    lock_pdomain(presentity->pdomain);

	       find_presence_tuple(_contact, presentity, &tuple);
	       if (!tuple) {
		    new_presence_tuple(_contact, act_time + default_expires, presentity, &tuple);
		    add_presence_tuple(presentity, tuple);
	       };

	       orig = tuple->state;

	       if (state == 0) {
		    tuple->state = PS_OFFLINE;
	       } else {
		    tuple->state = PS_ONLINE;
	       }

	       tuple->expires = act_time + default_expires;

	       db_update_presentity(presentity);

	       if (orig != state) {
		    presentity->flags |= PFLAG_PRESENCE_CHANGED;
	       }

	       if (callback_lock_pdomain)
		    unlock_pdomain(presentity->pdomain);
	  }
     }
}

/*
 * Extract plain uri -- return URI without parameters
 * The uri will be in form username@domain
 *
 */
static int extract_plain_uri(str* _uri)
{
	struct sip_uri puri;

	if (parse_uri(_uri->s, _uri->len, &puri) < 0) {
		paerrno = PA_URI_PARSE;
		LOG(L_ERR, "extract_plain_uri(): Error while parsing URI\n");
		return -1;
	}
	
	_uri->s = puri.user.s;
	_uri->len = puri.host.s + puri.host.len - _uri->s;
	return 0;
}


/*
 * Get presentity URI, which is stored in R-URI
 */
int get_pres_uri(struct sip_msg* _m, str* _puri)
{
	if (_m->new_uri.s) {
		_puri->s = _m->new_uri.s;
		_puri->len = _m->new_uri.len;
	} else {
		_puri->s = _m->first_line.u.request.uri.s;
		_puri->len = _m->first_line.u.request.uri.len;
	}
	LOG(L_ERR, "get_pres_uri: _puri=%.*s\n", _puri->len, _puri->s);
	
	if (extract_plain_uri(_puri) < 0) {
		LOG(L_ERR, "get_pres_uri(): Error while extracting plain URI\n");
		return -1;
	}

	return 0;
}


static int get_watch_uri(struct sip_msg* _m, str* _wuri, str *_dn)
{
	_wuri->s = get_from(_m)->uri.s;
	_wuri->len = get_from(_m)->uri.len;
	_dn->s = get_from(_m)->body.s;
	_dn->len = get_from(_m)->body.len;

	if (extract_plain_uri(_wuri) < 0) {
		LOG(L_ERR, "get_watch_uri(): Error while extracting plain URI\n");
		return -1;
	}
	
	return 0;
}

/*
 * Parse all header fields that will be needed
 * to handle a SUBSCRIBE request
 */
static int parse_hfs(struct sip_msg* _m, int accept_header_required)
{
	int rc = 0;
	if ( ((rc = parse_headers(_m, HDR_FROM_F | HDR_EVENT_F | HDR_EXPIRES_F | HDR_ACCEPT_F, 0)) == -1) 
	     || (_m->from==0) || (_m->event==0) ) {
		paerrno = PA_PARSE_ERR;
		LOG(L_ERR, "parse_hfs(): Error while parsing headers: rc=%d\n", rc);
		return -1;
	}

	if (parse_from_header(_m) < 0) {
		paerrno = PA_FROM_ERR;
		LOG(L_ERR, "parse_hfs(): From malformed or missing\n");
		return -6;
	}

	if (_m->event) {
		if (parse_event(_m->event) < 0) {
			paerrno = PA_EVENT_PARSE;
			LOG(L_ERR, "parse_hfs(): Error while parsing Event header field\n");
			return -8;
		}
	}

	if (_m->expires) {
		if (parse_expires(_m->expires) < 0) {
			paerrno = PA_EXPIRES_PARSE;
			LOG(L_ERR, "parse_hfs(): Error while parsing Expires header field\n");
			return -9;
		}
	}

	/* now look for Accept header */
	if (_m->accept) {
		LOG(L_ERR, "parsing accept header\n");
		if (parse_accept_hdr(_m) < 0) {
			paerrno = PA_ACCEPT_PARSE;
			LOG(L_ERR, "parse_hfs(): Error while parsing Accept header field\n");
			return -10;
		}
	} else if (accept_header_required) {
		LOG(L_ERR, "no accept header\n");
		return -11;
	}

	return 0;
}


/*
 * Check if a message received has been constructed properly
 */
int check_message(struct sip_msg* _m)
{
	LOG(L_ERR, "check_message -0- _m=%p\n", _m);
	if (_m->event) {
		event_t *parsed_event;
		int *accepts_mimes = NULL;

		LOG(L_ERR, "check_message -1-");

		if (_m->accept) {
			accepts_mimes = get_accept(_m);
			if (accepts_mimes) {
				char buf[100];
				int offset = 0;
				int *a = accepts_mimes;
				buf[0] = '0';
				while (*a) {
					offset += sprintf(buf+offset, ":%#06x", *a);
					a++;
				}
				LOG(L_ERR, "pa check_message: accept=%.*s parsed=%s\n",
				    _m->accept->body.len, _m->accept->body.s, buf);
			}
		}
		LOG(L_ERR, "check_message -2- accepts_mimes=%p\n", accepts_mimes);

		if (!_m->event->parsed)
			parse_event(_m->event);
		LOG(L_ERR, "check_message -3-\n");
		parsed_event = (event_t*)(_m->event->parsed);

		LOG(L_ERR, "check_message -4- parsed_event=%p\n", parsed_event);
		if (parsed_event && accepts_mimes) {
			int i = 0;
			int eventtype = parsed_event->parsed;
			LOG(L_ERR, "check_message -4- eventtype=%#06x\n", eventtype);
			while (event_package_mimetypes[i].event_type != -1) {
				LOG(L_ERR, "check_message -4a- eventtype=%#x epm[i].event_type=%#x", 
				    eventtype, event_package_mimetypes[i].event_type);
				if (eventtype == event_package_mimetypes[i].event_type) {
					int j = 0;
					int mimetype;
					while ((mimetype = event_package_mimetypes[i].mimes[j]) != 0) {
						int k = 0;
						while (accepts_mimes[k]) {
							LOG(L_ERR, "check_message -4c- eventtype=%#x mimetype=%#x accepts_mimes[k]=%#x\n", eventtype, mimetype, accepts_mimes[k]);

							if (accepts_mimes[k] == mimetype) {
								int am0 = accepts_mimes[0];
								/* we have a match */
								LOG(L_ERR, "check_message -4b- eventtype=%#x accepts_mime=%#x\n", eventtype, mimetype);
								/* move it to front for later */
								accepts_mimes[0] = mimetype;
								accepts_mimes[k] = am0;
								return 0;
							}
							k++;
						}
						j++;
					}
				}
				i++;
			}
			/* else, none of the mimetypes accepted are generated for this event package */
			{
				char *accept_s = NULL;
				int accept_len = 0;
				if (_m->accept && _m->accept->body.len) {
					accept_s = _m->accept->body.s;
					accept_len = _m->accept->body.len;
				}
				LOG(L_ERR, "check_message(): Accepts %.*s not valid for event package et=%.*s\n",
				    _m->accept->body.len, _m->accept->body.s, _m->event->body.len, _m->event->body.s);
				return -1;
			}
		}
		LOG(L_ERR, "check_message -5-\n");
	}
	return 0;
}


int get_preferred_event_mimetype(struct sip_msg *_m, int et)
{
	int acc = 0;
	if (_m->accept) {
		//LOG(L_ERR, "%s: has accept header\n", __FUNCTION__);
		int *accepts_mimes = get_accept(_m);
		acc = accepts_mimes[0];
	} else {
		int i = 0;
		//LOG(L_ERR, "%s: no accept header\n", __FUNCTION__);
		while (event_package_mimetypes[i].event_type != -1) {
			if (event_package_mimetypes[i].event_type == et) {
				acc = event_package_mimetypes[i].mimes[0];
				LOG(L_ERR, "%s: defaulting to mimetype %x for event_type=%d\n", __FUNCTION__, acc, et);
				break;
			}
			i++;
		}
	}
	return acc;
}

/*
 * Create a new presentity and corresponding watcher list
 */
int create_presentity(struct sip_msg* _m, struct pdomain* _d, str* _puri, 
			     struct presentity** _p, struct watcher** _w)
{
	time_t e;
	dlg_t* dialog;
	str watch_uri;
	str watch_dn;
	event_t *event = NULL;
	int et = 0;
	int acc = 0;
	if (_m->event) {
		event = (event_t*)(_m->event->parsed);
		et = event->parsed;
	} else {
		et = EVENT_PRESENCE;
	}
	acc = get_preferred_event_mimetype(_m, et);

	if (_m->expires) {
		e = ((exp_body_t*)_m->expires->parsed)->val;
	} else {
		e = default_expires;
	}
	
	if (e == 0) {
		*_p = 0;
		*_w = 0;
		DBG("create_presentity(): expires = 0\n");
		return 0;
	}

	/* Convert to absolute time */
	e += act_time;

	if (get_watch_uri(_m, &watch_uri, &watch_dn) < 0) {
		LOG(L_ERR, "create_presentity(): Error while extracting watcher URI\n");
		return -1;
	}

	if (new_presentity(_d, _puri, _p) < 0) {
		LOG(L_ERR, "create_presentity(): Error while creating presentity\n");
		return -2;
	}

	if (tmb.new_dlg_uas(_m, 200, &dialog) < 0) {
		paerrno = PA_DIALOG_ERR;
		LOG(L_ERR, "create_presentity(): Error while creating dialog state\n");
		free_presentity(*_p);
		return -3;
	}

	if (et != EVENT_PRESENCE_WINFO) {
		if (add_watcher(*_p, &watch_uri, e, et, acc, dialog, &watch_dn, _w) < 0) {
			LOG(L_ERR, "create_presentity(): Error while adding a watcher\n");
			tmb.free_dlg(dialog);
			free_presentity(*_p);
			return -4;
		}
	} else if (et == EVENT_PRESENCE_WINFO) {
		if (add_winfo_watcher(*_p, &watch_uri, e, et, acc, dialog, &watch_dn, _w) < 0) {
			LOG(L_ERR, "create_presentity(): Error while adding a winfo watcher\n");
			tmb.free_dlg(dialog);
			free_presentity(*_p);
			return -5;
		}
	}
	add_presentity(_d, *_p);

	_d->reg(&watch_uri, _puri, (void*)callback, *_p);
	return 0;
}


/*
 * Update existing presentity and watcher list
 */
static int update_presentity(struct sip_msg* _m, struct pdomain* _d, 
			     struct presentity* _p, struct watcher** _w)
{
	time_t e;
	dlg_t* dialog;
	str watch_uri;
	str watch_dn;
	event_t *event = NULL;
	int et = 0;
	int acc = 0;
	if (_m->event) {
		event = (event_t*)(_m->event->parsed);
		et = event->parsed;
	} else {
		LOG(L_ERR, "update_presentity defaulting to EVENT_PRESENCE\n");
		et = EVENT_PRESENCE;
	}
	acc = get_preferred_event_mimetype(_m, et);

	if (_m->expires) {
		e = ((exp_body_t*)_m->expires->parsed)->val;
	} else {
		e = default_expires;
	}

	if (get_watch_uri(_m, &watch_uri, &watch_dn) < 0) {
		LOG(L_ERR, "update_presentity(): Error while extracting watcher URI\n");
		return -1;
	}

	if (find_watcher(_p, &watch_uri, et, _w) == 0) {
		LOG(L_ERR, "update_presentity() found watcher\n");
		if (e == 0) {
			if (et != EVENT_PRESENCE_WINFO) {
				if (remove_watcher(_p, *_w) < 0) {
					LOG(L_ERR, "update_presentity(): Error while deleting winfo watcher\n");
					return -2;
				} 
			} else {
				if (remove_winfo_watcher(_p, *_w) < 0) {
					LOG(L_ERR, "update_presentity(): Error while deleting winfo watcher\n");
					return -2;
				} 
			}
			
			(*_w)->expires = 0;   /* The watcher will be freed after NOTIFY is sent */
			if (!_p->watchers && !_p->winfo_watchers) {
				remove_presentity(_d, _p);
			}
		} else {
			e += act_time;
			if (update_watcher(*_w, e) < 0) {
				LOG(L_ERR, "update_presentity(): Error while updating watcher\n");
				return -3;
			}
		}
	} else {
		if (e) {
			e += act_time;

			if (tmb.new_dlg_uas(_m, 200, &dialog) < 0) {
				paerrno = PA_DIALOG_ERR;
				LOG(L_ERR, "update_presentity(): Error while creating dialog state\n");
				return -4;
			}

			if (et != EVENT_PRESENCE_WINFO) {
				if (add_watcher(_p, &watch_uri, e, et, acc, dialog, &watch_dn, _w) < 0) {
					LOG(L_ERR, "update_presentity(): Error while creating presentity\n");
					tmb.free_dlg(dialog);
					return -5;
				}
			} else {
				if (add_winfo_watcher(_p, &watch_uri, e, et, acc, dialog, &watch_dn, _w) < 0) {
					LOG(L_ERR, "update_presentity(): Error while creating winfo watcher\n");
					tmb.free_dlg(dialog);
					return -5;
				}			
			}
		} else {
			DBG("update_presentity(): expires = 0 but no watcher found\n");
			*_w = 0;
		}
	}

	return 0;
}


/*
 * Handle a registration request -- make sure aor exists in presentity table
 */
/*
 * Extract Address of Record
 */
#define MAX_AOR_LEN 256

int pa_extract_aor(str* _uri, str* _a)
{
	static char aor_buf[MAX_AOR_LEN];
	struct sip_uri puri;
	int user_len;

	if (parse_uri(_uri->s, _uri->len, &puri) < 0) {
		LOG(L_ERR, "pa_extract_aor(): Error while parsing Address of Record\n");
		return -1;
	}
	
	if ((puri.user.len + puri.host.len + 1) > MAX_AOR_LEN) {
		LOG(L_ERR, "pa_extract_aor(): Address Of Record too long\n");
		return -2;
	}

	_a->s = aor_buf;
	_a->len = puri.user.len;

	user_len = _a->len;

	memcpy(aor_buf, puri.user.s, puri.user.len);
	aor_buf[_a->len] = '@';
	memcpy(aor_buf + _a->len + 1, puri.host.s, puri.host.len);
	_a->len += 1 + puri.host.len;

#if 0
	if (case_sensitive) {
		tmp.s = _a->s + user_len + 1;
		tmp.len = puri.host.len;
		strlower(&tmp);
	} else {
		strlower(_a);
	}
#endif

	return 0;
}

int pa_handle_registration(struct sip_msg* _m, char* _domain, char* _s2)
{
     struct pdomain* d = (struct pdomain*)_domain;
     struct presentity *presentity;
     str p_uri;
     struct to_body *from = NULL;
     int e = 0;


     // LOG(L_ERR, "pa_handle_registration() entered\n");
     paerrno = PA_OK;

     d = (struct pdomain*)_domain;

     if (parse_hfs(_m, 0) < 0) {
	  paerrno = PA_PARSE_ERR;
	  LOG(L_ERR, "pa_handle_registration(): Error while parsing headers\n");
	  return -1;
     }

     from = get_from(_m);
     if (!from || (pa_extract_aor(&from->uri, &p_uri) < 0)) {
	  LOG(L_ERR, "pa_handle_registration(): Error while extracting Address Of Record\n");
	  goto error;
     }

     if (_m->expires) {
	  e = ((exp_body_t*)_m->expires->parsed)->val;
     } else {
	  e = default_expires;
     }

     if (from)
       LOG(L_ERR, "pa_handle_registration: from=%.*s p_uri=%.*s expires=%d\n", 
	   from->uri.len, from->uri.s, p_uri.len, p_uri.s, e);

     lock_pdomain(d);
	
     if (find_presentity(d, &p_uri, &presentity) > 0) {
	  LOG(L_ERR, "pa_handle_registration: find_presentity did not find presentity\n");
	  if (e > 0) {
	       if (create_presentity_only(_m, d, &p_uri, &presentity) < 0) {
		    LOG(L_ERR, "pa_handle_registration(): Error while creating new presentity\n");
		    goto error2;
	       }
	  } 
#if 0
	  else {
	       presence_tuple_t *tuple = NULL;
	       if (_m->contact) {
		    struct hdr_field* ptr = _m->contact;
		    while (ptr) {
			 if (ptr->type == HDR_CONTACT_T) {
			      if (!ptr->parsed && (parse_contact(ptr) < 0)) {
				   goto next;
			      }
			 }
			 if (find_presence_tuple(contact, presentity, &tuple) == 0) {
			      tuple->state = PS_OFFLINE;
			 }
		    next:
			 ptr = ptr->next;
		    }
	       }

	       db_update_presentity(presentity);
	  }
#endif
     }

     if (presentity && e > 0) {
	  LOG(L_ERR, "pa_handle_registration about to call d->reg p=%p expires=%d", presentity, e);
	  d->reg(&presentity->uri, &presentity->uri, (void*)callback, presentity);
     }

     LOG(L_ERR, "pa_handle_registration about to return 1");
     unlock_pdomain(d);
     return 1;
	
 error2:
     LOG(L_ERR, "pa_handle_registration about to return -1\n");
     unlock_pdomain(d);
     return -1;
 error:
     LOG(L_ERR, "pa_handle_registration about to return -2\n");
     return -1;
}

/*
 * Handle a subscribe Request
 */
int handle_subscription(struct sip_msg* _m, char* _domain, char* _s2)
{
	struct pdomain* d;
	struct presentity *p;
	struct watcher* w;
	str p_uri;

	LOG(L_ERR, "handle_subscription() entered\n");
	get_act_time();
	paerrno = PA_OK;

	if (parse_hfs(_m, 0) < 0) {
		LOG(L_ERR, "handle_subscription(): Error while parsing message header\n");
		goto error;
	}

	if (check_message(_m) < 0) {
		LOG(L_ERR, "handle_subscription(): Error while checking message\n");
		goto error;
	}

	d = (struct pdomain*)_domain;

	if (get_pres_uri(_m, &p_uri) < 0) {
		LOG(L_ERR, "handle_subscription(): Error while extracting presentity URI\n");
		goto error;
	}

	lock_pdomain(d);
	
	LOG(L_ERR, "handle_subscription(): -1-\n");
	if (find_presentity(d, &p_uri, &p) > 0) {
		LOG(L_ERR, "handle_subscription(): -2-\n");
		if (create_presentity(_m, d, &p_uri, &p, &w) < 0) {
			LOG(L_ERR, "handle_subscription(): Error while creating new presentity\n");
			goto error2;
		}
	} else {
		LOG(L_ERR, "handle_subscription(): -3-\n");
		if (update_presentity(_m, d, p, &w) < 0) {
			LOG(L_ERR, "handle_subscription(): Error while updating presentity\n");
			goto error2;
		}
	}

	if (send_reply(_m) < 0) {
	  LOG(L_ERR, "handle_subscription(): Error while sending reply\n");
	  goto error2;
	}

	if (p) {
		p->flags |= PFLAG_WATCHERINFO_CHANGED;
	}
	if (w) {
		w->flags |= WFLAG_SUBSCRIPTION_CHANGED;
	}

	LOG(L_ERR, "handle_subscription about to return 1: w->event_package=%d w->accept=%d p->flags=%x w->flags=%x w=%p\n",
	    (w ? w->event_package : -1), (w ? w->preferred_mimetype : -1), (p ? p->flags : -1), (w ? w->flags : -1), w);
	unlock_pdomain(d);
	return 1;
	
 error2:
	LOG(L_ERR, "handle_subscription about to return -1\n");
	unlock_pdomain(d);
	return -1;
 error:
	LOG(L_ERR, "handle_subscription about to send_reply and return -2\n");
	send_reply(_m);
	return -1;
}


/*
 * Returns 1 if subscription exists and -1 if not
 */
int existing_subscription(struct sip_msg* _m, char* _domain, char* _s2)
{
	struct pdomain* d;
	struct presentity* p;
	struct watcher* w;
	str p_uri, w_uri;
	str w_dn;
	int et = 0;
	if (_m->event) {
		event_t *event = (event_t*)(_m->event->parsed);
		et = event->parsed;
	} else {
		LOG(L_ERR, "existing_subscription defaulting to EVENT_PRESENCE\n");
		et = EVENT_PRESENCE;
	}

	paerrno = PA_OK;

	if (parse_from_header(_m) < 0) {
		paerrno = PA_PARSE_ERR;
		LOG(L_ERR, "existing_subscription(): Error while parsing From header field\n");
		goto error;
	}

	d = (struct pdomain*)_domain;

	if (get_pres_uri(_m, &p_uri) < 0) {
		LOG(L_ERR, "existing_subscription(): Error while extracting presentity URI\n");
		goto error;
	}

	if (get_watch_uri(_m, &w_uri, &w_dn) < 0) {
		LOG(L_ERR, "existing_subscription(): Error while extracting watcher URI\n");
		goto error;
	}

	lock_pdomain(d);
	
	if (find_presentity(d, &p_uri, &p) == 0) {
		if (find_watcher(p, &w_uri, et, &w) == 0) {
			LOG(L_ERR, "existing_subscription() found watcher\n");
			unlock_pdomain(d);
			return 1;
		}
	}

	unlock_pdomain(d);
	return -1;

 error:
	send_reply(_m);       
	return 0;
}


/*
 * Returns 1 if possibly a user agent can handle SUBSCRIBE
 * itself, 0 if it cannot for sure
 */
int pua_exists(struct sip_msg* _m, char* _domain, char* _s2)
{

	return 0;
}





syntax highlighted by Code2HTML, v. 0.9.1