/****************************************************************************
 * This file is part of the project AqHome.
 * AqHome (c) by 2025 Martin Preuss, all rights reserved.
 *
 * The license for this file can be found in the file COPYING which you
 * should have received along with this file.
 ****************************************************************************/

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif


#include "./musers.h"

#include "aqhome-cgi/service/module.h"

#include <gwenhywfar/debug.h>
#include <gwenhywfar/timestamp.h>
#include <gwenhywfar/text.h>



/* ------------------------------------------------------------------------------------------------
 * defs and enums
 * ------------------------------------------------------------------------------------------------
 */

#define GBAS GWEN_Buffer_AppendString
#define GBAA GWEN_Buffer_AppendArgs



/* ------------------------------------------------------------------------------------------------
 * forward declarations
 * ------------------------------------------------------------------------------------------------
 */

static void _createPermDefList(AQH_MODULE *m);
static void _createRoleList(AQH_MODULE *m);

static AQH_MODULE *_loadSubModule(AQH_MODULE *m, AQCGI_REQUEST *rq, AQH_SESSION *session, const char *sModuleName);
static int _handleRequest(AQH_MODULE *m, AQCGI_REQUEST *rq, AQH_SESSION *session, const char *sLastPathElem);
static void _handleRqIndex(AQH_MODULE *m, AQCGI_REQUEST *rq, AQH_SESSION *session, GWEN_BUFFER *dbuf);
static void _handleRqEditUserGet(AQH_MODULE *m, AQCGI_REQUEST *rq, AQH_SESSION *session, GWEN_BUFFER *dbuf);
static void _handleRqEditUserPost(AQH_MODULE *m, AQCGI_REQUEST *rq, AQH_SESSION *session, GWEN_BUFFER *dbuf);
static void _handleRqAddUserGet(AQH_MODULE *m, AQCGI_REQUEST *rq, AQH_SESSION *session, GWEN_BUFFER *dbuf);
static void _handleRqAddUserPost(AQH_MODULE *m, AQCGI_REQUEST *rq, AQH_SESSION *session, GWEN_BUFFER *dbuf);

static int _getHighestUserId(const AQH_USER_LIST *userList);
static int _modulePermsHasRole(const AQH_MODULE_PERMS *modPerms, uint8_t rid);

static void _writeEditUserForm(AQH_MODULE *m,
                               const AQH_USER *u,
                               const char *sAlias,
                               const char *sUrl,
                               const char *sSubmitText,
                               GWEN_BUFFER *dbuf);
static void _writeUserModRolesToForm(const AQH_ROLE_LIST *roles, const AQH_MODULE_PERMS *perms, const char *sModName, GWEN_BUFFER *dbuf);

static void _readAllModRolesForUserFromForm(AQH_MODULE *m, GWEN_DB_NODE *dbPost, AQH_USER *u);
static void _readModRolesFromForm(GWEN_DB_NODE *dbPost,
                                  const AQH_ROLE_LIST *roleList,
                                  const char *sPrefix,
                                  AQH_MODULE_PERMS *modPerms);

static void _addLabelAndInputToFormTableH(const char *title, const char *name, const char *value, const char *xxtra, GWEN_BUFFER *dbuf);
static void _addUserStateLabelAndSelectionToFormTableH(const char *sTitle, const char *sName, int st, GWEN_BUFFER *dbuf);

static void _setLocationHeaderForMod(AQCGI_REQUEST *rq, const char *page, const char *sModName);



/* ------------------------------------------------------------------------------------------------
 * vars
 * ------------------------------------------------------------------------------------------------
 */

static AQH_MODSERVICE_HANDLER_ENTRY _requestTable[]={
  {"index.html",      AQCGI_REQUEST_METHOD_GET,  AQH_MODADMUSERS_PERMS_USERSREAD,  _handleRqIndex},
  {"edituser.html",   AQCGI_REQUEST_METHOD_GET,  AQH_MODADMUSERS_PERMS_USERSWRITE, _handleRqEditUserGet},
  {"edituser.html",   AQCGI_REQUEST_METHOD_POST, AQH_MODADMUSERS_PERMS_USERSWRITE, _handleRqEditUserPost},
  {"adduser.html",    AQCGI_REQUEST_METHOD_GET,  AQH_MODADMUSERS_PERMS_USERSWRITE, _handleRqAddUserGet},
  {"adduser.html",    AQCGI_REQUEST_METHOD_POST, AQH_MODADMUSERS_PERMS_USERSWRITE, _handleRqAddUserPost},
  {NULL, 0, 0, NULL}
};



/* ------------------------------------------------------------------------------------------------
 * code
 * ------------------------------------------------------------------------------------------------
 */

void AQH_ModAdmUsers_Extend(AQH_MODULE *m, AQH_SERVICE *sv, const char *baseFolder)
{
  AQH_ModService_Extend(m, sv, baseFolder);
  AQH_ModService_SetHandleRequestFn(m, _handleRequest);
  AQH_ModService_SetLoadSubModuleFn(m, _loadSubModule);
}



int AQH_ModAdmUsers_Create(AQH_SERVICE *sv)
{
  AQH_MODULE *m;
  int rv;

  m=AQH_Module_new();
  AQH_Module_SetName(m, "users");
  AQH_Module_SetDescr(m, "user administration module");
  AQH_Module_SetGuestPerms(m, 0);

  _createPermDefList(m);
  _createRoleList(m);

  rv=AQH_Service_AddModule(sv, m);
  if (rv<0) {
    DBG_INFO(NULL, "here (%d)", rv);
  }
  AQH_Module_free(m);
  return rv;
}



void _createPermDefList(AQH_MODULE *m)
{
  AQH_PERMDEF_LIST *permDefList;

  permDefList=AQH_PermDef_List_new();

  AQH_ModService_AddPermDef(permDefList, "UserRead",  0x001, "Read users");
  AQH_ModService_AddPermDef(permDefList, "UserWrite", 0x002, "Modify users");
  AQH_ModService_AddPermDef(permDefList, "UserAdd",   0x004, "Add users");
  AQH_ModService_AddPermDef(permDefList, "UserDel",   0x008, "Remove users");

  AQH_Module_SetPermDefList(m, permDefList);
}



void _createRoleList(AQH_MODULE *m)
{
  AQH_ROLE_LIST *roleList;
  int id=0;

  roleList=AQH_Role_List_new();
  AQH_ModService_AddRole(roleList, id++, "admin",
                         AQH_MODADMUSERS_PERMS_USERSREAD |
                         AQH_MODADMUSERS_PERMS_USERSWRITE |
                         AQH_MODADMUSERS_PERMS_USERSADD |
                         AQH_MODADMUSERS_PERMS_USERSDEL,
                         "Administrator Role");
  AQH_Module_SetRoleList(m, roleList);
}



AQH_MODULE *_loadSubModule(AQH_MODULE *m, AQCGI_REQUEST *rq, AQH_SESSION *session, const char *sModuleName)
{
  /* no sub-modules */
  return NULL;
}



int _handleRequest(AQH_MODULE *m, AQCGI_REQUEST *rq, AQH_SESSION *session, const char *sLastPathElem)
{
  AQH_ModService_HandleRequestWithTable(m, rq, session, sLastPathElem, _requestTable);
  return AQCGI_SendResponse(rq);
}



void _handleRqIndex(AQH_MODULE *m, AQCGI_REQUEST *rq, AQH_SESSION *session, GWEN_BUFFER *dbuf)
{
  AQH_USER_LIST *userList;
  uint32_t perms;

  perms=AQH_ModService_GetUserPerms(m);
  userList=AQH_ModService_LoadRawUsers(m);

  GBAS(dbuf, "<h1>Users</h1>\n");
  GBAS(dbuf,
       "<table class=\"datatable\">\n"
       "<thead>"
       "<tr><th>Id</th><th>Alias</th><th>Name</th><th>Status</th><th>Email</th><th>Notes</th><th>Actions</th></tr>\n"
       "</thead>\n"
       "<tbody>\n");
  if (userList) {
    const AQH_USER *u;

    AQH_User_List_SortByAlias(userList, 1);
    u=AQH_User_List_First(userList);
    while(u) {
      const char *sUserAlias;

      sUserAlias=AQH_User_GetAlias(u);
      if (sUserAlias && *sUserAlias) {
        uint32_t id;
        const char *s;
        const char *sAlias;

        id=AQH_User_GetId(u);
        sAlias=AQH_User_GetAlias(u);
        GBAS(dbuf, "<tr>");
        GBAA(dbuf, "<td>%lu</td>", (unsigned long int) id);
        GBAA(dbuf, "<td>%s</td>", sAlias?sAlias:"");
        s=AQH_User_GetName(u);
        GBAA(dbuf, "<td>%s</td>", s?s:"");
        s=AQH_UserState_toString(AQH_User_GetState(u));
        GBAA(dbuf, "<td>%s</td>", s?s:"");
        s=AQH_User_GetEmail(u);
        GBAA(dbuf, "<td>%s</td>", s?s:"");
        s=AQH_User_GetNotes(u);
        GBAA(dbuf, "<td>%s</td>", s?s:"");

        GBAS(dbuf, "<td>");
        if (perms & AQH_MODADMUSERS_PERMS_USERSWRITE) {
          DBG_ERROR(NULL, "User=%s", sAlias?sAlias:"");
          GBAS(dbuf, "<a href=\"edituser.html?alias=");
          GWEN_Text_EscapeToBufferTolerant(sAlias?sAlias:"", dbuf);
          GBAS(dbuf, "\"><img src=\"/pics/edit.png\"></a>");
        }
        GBAA(dbuf, "</td>\n");
        GBAA(dbuf, "</tr>\n");
      }
      u=AQH_User_List_Next(u);
    }
    GBAS(dbuf,
         "</tbody>\n"
         "</table>\n");
    AQH_User_List_free(userList);
  }
  if (perms & AQH_MODADMUSERS_PERMS_USERSADD)
    GBAS(dbuf, "<hr><a href=\"adduser.html\">Add User</a>");
}



void _handleRqEditUserGet(AQH_MODULE *m, AQCGI_REQUEST *rq, AQH_SESSION *session, GWEN_BUFFER *dbuf)
{
  AQH_SERVICE *sv;
  GWEN_DB_NODE *dbQuery;
  const char *sAlias;
  AQH_USER *user;

  sv=AQH_ModService_GetService(m);
  dbQuery=AQCGI_Request_GetDbQuery(rq);
  sAlias=dbQuery?GWEN_DB_GetCharValue(dbQuery, "alias", 0, NULL):NULL;
  user=(sAlias && *sAlias)?AQH_Service_LoadUser(sv, sAlias):NULL;
  if (user) {
    _writeEditUserForm(m, user, sAlias, "edituser.html", "Save", dbuf);
    AQH_User_free(user);
  }
  else {
    AQCGI_Request_AddResponseHeaderData(rq, "Location: index.html");
    AQCGI_Request_SetResponseCode(rq, 303);
    AQCGI_Request_SetResponseText(rq, "See other");
  }
}



void _handleRqEditUserPost(AQH_MODULE *m, AQCGI_REQUEST *rq, AQH_SESSION *session, GWEN_BUFFER *dbuf)
{
  AQH_SERVICE *sv;
  GWEN_DB_NODE *dbPost;
  const char *sAlias;
  AQH_USER *u;

  sv=AQH_ModService_GetService(m);
  dbPost=AQCGI_Request_GetDbPostBody(rq);
  sAlias=dbPost?GWEN_DB_GetCharValue(dbPost, "alias", 0, NULL):NULL;
  u=(sAlias && *sAlias)?AQH_Service_LoadUser(sv, sAlias):NULL;
  if (u) {
    const char *s;
    int state;
    int rv;

    s=GWEN_DB_GetCharValue(dbPost, "name", 0, NULL);
    AQH_User_SetName(u, s);
    s=GWEN_DB_GetCharValue(dbPost, "email", 0, NULL);
    AQH_User_SetEmail(u, s);
    s=GWEN_DB_GetCharValue(dbPost, "notes", 0, NULL);
    AQH_User_SetNotes(u, s);
    s=GWEN_DB_GetCharValue(dbPost, "status", 0, NULL);
    state=(s && *s)?AQH_UserState_fromString(s):AQH_UserState_Unknown;
    if (state!=AQH_UserState_Unknown)
      AQH_User_SetState(u, state);

    _readAllModRolesForUserFromForm(m, dbPost, u);

    rv=AQH_Service_SaveUser(sv, u);
    if (rv<0) {
      GBAS(dbuf, "<h2>Error</h2><p>Error saving user</p>");
      DBG_ERROR(NULL, "Could not save user \"%s\"", sAlias);
      AQH_User_free(u);
      return;
    }
    DBG_ERROR(NULL, "User \"%s\" saved", sAlias);
    AQH_User_free(u);
    AQCGI_Request_AddResponseHeaderData(rq, "Location: index.html");
    AQCGI_Request_SetResponseCode(rq, 303);
    AQCGI_Request_SetResponseText(rq, "See other");
  }
  else {
    DBG_ERROR(NULL, "Could not load user \"%s\"", sAlias?sAlias:"<no name>");
    GBAS(dbuf, "<p>Error loading user.</p>\n");
  }
}



void _handleRqAddUserGet(AQH_MODULE *m, AQCGI_REQUEST *rq, AQH_SESSION *session, GWEN_BUFFER *dbuf)
{
  DBG_ERROR(NULL, "AddUser");
  _writeEditUserForm(m, NULL, NULL, "adduser.html", "Add", dbuf);
}



void _handleRqAddUserPost(AQH_MODULE *m, AQCGI_REQUEST *rq, AQH_SESSION *session, GWEN_BUFFER *dbuf)
{
  AQH_SERVICE *sv;
  GWEN_DB_NODE *dbPost;
  const char *sAlias;

  sv=AQH_ModService_GetService(m);
  dbPost=AQCGI_Request_GetDbPostBody(rq);
  sAlias=dbPost?GWEN_DB_GetCharValue(dbPost, "alias", 0, NULL):NULL;
  // TODO: check alias validity
  if (sAlias) {
    AQH_USER *u;
    const char *s;
    int state;
    int rv;
    uint32_t userId;
    AQH_USER_LIST *userList;

    u=AQH_User_new();
    userList=AQH_ModService_LoadRawUsers(m);
    userId=_getHighestUserId(userList)+1;
    AQH_User_SetId(u, userId);
    AQH_User_SetAlias(u, sAlias);
    s=GWEN_DB_GetCharValue(dbPost, "name", 0, NULL);
    AQH_User_SetName(u, s);
    s=GWEN_DB_GetCharValue(dbPost, "email", 0, NULL);
    AQH_User_SetEmail(u, s);
    s=GWEN_DB_GetCharValue(dbPost, "notes", 0, NULL);
    AQH_User_SetNotes(u, s);
    s=GWEN_DB_GetCharValue(dbPost, "status", 0, NULL);
    state=(s && *s)?AQH_UserState_fromString(s):AQH_UserState_Unknown;
    if (state!=AQH_UserState_Unknown)
      AQH_User_SetState(u, state);

    rv=AQH_Service_AddUser(sv, u);
    if (rv<0) {
      GBAS(dbuf, "<h2>Error</h2><p>Error saving user</p>");
      DBG_ERROR(NULL, "Could not save user \"%s\" (%d)", sAlias, rv);
      AQH_User_free(u);
      AQH_User_List_free(userList);
      return;
    }
    DBG_ERROR(NULL, "User \"%s\" saved", sAlias);
    AQH_User_free(u);
    AQCGI_Request_AddResponseHeaderData(rq, "Location: index.html");
    AQCGI_Request_SetResponseCode(rq, 303);
    AQCGI_Request_SetResponseText(rq, "See other");
    AQH_User_List_free(userList);
  }
  else {
    DBG_ERROR(NULL, "Missing alias");
    GBAS(dbuf, "<p>Missing alias.</p>\n");
  }
}



int _getHighestUserId(const AQH_USER_LIST *userList)
{
  int id=0;

  if (userList) {
    const AQH_USER *user;

    user=AQH_User_List_First(userList);
    while(user) {
      int uid;

      uid=AQH_User_GetId(user);
      id=(uid>id)?uid:id;
      user=AQH_User_List_Next(user);
    }
  }
  return id;
}



int _modulePermsHasRole(const AQH_MODULE_PERMS *modPerms, uint8_t rid)
{
  if (modPerms) {
    int arraySize;
    int i;

    arraySize=AQH_ModulePerms_GetRoleArrayArraySize();
    for(i=0; i<arraySize; i++) {
      if (AQH_ModulePerms_GetRoleArrayAt(modPerms, i)==rid)
        return 1;
    }
  }

  return 0;
}





void _writeEditUserForm(AQH_MODULE *m,
                        const AQH_USER *u,
                        const char *sAlias,
                        const char *sUrl,
                        const char *sSubmitText,
                        GWEN_BUFFER *dbuf)
{
  AQH_MODULE_LIST *moduleList;

  /* write user info */
  GBAS(dbuf, "<h2>User Info</h2>\n");
  GBAA(dbuf,
       "<form action=\"%s\" method=\"post\">\n"
       "<table class=\"formtable\">\n",
       sUrl?sUrl:"");
  _addLabelAndInputToFormTableH("Alias", "alias", sAlias,                      "required", dbuf);
  _addLabelAndInputToFormTableH("Name",  "name",  u?AQH_User_GetName(u):NULL,  NULL,       dbuf);
  _addLabelAndInputToFormTableH("Email", "email", u?AQH_User_GetEmail(u):NULL, NULL,       dbuf);
  _addLabelAndInputToFormTableH("Notes", "notes", u?AQH_User_GetNotes(u):NULL, NULL,       dbuf);
  _addUserStateLabelAndSelectionToFormTableH("Status", "status", u?AQH_User_GetState(u):AQH_UserState_Unknown, dbuf);

  GBAS(dbuf, "</table>\n");

  /* module permissions */
  GBAS(dbuf, "<h2>Module Roles</h2>\n");
    GBAS(dbuf,
         "<table class=\"datatable\">\n"
         "<thead>"
         "<tr><th>Module</th><th>Roles</th></tr>\n"
         "</thead>\n"
         "<tbody>\n");
  moduleList=AQH_ModService_LoadRawModules(m);
  if (moduleList) {
    const AQH_MODULE_PERMS_LIST *modPermsList;
    const AQH_MODULE *currentMod;

    modPermsList=u?AQH_User_GetModulePermList(u):NULL;
    currentMod=AQH_Module_List_First(moduleList);
    while(currentMod) {
      const char *sModName;
      const AQH_MODULE_PERMS *modPerms;

      sModName=AQH_Module_GetName(currentMod);
      GBAA(dbuf, "<tr><td>%s</td><td>", sModName);

      modPerms=modPermsList?AQH_ModulePerms_List_GetByModuleId(modPermsList, sModName):NULL;
      _writeUserModRolesToForm(AQH_Module_GetRoleList(currentMod), modPerms, sModName, dbuf);
      GBAS(dbuf, "</td></tr>\n");
      GBAS(dbuf, "<td>");

      currentMod=AQH_Module_List_Next(currentMod);
    } /* while */
    AQH_Module_List_free(moduleList);
  }
  GBAS(dbuf,
       "</tbody>\n"
       "</table>\n");


  GBAA(dbuf, "<input type=\"hidden\" name=\"alias\" value=\"%s\">\n", sAlias?sAlias:"");
  GBAA(dbuf, "<input type=\"submit\" value=\"%s\">\n</form>\n\n", sSubmitText?sSubmitText:"Save");

}



void _readAllModRolesForUserFromForm(AQH_MODULE *m, GWEN_DB_NODE *dbPost, AQH_USER *u)
{
  AQH_MODULE_LIST *moduleList;

  moduleList=AQH_ModService_LoadRawModules(m);
  if (moduleList) {
    AQH_MODULE_PERMS_LIST *permsList;
    const AQH_MODULE *module;

    permsList=AQH_User_GetModulePermList(u);
    if (permsList==NULL) {
      DBG_ERROR(NULL, "Creating module perms list for user");
      permsList=AQH_ModulePerms_List_new();
      AQH_User_SetModulePermList(u, permsList);
    }

    module=AQH_Module_List_First(moduleList);
    while(module) {
      const char *sModName;
      const AQH_ROLE_LIST *roleList;

      sModName=AQH_Module_GetName(module);
      roleList=AQH_Module_GetRoleList(module);
      if (sModName && *sModName && roleList) {
        AQH_MODULE_PERMS *modPerms;

        modPerms=AQH_ModulePerms_List_GetByModuleId(permsList, sModName);
        if (modPerms==NULL) {
          modPerms=AQH_ModulePerms_new();
          AQH_ModulePerms_SetModuleId(modPerms, sModName);
          AQH_ModulePerms_List_Add(modPerms, permsList);
        }
        _readModRolesFromForm(dbPost, roleList, sModName, modPerms);
      }
      module=AQH_Module_List_Next(module);
    }

    AQH_Module_List_free(moduleList);
  }
}



void _writeUserModRolesToForm(const AQH_ROLE_LIST *roleList, const AQH_MODULE_PERMS *modPerms, const char *sModName, GWEN_BUFFER *dbuf)
{
  if (roleList) {
    const AQH_ROLE *role;

    role=AQH_Role_List_First(roleList);
    while(role) {
      const char *sRoleName;
      uint8_t roleId;

      roleId=AQH_Role_GetId(role);
      sRoleName=AQH_Role_GetName(role);
      if (sRoleName && *sRoleName) {
        int isChecked;

        isChecked=(modPerms && _modulePermsHasRole(modPerms, roleId));
        if (sModName && *sModName) {
          GBAA(dbuf, "<input type=\"checkbox\" name=\"%s:%s\" %s>", sModName, sRoleName, isChecked?"checked":"");
          GBAA(dbuf, "<label for=\"%s:%s\">%s</label>", sModName, sRoleName, sRoleName);
        }
        else {
          GBAA(dbuf, "<input type=\"checkbox\" name=\"%s\" %s>", sRoleName, isChecked?"checked":"");
          GBAA(dbuf, "<label for=\"%s\">%s</label>", sRoleName, sRoleName);
        }
      }
      role=AQH_Role_List_Next(role);
    }
  }
}



void _readModRolesFromForm(GWEN_DB_NODE *dbPost,
                           const AQH_ROLE_LIST *roleList,
                           const char *sPrefix,
                           AQH_MODULE_PERMS *modPerms)
{
  AQH_ModulePerms_PresetRoleArray(modPerms, 0);
  if (roleList) {
    GWEN_BUFFER *tbuf;
    uint32_t pos;
    const AQH_ROLE *role;
    int nextRolePos=0;

    tbuf=GWEN_Buffer_new(0, 256, 0, 1);
    if (sPrefix && *sPrefix)
      GBAA(tbuf, "%s:", sPrefix);
    pos=GWEN_Buffer_GetPos(tbuf);

    role=AQH_Role_List_First(roleList);
    while(role) {
      const char *roleName;

      roleName=AQH_Role_GetName(role);
      if (roleName && *roleName) {
        const char *s;

        GBAS(tbuf, roleName);
        s=GWEN_DB_GetCharValue(dbPost, GWEN_Buffer_GetStart(tbuf), 0, NULL);
        if (s && *s) {
          if (nextRolePos<AQH_ModulePerms_GetRoleArrayArraySize())
            AQH_ModulePerms_SetRoleArrayAt(modPerms, nextRolePos++, AQH_Role_GetId(role));
        }
        GWEN_Buffer_Crop(tbuf, 0, pos);
      }

      role=AQH_Role_List_Next(role);
    } /* while */
    GWEN_Buffer_free(tbuf);
  }
}







void _addUserStateLabelAndSelectionToFormTableH(const char *sTitle, const char *sName, int st, GWEN_BUFFER *dbuf)
{
  int i;

  GBAA(dbuf, "<tr><td><label for=\"%s\">%s:</label></td>", sName?sName:"", sTitle?sTitle:"");
  GBAA(dbuf, "<td><select name=\"%s\">", sName?sName:"");
  for(i=AQH_UserState_Unknown; i<=AQH_UserState_Active; i++) {
    const char *s;

    s=AQH_UserState_toString(i);
    GBAA(dbuf, "<option value=\"%s\" %s>%s</option>", s, (i==st)?"selected":"", s);
  }
  GBAS(dbuf, "</select></td></tr>");
}



void _addLabelAndInputToFormTableH(const char *sTitle, const char *sName, const char *sValue, const char *sExtra, GWEN_BUFFER *dbuf)
{
  GBAS(dbuf, "<tr>");
  GBAA(dbuf, "<td><label for=\"%s:\">%s</label></td>", sName?sName:"", sTitle?sTitle:"");
  GBAA(dbuf, "<td><input type=\"text\" name=\"%s\"", sName?sName:"");
  if (sValue && *sValue)
    GBAA(dbuf, " value=\"%s\"", sValue);
  if (sExtra && *sExtra)
    GBAA(dbuf, " %s", sExtra);
  GBAS(dbuf, "></td>");
  GBAS(dbuf, "</tr>\n");
}



