r2906 - maintenance->trunk sync of changesets 2886~2894
[racktables] / inc / database.php
index 96f8e7b..f9c20ee 100644 (file)
@@ -5,6 +5,124 @@
 *
 */
 
+$SQLSchema = array
+(
+       'object' => array
+       (
+               'table' => 'RackObject',
+               'columns' => array
+               (
+                       'id' => 'id',
+                       'name' => 'name',
+                       'label' => 'label',
+                       'barcode' => 'barcode',
+                       'asset_no' => 'asset_no',
+                       'objtype_id' => 'objtype_id',
+                       'rack_id' => '(select rack_id from RackSpace where object_id = id order by rack_id asc limit 1)',
+                       'Rack_name' => '(select name from Rack where id = rack_id)',
+                       'row_id' => '(select row_id from Rack where id = rack_id)',
+                       'Row_name' => '(select name from RackRow where id = row_id)',
+                       'objtype_name' => '(select dict_value from Dictionary where dict_key = objtype_id)',
+                       'has_problems' => 'has_problems',
+                       'comment' => 'comment',
+               ),
+               'keycolumn' => 'id',
+               'ordcolumns' => array ('name'),
+       ),
+       'user' => array
+       (
+               'table' => 'UserAccount',
+               'columns' => array
+               (
+                       'user_id' => 'user_id',
+                       'user_name' => 'user_name',
+                       'user_password_hash' => 'user_password_hash',
+                       'user_realname' => 'user_realname',
+               ),
+               'keycolumn' => 'user_id',
+               'ordcolumns' => array ('user_name'),
+       ),
+       'ipv4net' => array
+       (
+               'table' => 'IPv4Network',
+               'columns' => array
+               (
+                       'id' => 'id',
+                       'ip' => 'INET_NTOA(IPv4Network.ip)',
+                       'mask' => 'mask',
+                       'name' => 'name',
+               ),
+               'keycolumn' => 'id',
+               'ordcolumns' => array ('ip', 'mask'),
+       ),
+       'file' => array
+       (
+               'table' => 'File',
+               'columns' => array
+               (
+                       'id' => 'id',
+                       'name' => 'name',
+                       'type' => 'type',
+                       'size' => 'size',
+                       'ctime' => 'ctime',
+                       'mtime' => 'mtime',
+                       'atime' => 'atime',
+                       'comment' => 'comment',
+               ),
+               'keycolumn' => 'id',
+               'ordcolumns' => array ('name'),
+       ),
+       'ipv4vs' => array
+       (
+               'table' => 'IPv4VS',
+               'columns' => array
+               (
+                       'id' => 'id',
+                       'vip' => 'INET_NTOA(vip)',
+                       'vport' => 'vport',
+                       'proto' => 'proto',
+                       'name' => 'name',
+                       'vsconfig' => 'vsconfig',
+                       'rsconfig' => 'rsconfig',
+                       'poolcount' => '(select count(vs_id) from IPv4LB where vs_id = id)',
+                       'dname' => 'CONCAT_WS("/", CONCAT_WS(":", INET_NTOA(vip), vport), proto)',
+               ),
+               'keycolumn' => 'id',
+               'ordcolumns' => array ('vip', 'proto', 'vport'),
+       ),
+       'ipv4rspool' => array
+       (
+               'table' => 'IPv4RSPool',
+               'columns' => array
+               (
+                       'id' => 'id',
+                       'name' => 'name',
+                       'refcnt' => '(select count(rspool_id) from IPv4LB where rspool_id = id)',
+                       'rscount' => '(select count(rspool_id) from IPv4RS where rspool_id = IPv4RSPool.id)',
+                       'vsconfig' => 'vsconfig',
+                       'rsconfig' => 'rsconfig',
+               ),
+               'keycolumn' => 'id',
+               'ordcolumns' => array ('name', 'id'),
+       ),
+       'rack' => array
+       (
+               'table' => 'Rack',
+               'columns' => array
+               (
+                       'id' => 'id',
+                       'name' => 'name',
+                       'height' => 'height',
+                       'comment' => 'comment',
+                       'row_id' => 'row_id',
+                       'row_name' => '(select name from RackRow where RackRow.id = row_id)',
+               ),
+               'keycolumn' => 'id',
+               'ordcolumns' => array ('row_id', 'name'),
+               'pidcolumn' => 'row_id',
+       ),
+);
+
 function isInnoDBSupported ($dbh = FALSE) {
        global $dbxlink;
 
@@ -33,26 +151,6 @@ function escapeString ($value, $do_db_escape = TRUE)
        return $ret;
 }
 
-function getRackspace ($tagfilter = array(), $tfmode = 'any')
-{
-       $whereclause = getWhereClause ($tagfilter);
-       $query =
-               "select RackRow.id as row_id, RackRow.name as row_name, count(Rack.id) as count " .
-               "from RackRow left join Rack on Rack.row_id = RackRow.id " .
-               "left join TagStorage on Rack.id = TagStorage.entity_id and entity_realm = 'rack' " .
-               "where 1=1 " .
-               $whereclause .
-               " group by RackRow.id order by RackRow.name";
-       $result = useSelectBlade ($query, __FUNCTION__);
-       $ret = array();
-       $clist = array ('row_id', 'row_name', 'count');
-       while ($row = $result->fetch (PDO::FETCH_ASSOC))
-               foreach ($clist as $cname)
-                       $ret[$row['row_id']][$cname] = $row[$cname];
-       $result->closeCursor();
-       return $ret;
-}
-
 // Return detailed information about one rack row.
 function getRackRowInfo ($rackrow_id)
 {
@@ -60,7 +158,7 @@ function getRackRowInfo ($rackrow_id)
                "select RackRow.id as id, RackRow.name as name, count(Rack.id) as count, " .
                "if(isnull(sum(Rack.height)),0,sum(Rack.height)) as sum " .
                "from RackRow left join Rack on Rack.row_id = RackRow.id " .
-               " " .
+               "where RackRow.id = ${rackrow_id} " .
                "group by RackRow.id";
        $result = useSelectBlade ($query, __FUNCTION__);
        if ($row = $result->fetch (PDO::FETCH_ASSOC))
@@ -69,25 +167,21 @@ function getRackRowInfo ($rackrow_id)
                return NULL;
 }
 
-
 function getRackRows ()
 {
        $query = "select id, name from RackRow ";
        $result = useSelectBlade ($query, __FUNCTION__);
        $rows = array();
        while ($row = $result->fetch (PDO::FETCH_ASSOC))
-               $rows[$row['id']] = parseWikiLink ($row['name'], 'o');
+               $rows[$row['id']] = $row['name'];
        $result->closeCursor();
        asort ($rows);
        return $rows;
 }
 
-
-
 function commitAddRow($rackrow_name)
 {
-       useInsertBlade('RackRow', array('name'=>"'$rackrow_name'"));
-       return TRUE;
+       return useInsertBlade('RackRow', array('name'=>"'$rackrow_name'"));
 }
 
 function commitUpdateRow($rackrow_id, $rackrow_name)
@@ -131,9 +225,6 @@ function commitDeleteRow($rackrow_id)
        return TRUE;
 }
 
-
-
-
 // This function returns id->name map for all object types. The map is used
 // to build <select> input for objects.
 function getObjectTypeList ()
@@ -141,223 +232,290 @@ function getObjectTypeList ()
        return readChapter ('RackObjectType');
 }
 
-// Return a part of SQL query suitable for embeding into a bigger text.
-// The returned result should list all tag IDs shown in the tag filter.
-function getWhereClause ($tagfilter = array())
+// Return a simple object list w/o related information, so that the returned value
+// can be directly used by printSelect(). An optional argument is the name of config
+// option with constraint in RackCode.
+function getNarrowObjectList ($varname = '')
 {
-       $whereclause = '';
-       if (count ($tagfilter))
+       $wideList = listCells ('object');
+       if (strlen ($varname) and strlen (getConfigVar ($varname)))
        {
-               $whereclause .= ' and (';
-               $conj = '';
-               foreach ($tagfilter as $tag_id)
-               {
-                       $whereclause .= $conj . 'tag_id = ' . $tag_id;
-                       $conj = ' or ';
-               }
-               $whereclause .= ') ';
+               global $parseCache;
+               if (!isset ($parseCache[$varname]))
+                       $parseCache[$varname] = spotPayload (getConfigVar ($varname), 'SYNT_EXPR');
+               if ($parseCache[$varname]['result'] != 'ACK')
+                       return array();
+               $wideList = filterCellList ($wideList, $parseCache[$varname]['load']);
        }
-       return $whereclause;
-}
-
-// Return a simple object list w/o related information.
-function getNarrowObjectList ($type_id = 0)
-{
        $ret = array();
-       if (!$type_id)
-       {
-               showError ('Invalid argument', __FUNCTION__);
-               return $ret;
-       }
-       // object type id is known and constant, but it's Ok to have this standard overhead
-       $query =
-               "select RackObject.id as id, RackObject.name as name, dict_value as objtype_name, " .
-               "objtype_id from " .
-               "RackObject inner join Dictionary on objtype_id=dict_key join Chapter on Chapter.id = Dictionary.chapter_id " .
-               "where RackObject.deleted = 'no' and Chapter.name = 'RackObjectType' " .
-               "and objtype_id = ${type_id} " .
-               "order by name";
-       $result = useSelectBlade ($query, __FUNCTION__);
-       while ($row = $result->fetch (PDO::FETCH_ASSOC))
-       {
-               foreach (array (
-                       'id',
-                       'name',
-                       'objtype_name',
-                       'objtype_id'
-                       ) as $cname)
-                       $ret[$row['id']][$cname] = $row[$cname];
-               $ret[$row['id']]['dname'] = displayedName ($ret[$row['id']]);
-       }
+       foreach ($wideList as $cell)
+               $ret[$cell['id']] = $cell['dname'];
        return $ret;
 }
 
-// Return a filtered, detailed object list.
-function getObjectList ($type_id = 0, $tagfilter = array(), $tfmode = 'any')
-{
-       $whereclause = getWhereClause ($tagfilter);
-       if ($type_id != 0)
-               $whereclause .= " and objtype_id = '${type_id}' ";
-       $query =
-               "select distinct RackObject.id as id , RackObject.name as name, dict_value as objtype_name, " .
-               "RackObject.label as label, RackObject.barcode as barcode, " .
-               "dict_key as objtype_id, asset_no, rack_id, Rack.name as Rack_name, Rack.row_id, " .
-               "(SELECT name from RackRow where id = Rack.row_id) AS Row_name " .
-               "from ((RackObject inner join Dictionary on objtype_id=dict_key join Chapter on Chapter.id = Dictionary.chapter_id) " .
-               "left join RackSpace on RackObject.id = object_id) " .
-               "left join Rack on rack_id = Rack.id " .
-               "left join TagStorage on RackObject.id = TagStorage.entity_id and entity_realm = 'object' " .
-               "where RackObject.deleted = 'no' and Chapter.name = 'RackObjectType' " .
-               $whereclause .
-               "order by name";
+// For a given realm return a list of entity records, each with
+// enough information for judgeCell() to execute.
+function listCells ($realm, $parent_id = 0)
+{
+       if (!$parent_id)
+       {
+               global $entityCache;
+               if (isset ($entityCache['complete'][$realm]))
+                       return $entityCache['complete'][$realm];
+       }
+       global $SQLSchema;
+       if (!isset ($SQLSchema[$realm]))
+               throw new RealmNotFoundException ($realm);
+       $SQLinfo = $SQLSchema[$realm];
+       $query = 'SELECT tag_id';
+       foreach ($SQLinfo['columns'] as $alias => $expression)
+               // Automatically prepend table name to each single column, but leave all others intact.
+               $query .= ', ' . ($alias == $expression ? "${SQLinfo['table']}.${alias}" : "${expression} as ${alias}");
+       $query .= " FROM ${SQLinfo['table']} LEFT JOIN TagStorage on entity_realm = '${realm}' and entity_id = ${SQLinfo['table']}.${SQLinfo['keycolumn']}";
+       if (isset ($SQLinfo['pidcolumn']) and $parent_id)
+               $query .= " WHERE ${SQLinfo['table']}.${SQLinfo['pidcolumn']} = ${parent_id}";
+       $query .= " ORDER BY ";
+       foreach ($SQLinfo['ordcolumns'] as $oc)
+               $query .= "${SQLinfo['table']}.${oc}, ";
+       $query .= " tag_id";
        $result = useSelectBlade ($query, __FUNCTION__);
        $ret = array();
+       global $taglist;
+       // Index returned result by the value of key column.
        while ($row = $result->fetch (PDO::FETCH_ASSOC))
        {
-               foreach (array (
-                       'id',
-                       'name',
-                       'label',
-                       'barcode',
-                       'objtype_name',
-                       'objtype_id',
-                       'asset_no',
-                       'rack_id',
-                       'Rack_name',
-                       'row_id',
-                       'Row_name'
-                       ) as $cname)
-                       $ret[$row['id']][$cname] = $row[$cname];
-               $ret[$row['id']]['dname'] = displayedName ($ret[$row['id']]);
+               $entity_id = $row[$SQLinfo['keycolumn']];
+               // Init the first record anyway, but store tag only if there is one.
+               if (!isset ($ret[$entity_id]))
+               {
+                       $ret[$entity_id] = array ('realm' => $realm);
+                       foreach (array_keys ($SQLinfo['columns']) as $alias)
+                               $ret[$entity_id][$alias] = $row[$alias];
+                       $ret[$entity_id]['etags'] = array();
+                       if ($row['tag_id'] != NULL && isset ($taglist[$row['tag_id']]))
+                               $ret[$entity_id]['etags'][] = array
+                               (
+                                       'id' => $row['tag_id'],
+                                       'tag' => $taglist[$row['tag_id']]['tag'],
+                                       'parent_id' => $taglist[$row['tag_id']]['parent_id'],
+                               );
+               }
+               elseif (isset ($taglist[$row['tag_id']]))
+                       // Meeting existing key later is always more tags on existing list.
+                       $ret[$entity_id]['etags'][] = array
+                       (
+                               'id' => $row['tag_id'],
+                               'tag' => $taglist[$row['tag_id']]['tag'],
+                               'parent_id' => $taglist[$row['tag_id']]['parent_id'],
+                       );
+       }
+       // Add necessary finish to the list before returning it. Maintain caches.
+       if (!$parent_id)
+               unset ($entityCache['partial'][$realm]);
+       foreach (array_keys ($ret) as $entity_id)
+       {
+               $ret[$entity_id]['etags'] = getExplicitTagsOnly ($ret[$entity_id]['etags']);
+               $ret[$entity_id]['itags'] = getImplicitTags ($ret[$entity_id]['etags']);
+               $ret[$entity_id]['atags'] = generateEntityAutoTags ($ret[$entity_id]);
+               switch ($realm)
+               {
+               case 'object':
+                       $ret[$entity_id]['dname'] = displayedName ($ret[$entity_id]);
+                       break;
+               case 'ipv4net':
+                       $ret[$entity_id]['ip_bin'] = ip2long ($ret[$entity_id]['ip']);
+                       $ret[$entity_id]['mask_bin'] = binMaskFromDec ($ret[$entity_id]['mask']);
+                       $ret[$entity_id]['mask_bin_inv'] = binInvMaskFromDec ($ret[$entity_id]['mask']);
+                       $ret[$entity_id]['db_first'] = sprintf ('%u', 0x00000000 + $ret[$entity_id]['ip_bin'] & $ret[$entity_id]['mask_bin']);
+                       $ret[$entity_id]['db_last'] = sprintf ('%u', 0x00000000 + $ret[$entity_id]['ip_bin'] | ($ret[$entity_id]['mask_bin_inv']));
+                       break;
+               default:
+                       break;
+               }
+               if (!$parent_id)
+                       $entityCache['complete'][$realm][$entity_id] = $ret[$entity_id];
+               else
+                       $entityCache['partial'][$realm][$entity_id] = $ret[$entity_id];
        }
-       $result->closeCursor();
        return $ret;
 }
 
-function getRacksForRow ($row_id = 0, $tagfilter = array(), $tfmode = 'any')
-{
-       $query =
-               "select Rack.id, Rack.name, height, Rack.comment, row_id, RackRow.name as row_name " .
-               "from Rack left join RackRow on Rack.row_id = RackRow.id " .
-               "left join TagStorage on Rack.id = TagStorage.entity_id and entity_realm = 'rack' " .
-               "where  Rack.deleted = 'no' " .
-               (($row_id == 0) ? "" : "and row_id = ${row_id} ") .
-               getWhereClause ($tagfilter) .
-               " order by row_name, Rack.name";
+// Very much like listCells(), but return only one record requested (or NULL,
+// if it does not exist).
+function spotEntity ($realm, $id)
+{
+       global $entityCache;
+       if (isset ($entityCache['complete'][$realm]))
+       // Emphasize the absence of record, if listCells() has already been called.
+               if (isset ($entityCache['complete'][$realm][$id]))
+                       return $entityCache['complete'][$realm][$id];
+               else
+                       throw new EntityNotFoundException ($realm, $id);
+       elseif (isset ($entityCache['partial'][$realm][$id]))
+               return $entityCache['partial'][$realm][$id];
+       global $SQLSchema;
+       if (!isset ($SQLSchema[$realm]))
+               throw new RealmNotFoundException ($realm);
+       $SQLinfo = $SQLSchema[$realm];
+       $query = 'SELECT tag_id';
+       foreach ($SQLinfo['columns'] as $alias => $expression)
+               // Automatically prepend table name to each single column, but leave all others intact.
+               $query .= ', ' . ($alias == $expression ? "${SQLinfo['table']}.${alias}" : "${expression} as ${alias}");
+       $query .= " FROM ${SQLinfo['table']} LEFT JOIN TagStorage on entity_realm = '${realm}' and entity_id = ${SQLinfo['table']}.${SQLinfo['keycolumn']}";
+       $query .= " WHERE ${SQLinfo['table']}.${SQLinfo['keycolumn']} = ${id}";
+       $query .= " ORDER BY tag_id";
        $result = useSelectBlade ($query, __FUNCTION__);
        $ret = array();
-       $clist = array
-       (
-               'id',
-               'name',
-               'height',
-               'comment',
-               'row_id',
-               'row_name'
-       );
+       global $taglist;
        while ($row = $result->fetch (PDO::FETCH_ASSOC))
-               foreach ($clist as $cname)
-                       $ret[$row['id']][$cname] = $row[$cname];
-       $result->closeCursor();
-       return $ret;
-}
-
-// This is a popular helper for getting information about
-// a particular rack and its rackspace at once.
-function getRackData ($rack_id = 0, $silent = FALSE)
-{
-       if ($rack_id == 0)
-       {
-               if ($silent == FALSE)
-                       showError ('Invalid rack_id', __FUNCTION__);
-               return NULL;
-       }
-       $query =
-               "select Rack.id, Rack.name, row_id, height, Rack.comment, RackRow.name as row_name from " .
-               "Rack left join RackRow on Rack.row_id = RackRow.id  " .
-               "where  Rack.id='${rack_id}' and Rack.deleted = 'no'";
-       $result = useSelectBlade ($query, __FUNCTION__);
-       if (($row = $result->fetch (PDO::FETCH_ASSOC)) == NULL)
-       {
-               if ($silent == FALSE)
-                       showError ('Query #1 succeded, but returned no data', __FUNCTION__);
-               return NULL;
-       }
-
-       // load metadata
-       $clist = array
-       (
-               'id',
-               'name',
-               'height',
-               'comment',
-               'row_id',
-               'row_name'
-       );
-       foreach ($clist as $cname)
-               $rack[$cname] = $row[$cname];
-       $result->closeCursor();
+               if (!isset ($ret['realm']))
+               {
+                       $ret = array ('realm' => $realm);
+                       foreach (array_keys ($SQLinfo['columns']) as $alias)
+                               $ret[$alias] = $row[$alias];
+                       $ret['etags'] = array();
+                       if ($row['tag_id'] != NULL && isset ($taglist[$row['tag_id']]))
+                               $ret['etags'][] = array
+                               (
+                                       'id' => $row['tag_id'],
+                                       'tag' => $taglist[$row['tag_id']]['tag'],
+                                       'parent_id' => $taglist[$row['tag_id']]['parent_id'],
+                               );
+               }
+               elseif (isset ($taglist[$row['tag_id']]))
+                       $ret['etags'][] = array
+                       (
+                               'id' => $row['tag_id'],
+                               'tag' => $taglist[$row['tag_id']]['tag'],
+                               'parent_id' => $taglist[$row['tag_id']]['parent_id'],
+                       );
        unset ($result);
-
-       // start with default rackspace
-       for ($i = $rack['height']; $i > 0; $i--)
-               for ($locidx = 0; $locidx < 3; $locidx++)
-                       $rack[$i][$locidx]['state'] = 'F';
-
-       // load difference
-       $query =
-               "select unit_no, atom, state, object_id " .
-               "from RackSpace where rack_id = ${rack_id} and " .
-               "unit_no between 1 and " . $rack['height'] . " order by unit_no";
-       $result = useSelectBlade ($query, __FUNCTION__);
-       global $loclist;
-       $mounted_objects = array();
-       while ($row = $result->fetch (PDO::FETCH_ASSOC))
-       {
-               $rack[$row['unit_no']][$loclist[$row['atom']]]['state'] = $row['state'];
-               $rack[$row['unit_no']][$loclist[$row['atom']]]['object_id'] = $row['object_id'];
-               if ($row['state'] == 'T' and $row['object_id']!=NULL)
-                       $mounted_objects[$row['object_id']] = TRUE;
-       }
-       $rack['mountedObjects'] = array_keys($mounted_objects);
-       $result->closeCursor();
-       return $rack;
+       if (!isset ($ret['realm'])) // no rows were returned
+               throw new EntityNotFoundException ($realm, $id);
+       $ret['etags'] = getExplicitTagsOnly ($ret['etags']);
+       $ret['itags'] = getImplicitTags ($ret['etags']);
+       $ret['atags'] = generateEntityAutoTags ($ret);
+       switch ($realm)
+       {
+       case 'object':
+               $ret['dname'] = displayedName ($ret);
+               break;
+       case 'ipv4net':
+               $ret['ip_bin'] = ip2long ($ret['ip']);
+               $ret['mask_bin'] = binMaskFromDec ($ret['mask']);
+               $ret['mask_bin_inv'] = binInvMaskFromDec ($ret['mask']);
+               $ret['db_first'] = sprintf ('%u', 0x00000000 + $ret['ip_bin'] & $ret['mask_bin']);
+               $ret['db_last'] = sprintf ('%u', 0x00000000 + $ret['ip_bin'] | ($ret['mask_bin_inv']));
+               break;
+       default:
+               break;
+       }
+       $entityCache['partial'][$realm][$id] = $ret;
+       return $ret;
 }
 
-// This is a popular helper.
-function getObjectInfo ($object_id = 0)
-{
-       if ($object_id == 0)
-       {
-               showError ('Invalid object_id', __FUNCTION__);
-               return;
-       }
-       $query =
-               "select RackObject.id as id, RackObject.name as name, label, barcode, dict_value as objtype_name, asset_no, dict_key as objtype_id, has_problems, comment from " .
-               "RackObject inner join Dictionary on objtype_id = dict_key join Chapter on Chapter.id = Dictionary.chapter_id " .
-               "where RackObject.id = '${object_id}' and RackObject.deleted = 'no' and Chapter.name = 'RackObjectType' limit 1";
-       $result = useSelectBlade ($query, __FUNCTION__);
-       if (($row = $result->fetch (PDO::FETCH_ASSOC)) == NULL)
-       {
-               showError ('Query succeeded, but returned no data', __FUNCTION__);
-               $ret = NULL;
-       }
-       else
-       {
-               $ret['id'] = $row['id'];
-               $ret['name'] = $row['name'];
-               $ret['label'] = $row['label'];
-               $ret['barcode'] = $row['barcode'];
-               $ret['objtype_name'] = $row['objtype_name'];
-               $ret['objtype_id'] = $row['objtype_id'];
-               $ret['has_problems'] = $row['has_problems'];
-               $ret['asset_no'] = $row['asset_no'];
-               $ret['dname'] = displayedName ($ret);
-               $ret['comment'] = $row['comment'];
+// This function can be used with array_walk().
+function amplifyCell (&$record, $dummy = NULL)
+{
+       switch ($record['realm'])
+       {
+       case 'object':
+               $record['ports'] = getObjectPortsAndLinks ($record['id']);
+               $record['ipv4'] = getObjectIPv4Allocations ($record['id']);
+               $record['nat4'] = getNATv4ForObject ($record['id']);
+               $record['ipv4rspools'] = getRSPoolsForObject ($record['id']);
+               $record['files'] = getFilesOfEntity ($record['realm'], $record['id']);
+               break;
+       case 'ipv4net':
+               $record['ip_bin'] = ip2long ($record['ip']);
+               $record['parent_id'] = getIPv4AddressNetworkId ($record['ip'], $record['mask']);
+               break;
+       case 'file':
+               $record['links'] = getFileLinks ($record['id']);
+               break;
+       case 'ipv4rspool':
+               $record['lblist'] = array();
+               $query = "select object_id, vs_id, lb.vsconfig, lb.rsconfig from " .
+                       "IPv4LB as lb inner join IPv4VS as vs on lb.vs_id = vs.id " .
+                       "where rspool_id = ${record['id']} order by object_id, vip, vport";
+               $result = useSelectBlade ($query, __FUNCTION__);
+               while ($row = $result->fetch (PDO::FETCH_ASSOC))
+                       $record['lblist'][$row['object_id']][$row['vs_id']] = array
+                       (
+                               'rsconfig' => $row['rsconfig'],
+                               'vsconfig' => $row['vsconfig'],
+                       );
+               unset ($result);
+               $record['rslist'] = array();
+               $query = "select id, inservice, inet_ntoa(rsip) as rsip, rsport, rsconfig from " .
+                       "IPv4RS where rspool_id = ${record['id']} order by IPv4RS.rsip, rsport";
+               $result = useSelectBlade ($query, __FUNCTION__);
+               while ($row = $result->fetch (PDO::FETCH_ASSOC))
+                       $record['rslist'][$row['id']] = array
+                       (
+                               'inservice' => $row['inservice'],
+                               'rsip' => $row['rsip'],
+                               'rsport' => $row['rsport'],
+                               'rsconfig' => $row['rsconfig'],
+                       );
+               unset ($result);
+               break;
+       case 'ipv4vs':
+               // Get the detailed composition of a particular virtual service, namely the list
+               // of all pools, each shown with the list of objects servicing it. VS/RS configs
+               // will be returned as well.
+               $record['rspool'] = array();
+               $query = "select pool.id, name, pool.vsconfig, pool.rsconfig, object_id, " .
+                       "lb.vsconfig as lb_vsconfig, lb.rsconfig as lb_rsconfig from " .
+                       "IPv4RSPool as pool left join IPv4LB as lb on pool.id = lb.rspool_id " .
+                       "where vs_id = ${record['id']} order by pool.name, object_id";
+               $result = useSelectBlade ($query, __FUNCTION__);
+               while ($row = $result->fetch (PDO::FETCH_ASSOC))
+               {
+                       if (!isset ($record['rspool'][$row['id']]))
+                               $record['rspool'][$row['id']] = array
+                               (
+                                       'name' => $row['name'],
+                                       'vsconfig' => $row['vsconfig'],
+                                       'rsconfig' => $row['rsconfig'],
+                                       'lblist' => array(),
+                               );
+                       if ($row['object_id'] == NULL)
+                               continue;
+                       $record['rspool'][$row['id']]['lblist'][$row['object_id']] = array
+                       (
+                               'vsconfig' => $row['lb_vsconfig'],
+                               'rsconfig' => $row['lb_rsconfig'],
+                       );
+               }
+               unset ($result);
+               break;
+       case 'rack':
+               $record['mountedObjects'] = array();
+               // start with default rackspace
+               for ($i = $record['height']; $i > 0; $i--)
+                       for ($locidx = 0; $locidx < 3; $locidx++)
+                               $record[$i][$locidx]['state'] = 'F';
+               // load difference
+               $query =
+                       "select unit_no, atom, state, object_id " .
+                       "from RackSpace where rack_id = ${record['id']} and " .
+                       "unit_no between 1 and " . $record['height'] . " order by unit_no";
+               $result = useSelectBlade ($query, __FUNCTION__);
+               global $loclist;
+               $mounted_objects = array();
+               while ($row = $result->fetch (PDO::FETCH_ASSOC))
+               {
+                       $record[$row['unit_no']][$loclist[$row['atom']]]['state'] = $row['state'];
+                       $record[$row['unit_no']][$loclist[$row['atom']]]['object_id'] = $row['object_id'];
+                       if ($row['state'] == 'T' and $row['object_id'] != NULL)
+                               $mounted_objects[$row['object_id']] = TRUE;
+               }
+               $record['mountedObjects'] = array_keys ($mounted_objects);
+               unset ($result);
+               break;
+       default:
        }
-       $result->closeCursor();
-       unset ($result);
-       return $ret;
 }
 
 function getPortTypes ()
@@ -365,13 +523,8 @@ function getPortTypes ()
        return readChapter ('PortType');
 }
 
-function getObjectPortsAndLinks ($object_id = 0)
+function getObjectPortsAndLinks ($object_id)
 {
-       if ($object_id == 0)
-       {
-               showError ('Invalid object_id', __FUNCTION__);
-               return;
-       }
        // prepare decoder
        $ptd = readChapter ('PortType');
        $query = "select id, name, label, l2address, type as type_id, reservation_comment from Port where object_id = ${object_id}";
@@ -386,10 +539,12 @@ function getObjectPortsAndLinks ($object_id = 0)
                $row['remote_name'] = NULL;
                $row['remote_object_id'] = NULL;
                $row['remote_object_name'] = NULL;
+               $row['remote_type_id'] = NULL;
                $ret[] = $row;
        }
        unset ($result);
        // now find and decode remote ends for all locally terminated connections
+       // FIXME: can't this data be extracted in one pass with sub-queries?
        foreach (array_keys ($ret) as $tmpkey)
        {
                $portid = $ret[$tmpkey]['id'];
@@ -415,14 +570,15 @@ function getObjectPortsAndLinks ($object_id = 0)
                                $ret[$tmpkey]['remote_name'] = $row['port_name'];
                                $ret[$tmpkey]['remote_object_id'] = $row['object_id'];
                                $ret[$tmpkey]['remote_object_name'] = $row['object_name'];
+                               $ret[$tmpkey]['remote_type_id'] = $row['port_type'];
                        }
                        $ret[$tmpkey]['remote_id'] = $remote_id;
                        unset ($result);
                        // only call displayedName() when necessary
-                       if (empty ($ret[$tmpkey]['remote_object_name']) and !empty ($ret[$tmpkey]['remote_object_id']))
+                       if (!strlen ($ret[$tmpkey]['remote_object_name']) and strlen ($ret[$tmpkey]['remote_object_id']))
                        {
-                               $oi = getObjectInfo ($ret[$tmpkey]['remote_object_id']);
-                               $ret[$tmpkey]['remote_object_name'] = displayedName ($oi);
+                               $oi = spotEntity ('object', $ret[$tmpkey]['remote_object_id']);
+                               $ret[$tmpkey]['remote_object_name'] = $oi['dname'];
                        }
                }
        }
@@ -431,7 +587,7 @@ function getObjectPortsAndLinks ($object_id = 0)
 
 function commitAddRack ($name, $height = 0, $row_id = 0, $comment, $taglist)
 {
-       if ($row_id <= 0 or $height <= 0 or empty ($name))
+       if ($row_id <= 0 or $height <= 0 or !strlen ($name))
                return FALSE;
        $result = useInsertBlade
        (
@@ -463,11 +619,11 @@ function commitAddObject ($new_name, $new_label, $new_barcode, $new_type_id, $ne
                'RackObject',
                array
                (
-                       'name' => empty ($new_name) ? 'NULL' : "'${new_name}'",
+                       'name' => !strlen ($new_name) ? 'NULL' : "'${new_name}'",
                        'label' => "'${new_label}'",
-                       'barcode' => empty ($new_barcode) ? 'NULL' : "'${new_barcode}'",
+                       'barcode' => !strlen ($new_barcode) ? 'NULL' : "'${new_barcode}'",
                        'objtype_id' => $new_type_id,
-                       'asset_no' => empty ($new_asset_no) ? 'NULL' : "'${new_asset_no}'"
+                       'asset_no' => !strlen ($new_asset_no) ? 'NULL' : "'${new_asset_no}'"
                )
        );
        if ($result1 == NULL)
@@ -490,15 +646,12 @@ function commitAddObject ($new_name, $new_label, $new_barcode, $new_type_id, $ne
 
 function commitUpdateObject ($object_id = 0, $new_name = '', $new_label = '', $new_barcode = '', $new_type_id = 0, $new_has_problems = 'no', $new_asset_no = '', $new_comment = '')
 {
-       if ($object_id == 0 || $new_type_id == 0)
-       {
-               showError ('Not all required args are present.', __FUNCTION__);
-               return FALSE;
-       }
+       if ($new_type_id == 0)
+               throw new InvalidArgException ('$new_type_id', $new_type_id);
        global $dbxlink;
-       $new_asset_no = empty ($new_asset_no) ? 'NULL' : "'${new_asset_no}'";
-       $new_barcode = empty ($new_barcode) ? 'NULL' : "'${new_barcode}'";
-       $new_name = empty ($new_name) ? 'NULL' : "'${new_name}'";
+       $new_asset_no = !strlen ($new_asset_no) ? 'NULL' : "'${new_asset_no}'";
+       $new_barcode = !strlen ($new_barcode) ? 'NULL' : "'${new_barcode}'";
+       $new_name = !strlen ($new_name) ? 'NULL' : "'${new_name}'";
        $query = "update RackObject set name=${new_name}, label='${new_label}', barcode=${new_barcode}, objtype_id='${new_type_id}', " .
                "has_problems='${new_has_problems}', asset_no=${new_asset_no}, comment='${new_comment}' " .
                "where id='${object_id}' limit 1";
@@ -512,23 +665,25 @@ function commitUpdateObject ($object_id = 0, $new_name = '', $new_label = '', $n
        return recordHistory ('RackObject', "id = ${object_id}");
 }
 
+// Remove file links related to the entity, but leave the entity and file(s) intact.
+function releaseFiles ($entity_realm, $entity_id)
+{
+       global $dbxlink;
+       $dbxlink->exec ("DELETE FROM FileLink WHERE entity_type = '${entity_realm}' AND entity_id = ${entity_id}");
+}
+
 // There are times when you want to delete all traces of an object
 function commitDeleteObject ($object_id = 0)
 {
-       if ($object_id <= 0)
-       {
-               showError ('Invalid args', __FUNCTION__);
-               die;
-       }
        global $dbxlink;
+       releaseFiles ('object', $object_id);
+       destroyTagsForEntity ('object', $object_id);
        $dbxlink->query("DELETE FROM AttributeValue WHERE object_id = ${object_id}");
-       $dbxlink->query("DELETE FROM File WHERE id IN (SELECT file_id FROM FileLink WHERE entity_id = 'object' AND entity_id = ${object_id})");
        $dbxlink->query("DELETE FROM IPv4LB WHERE object_id = ${object_id}");
        $dbxlink->query("DELETE FROM IPv4Allocation WHERE object_id = ${object_id}");
        $dbxlink->query("DELETE FROM Port WHERE object_id = ${object_id}");
        $dbxlink->query("DELETE FROM IPv4NAT WHERE object_id = ${object_id}");
        $dbxlink->query("DELETE FROM RackSpace WHERE object_id = ${object_id}");
-       $dbxlink->query("DELETE FROM TagStorage WHERE entity_realm = 'object' and entity_id = ${object_id}");
        $dbxlink->query("DELETE FROM Atom WHERE molecule_id IN (SELECT new_molecule_id FROM MountOperation WHERE object_id = ${object_id})");
        $dbxlink->query("DELETE FROM Molecule WHERE id IN (SELECT new_molecule_id FROM MountOperation WHERE object_id = ${object_id})");
        $dbxlink->query("DELETE FROM MountOperation WHERE object_id = ${object_id}");
@@ -541,10 +696,10 @@ function commitDeleteObject ($object_id = 0)
 function commitDeleteRack($rack_id)
 {
        global $dbxlink;
+       releaseFiles ('rack', $rack_id);
+       destroyTagsForEntity ('rack', $rack_id);
        $query = "delete from RackSpace where rack_id = '${rack_id}'";
        $dbxlink->query ($query);
-       $query = "delete from TagStorage where entity_realm='rack' and entity_id='${rack_id}'";
-       $dbxlink->query ($query);
        $query = "delete from RackHistory where id = '${rack_id}'";
        $dbxlink->query ($query);
        $query = "delete from Rack where id = '${rack_id}'";
@@ -554,16 +709,27 @@ function commitDeleteRack($rack_id)
 
 function commitUpdateRack ($rack_id, $new_name, $new_height, $new_row_id, $new_comment)
 {
-       if (empty ($rack_id) || empty ($new_name) || empty ($new_height))
+       if (!strlen ($rack_id) || !strlen ($new_name) || !strlen ($new_height))
        {
                showError ('Not all required args are present.', __FUNCTION__);
                return FALSE;
        }
        global $dbxlink;
-       $query = "update Rack set name='${new_name}', height='${new_height}', comment='${new_comment}', row_id=${new_row_id} " .
+
+       // Can't shrink a rack if rows being deleted contain mounted objects
+       $check_sql = "SELECT COUNT(*) AS count FROM RackSpace WHERE rack_id = '${rack_id}' AND unit_no > '{$new_height}'";
+       $check_result = $dbxlink->query($check_sql);
+       $check_row = $check_result->fetch (PDO::FETCH_ASSOC);
+       unset ($check_result);
+       if ($check_row['count'] > 0) {
+               showError ('Cannot shrink rack, objects are still mounted there', __FUNCTION__);
+               return FALSE;
+       }
+
+       $update_sql = "update Rack set name='${new_name}', height='${new_height}', comment='${new_comment}', row_id=${new_row_id} " .
                "where id='${rack_id}' limit 1";
-       $result1 = $dbxlink->query ($query);
-       if ($result1->rowCount() != 1)
+       $update_result = $dbxlink->query ($update_sql);
+       if ($update_result->rowCount() != 1)
        {
                showError ('Error updating rack information', __FUNCTION__);
                return FALSE;
@@ -571,7 +737,7 @@ function commitUpdateRack ($rack_id, $new_name, $new_height, $new_row_id, $new_c
        return recordHistory ('Rack', "id = ${rack_id}");
 }
 
-// This function accepts rack data returned by getRackData(), validates and applies changes
+// This function accepts rack data returned by amplifyCell(), validates and applies changes
 // supplied in $_REQUEST and returns resulting array. Only those changes are examined, which
 // correspond to current rack ID.
 // 1st arg is rackdata, 2nd arg is unchecked state, 3rd arg is checked state.
@@ -651,11 +817,6 @@ function processGridForm (&$rackData, $unchecked_state, $checked_state, $object_
 // the requested object.
 function getMoleculeForObject ($object_id = 0)
 {
-       if ($object_id == 0)
-       {
-               showError ("object_id == 0", __FUNCTION__);
-               return NULL;
-       }
        $query =
                "select rack_id, unit_no, atom from RackSpace " .
                "where state = 'T' and object_id = ${object_id} order by rack_id, unit_no, atom";
@@ -668,11 +829,6 @@ function getMoleculeForObject ($object_id = 0)
 // This function builds a list of rack-unit-atom records for requested molecule.
 function getMolecule ($mid = 0)
 {
-       if ($mid == 0)
-       {
-               showError ("mid == 0", __FUNCTION__);
-               return NULL;
-       }
        $query =
                "select rack_id, unit_no, atom from Atom " .
                "where molecule_id=${mid}";
@@ -759,11 +915,6 @@ function getRackspaceHistory ()
 // This function is used in renderRackspaceHistory()
 function getOperationMolecules ($op_id = 0)
 {
-       if ($op_id <= 0)
-       {
-               showError ("Missing argument", __FUNCTION__);
-               return;
-       }
        $query = "select old_molecule_id, new_molecule_id from MountOperation where id = ${op_id}";
        $result = useSelectBlade ($query, __FUNCTION__);
        // We expect one row.
@@ -781,15 +932,10 @@ function getOperationMolecules ($op_id = 0)
 
 function getResidentRacksData ($object_id = 0, $fetch_rackdata = TRUE)
 {
-       if ($object_id <= 0)
-       {
-               showError ('Invalid object_id', __FUNCTION__);
-               return;
-       }
        $query = "select distinct rack_id from RackSpace where object_id = ${object_id} order by rack_id";
        $result = useSelectBlade ($query, __FUNCTION__);
        $rows = $result->fetchAll (PDO::FETCH_NUM);
-       $result->closeCursor();
+       unset ($result);
        $ret = array();
        foreach ($rows as $row)
        {
@@ -798,91 +944,19 @@ function getResidentRacksData ($object_id = 0, $fetch_rackdata = TRUE)
                        $ret[$row[0]] = $row[0];
                        continue;
                }
-               $rackData = getRackData ($row[0]);
-               if ($rackData == NULL)
+               if (NULL == ($rackData = spotEntity ('rack', $row[0])))
                {
-                       showError ('getRackData() failed', __FUNCTION__);
+                       showError ('Rack not found', __FUNCTION__);
                        return NULL;
                }
+               amplifyCell ($rackData);
                $ret[$row[0]] = $rackData;
        }
-       $result->closeCursor();
-       return $ret;
-}
-
-function getObjectGroupInfo ()
-{
-       $query =
-               'select dict_key as id, dict_value as name, count(dict_key) as count from ' .
-               'Dictionary join Chapter on Chapter.id = Dictionary.chapter_id join RackObject on dict_key = objtype_id ' .
-               'where Chapter.name = "RackObjectType" ' .
-               'group by dict_key order by dict_value';
-       $result = useSelectBlade ($query, __FUNCTION__);
-       $ret = array();
-       $ret[0] = array ('id' => 0, 'name' => 'ALL types');
-       $clist = array ('id', 'name', 'count');
-       $total = 0;
-       while ($row = $result->fetch (PDO::FETCH_ASSOC))
-               if ($row['count'] > 0)
-               {
-                       $total += $row['count'];
-                       foreach ($clist as $cname)
-                               $ret[$row['id']][$cname] = $row[$cname];
-               }
-       $result->closeCursor();
-       $ret[0]['count'] = $total;
-       return $ret;
-}
-
-// This function returns objects, which have no rackspace assigned to them.
-// Additionally it keeps rack_id parameter, so we can silently pre-select
-// the rack required.
-function getUnmountedObjects ()
-{
-       $query =
-               'select dict_value as objtype_name, dict_key as objtype_id, name, label, barcode, id, asset_no from ' .
-               'RackObject inner join Dictionary on objtype_id = dict_key join Chapter on Chapter.id = Dictionary.chapter_id ' .
-               'left join RackSpace on id = object_id '.
-               'where rack_id is null and Chapter.name = "RackObjectType" order by dict_value, name, label, asset_no, barcode';
-       $result = useSelectBlade ($query, __FUNCTION__);
-       $ret = array();
-       $clist = array ('id', 'name', 'label', 'barcode', 'objtype_name', 'objtype_id', 'asset_no');
-       while ($row = $result->fetch (PDO::FETCH_ASSOC))
-       {
-               foreach ($clist as $cname)
-                       $ret[$row['id']][$cname] = $row[$cname];
-               $ret[$row['id']]['dname'] = displayedName ($ret[$row['id']]);
-       }
-       $result->closeCursor();
-       return $ret;
-}
-
-function getProblematicObjects ()
-{
-       $query =
-               'select dict_value as objtype_name, dict_key as objtype_id, name, id, asset_no from ' .
-               'RackObject inner join Dictionary on objtype_id = dict_key join Chapter on Chapter.id = Dictionary.chapter_id '.
-               'where has_problems = "yes" and Chapter.name = "RackObjectType" order by objtype_name, name';
-       $result = useSelectBlade ($query, __FUNCTION__);
-       $ret = array();
-       $clist = array ('id', 'name', 'objtype_name', 'objtype_id', 'asset_no');
-       while ($row = $result->fetch (PDO::FETCH_ASSOC))
-       {
-               foreach ($clist as $cname)
-                       $ret[$row['id']][$cname] = $row[$cname];
-               $ret[$row['id']]['dname'] = displayedName ($ret[$row['id']]);
-       }
-       $result->closeCursor();
        return $ret;
 }
 
 function commitAddPort ($object_id = 0, $port_name, $port_type_id, $port_label, $port_l2address)
 {
-       if ($object_id <= 0)
-       {
-               showError ('Invalid object_id', __FUNCTION__);
-               return;
-       }
        if (NULL === ($db_l2address = l2addressForDatabase ($port_l2address)))
                return "Invalid L2 address ${port_l2address}";
        $result = useInsertBlade
@@ -1068,7 +1142,7 @@ function scanIPv4Space ($pairlist)
 {
        $ret = array();
        if (!count ($pairlist)) // this is normal for a network completely divided into smaller parts
-               return $ret;;
+               return $ret;
        $dnamechache = array();
        // FIXME: this is a copy-and-paste prototype
        $or = '';
@@ -1129,6 +1203,7 @@ function scanIPv4Space ($pairlist)
                        $ret[$ip_bin] = constructIPv4Address ($row['ip']);
                if (!isset ($dnamecache[$row['object_id']]))
                {
+                       $quasiobject['id'] = $row['object_id'];
                        $quasiobject['name'] = $row['object_name'];
                        $quasiobject['objtype_id'] = $row['objtype_id'];
                        $quasiobject['objtype_name'] = $row['objtype_name'];
@@ -1237,37 +1312,10 @@ function scanIPv4Space ($pairlist)
        return $ret;
 }
 
-// Return summary data about an IPv4 prefix, if it exists, or NULL otherwise.
-function getIPv4NetworkInfo ($id = 0)
-{
-       if ($id <= 0)
-       {
-               showError ('Invalid arg', __FUNCTION__);
-               return NULL;
-       }
-       $query = "select INET_NTOA(ip) as ip, mask, name ".
-               "from IPv4Network where id = $id";
-       $result = useSelectBlade ($query, __FUNCTION__);
-       $ret = $result->fetch (PDO::FETCH_ASSOC);
-       if ($ret == NULL)
-               return NULL;
-       unset ($result);
-       $ret['id'] = $id;
-       $ret['ip_bin'] = ip2long ($ret['ip']);
-       $ret['mask_bin'] = binMaskFromDec ($ret['mask']);
-       $ret['mask_bin_inv'] = binInvMaskFromDec ($ret['mask']);
-       $ret['db_first'] = sprintf ('%u', 0x00000000 + $ret['ip_bin'] & $ret['mask_bin']);
-       $ret['db_last'] = sprintf ('%u', 0x00000000 + $ret['ip_bin'] | ($ret['mask_bin_inv']));
-       return $ret;
-}
-
 function getIPv4Address ($dottedquad = '')
 {
        if ($dottedquad == '')
-       {
-               showError ('Invalid arg', __FUNCTION__);
-               return NULL;
-       }
+               throw new InvalidArgException ('$dottedquad', $dottedquad);
        $i32 = ip2long ($dottedquad); // signed 32 bit
        $scanres = scanIPv4Space (array (array ('i32_first' => $i32, 'i32_last' => $i32)));
        if (!isset ($scanres[$i32]))
@@ -1293,44 +1341,11 @@ function bindIpToObject ($ip = '', $object_id = 0, $name = '', $type = '')
        return $result ? '' : (__FUNCTION__ . '(): useInsertBlade() failed');
 }
 
-// Collect data necessary to build a tree. Calling functions should care about
-// setting the rest of data.
-function getIPv4NetworkList ($tagfilter = array(), $tfmode = 'any')
-{
-       $whereclause = getWhereClause ($tagfilter);
-       $query =
-               "select distinct id, INET_NTOA(ip) as ip, mask, name " .
-               "from IPv4Network left join TagStorage on id = entity_id and entity_realm = 'ipv4net' " .
-               "where true ${whereclause} order by IPv4Network.ip";
-       $result = useSelectBlade ($query, __FUNCTION__);
-       $ret = array();
-       while ($row = $result->fetch (PDO::FETCH_ASSOC))
-       {
-               // ip_bin and mask are used by iptree_fill()
-               $row['ip_bin'] = ip2long ($row['ip']);
-               $ret[$row['id']] = $row;
-       }
-       // After all the keys are known we can update parent_id appropriately. Also we don't
-       // run two queries in parallel this way.
-       $keys = array_keys ($ret);
-       foreach ($keys as $netid)
-       {
-               // parent_id is for treeFromList()
-               $ret[$netid]['parent_id'] = getIPv4AddressNetworkId ($ret[$netid]['ip'], $ret[$netid]['mask']);
-               if ($ret[$netid]['parent_id'] and !in_array ($ret[$netid]['parent_id'], $keys))
-               {
-                       $ret[$netid]['real_parent_id'] = $ret[$netid]['parent_id'];
-                       $ret[$netid]['parent_id'] = NULL;
-               }
-       }
-       return $ret;
-}
-
-// Return the id of the smallest IPv4 network containing the given IPv4 address
-// or NULL, if nothing was found. When finding the covering network for
-// another network, it is important to filter out matched records with longer
-// masks (they aren't going to be the right pick).
-function getIPv4AddressNetworkId ($dottedquad, $masklen = 32)
+// Return the id of the smallest IPv4 network containing the given IPv4 address
+// or NULL, if nothing was found. When finding the covering network for
+// another network, it is important to filter out matched records with longer
+// masks (they aren't going to be the right pick).
+function getIPv4AddressNetworkId ($dottedquad, $masklen = 32)
 {
 // N.B. To perform the same for IPv6 address and networks, some pre-requisites
 // are necessary and a different query. IPv6 addresses are 128 bit long, which
@@ -1403,26 +1418,6 @@ function unbindIpFromObject ($ip='', $object_id=0)
        return '';
 }
 
-// This function returns either all or one user account. Array key is user name.
-function getUserAccounts ($tagfilter = array(), $tfmode = 'any')
-{
-       $whereclause = getWhereClause ($tagfilter);
-       $query =
-               'select user_id, user_name, user_password_hash, user_realname ' .
-               'from UserAccount left join TagStorage ' .
-               "on UserAccount.user_id = TagStorage.entity_id and entity_realm = 'user' " .
-               "where true ${whereclause} " .
-               'order by user_name';
-       $result = useSelectBlade ($query, __FUNCTION__);
-       $ret = array();
-       $clist = array ('user_id', 'user_name', 'user_realname', 'user_password_hash');
-       while ($row = $result->fetch (PDO::FETCH_ASSOC))
-               foreach ($clist as $cname)
-                       $ret[$row['user_name']][$cname] = $row[$cname];
-       $result->closeCursor();
-       return $ret;
-}
-
 function searchByl2address ($port_l2address)
 {
        if (NULL === ($db_l2address = l2addressForDatabase ($port_l2address)))
@@ -1442,17 +1437,17 @@ function searchByl2address ($port_l2address)
 
 function getIPv4PrefixSearchResult ($terms)
 {
-       $query = "select id, inet_ntoa(ip) as ip, mask, name from IPv4Network where ";
-       $or = '';
-       foreach (explode (' ', $terms) as $term)
-       {
-               $query .= $or . "name like '%${term}%'";
-               $or = ' or ';
-       }
-       $result = useSelectBlade ($query, __FUNCTION__);
+       $byname = getSearchResultByField
+       (
+               'IPv4Network',
+               array ('id'),
+               'name',
+               $terms,
+               'ip'
+       );
        $ret = array();
-       while ($row = $result->fetch (PDO::FETCH_ASSOC))
-               $ret[] = $row;
+       foreach ($byname as $row)
+               $ret[] = spotEntity ('ipv4net', $row['id']);
        return $ret;
 }
 
@@ -1474,33 +1469,33 @@ function getIPv4AddressSearchResult ($terms)
 
 function getIPv4RSPoolSearchResult ($terms)
 {
-       $query = "select id as pool_id, name from IPv4RSPool where ";
-       $or = '';
-       foreach (explode (' ', $terms) as $term)
-       {
-               $query .= $or . "name like '%${term}%'";
-               $or = ' or ';
-       }
-       $result = useSelectBlade ($query, __FUNCTION__);
+       $byname = getSearchResultByField
+       (
+               'IPv4RSPool',
+               array ('id'),
+               'name',
+               $terms,
+               'name'
+       );
        $ret = array();
-       while ($row = $result->fetch (PDO::FETCH_ASSOC))
-               $ret[] = $row;
+       foreach ($byname as $row)
+               $ret[] = spotEntity ('ipv4rspool', $row['id']);
        return $ret;
 }
 
 function getIPv4VServiceSearchResult ($terms)
 {
-       $query = "select id, inet_ntoa(vip) as vip, vport, proto, name from IPv4VS where ";
-       $or = '';
-       foreach (explode (' ', $terms) as $term)
-       {
-               $query .= $or . "name like '%${term}%'";
-               $or = ' or ';
-       }
-       $result = useSelectBlade ($query, __FUNCTION__);
+       $byname = getSearchResultByField
+       (
+               'IPv4VS',
+               array ('id'),
+               'name',
+               $terms,
+               'vip'
+       );
        $ret = array();
-       while ($row = $result->fetch (PDO::FETCH_ASSOC))
-               $ret[] = $row;
+       foreach ($byname as $row)
+               $ret[] = spotEntity ('ipv4vs', $row['id']);
        return $ret;
 }
 
@@ -1530,15 +1525,19 @@ function getAccountSearchResult ($terms)
                                unset ($byRealname[$key2]);
                                continue 2;
                        }
-       return array_merge ($byUsername, $byRealname);
+       $ret = array_merge ($byUsername, $byRealname);
+       // Set realm, so it's renderable.
+       foreach (array_keys ($ret) as $key)
+               $ret[$key]['realm'] = 'user';
+       return $ret;
 }
 
 function getFileSearchResult ($terms)
 {
-       $byFilename = getSearchResultByField
+       $byName = getSearchResultByField
        (
                'File',
-               array ('id', 'name', 'comment'),
+               array ('id'),
                'name',
                $terms,
                'name'
@@ -1546,20 +1545,55 @@ function getFileSearchResult ($terms)
        $byComment = getSearchResultByField
        (
                'File',
-               array ('id', 'name', 'comment'),
+               array ('id'),
+               'comment',
+               $terms,
+               'name'
+       );
+       // Filter out dupes.
+       foreach ($byName as $res1)
+               foreach (array_keys ($byComment) as $key2)
+                       if ($res1['id'] == $byComment[$key2]['id'])
+                       {
+                               unset ($byComment[$key2]);
+                               continue 2;
+                       }
+       $ret = array();
+       foreach (array_merge ($byName, $byComment) as $row)
+               $ret[] = spotEntity ('file', $row['id']);
+       return $ret;
+}
+
+function getRackSearchResult ($terms)
+{
+       $byName = getSearchResultByField
+       (
+               'Rack',
+               array ('id'),
+               'name',
+               $terms,
+               'name'
+       );
+       $byComment = getSearchResultByField
+       (
+               'Rack',
+               array ('id'),
                'comment',
                $terms,
                'name'
        );
        // Filter out dupes.
-       foreach ($byFilename as $res1)
+       foreach ($byName as $res1)
                foreach (array_keys ($byComment) as $key2)
                        if ($res1['id'] == $byComment[$key2]['id'])
                        {
                                unset ($byComment[$key2]);
                                continue 2;
                        }
-       return array_merge ($byFilename, $byComment);
+       $ret = array();
+       foreach (array_merge ($byName, $byComment) as $row)
+               $ret[] = spotEntity ('rack', $row['id']);
+       return $ret;
 }
 
 function getSearchResultByField ($tname, $rcolumns, $scolumn, $terms, $ocolumn = '')
@@ -1680,12 +1714,14 @@ function addPortCompat ($type1 = 0, $type2 = 0)
 // This function returns the dictionary as an array of trees, each tree
 // representing a single chapter. Each element has 'id', 'name', 'sticky'
 // and 'word' keys with the latter holding all the words within the chapter.
+// FIXME: there's a lot of excess legacy code in this function, it's reasonable
+// to merge it with readChapter().
 function getDict ($parse_links = FALSE)
 {
-       $query1 =
+       $query =
                "select Chapter.name as chapter_name, Chapter.id as chapter_no, dict_key, dict_value, sticky from " .
                "Chapter left join Dictionary on Chapter.id = Dictionary.chapter_id order by Chapter.name, dict_value";
-       $result = useSelectBlade ($query1, __FUNCTION__);
+       $result = useSelectBlade ($query, __FUNCTION__);
        $dict = array();
        while ($row = $result->fetch (PDO::FETCH_ASSOC))
        {
@@ -1704,27 +1740,41 @@ function getDict ($parse_links = FALSE)
                        $dict[$chapter_no]['refcnt'][$row['dict_key']] = 0;
                }
        }
-       $result->closeCursor();
        unset ($result);
-// Find the list of all assigned values of dictionary-addressed attributes, each with
-// chapter/word keyed reference counters. Use the structure to adjust reference counters
-// of the returned disctionary words.
-       $query2 = "select a.id as attr_id, am.chapter_id as chapter_no, uint_value, count(object_id) as refcnt " .
+       // Find the list of all assigned values of dictionary-addressed attributes, each with
+       // chapter/word keyed reference counters. Use the structure to adjust reference counters
+       // of the returned disctionary words.
+       $query = "select a.id as attr_id, am.chapter_id as chapter_no, uint_value, count(object_id) as refcnt " .
                "from Attribute as a inner join AttributeMap as am on a.id = am.attr_id " .
                "inner join AttributeValue as av on a.id = av.attr_id " .
                "inner join Dictionary as d on am.chapter_id = d.chapter_id and av.uint_value = d.dict_key " .
                "where a.type = 'dict' group by a.id, am.chapter_id, uint_value " .
                "order by a.id, am.chapter_id, uint_value";
-       $result = useSelectBlade ($query2, __FUNCTION__);
+       $result = useSelectBlade ($query, __FUNCTION__);
        while ($row = $result->fetch (PDO::FETCH_ASSOC))
                $dict[$row['chapter_no']]['refcnt'][$row['uint_value']] = $row['refcnt'];
-       $result->closeCursor();
+       unset ($result);
+       // PortType chapter is referenced by PortCompat and Port tables
+       $query = 'select dict_key as uint_value, chapter_id as chapter_no, (select count(*) from PortCompat where type1 = dict_key or type2 = dict_key) + ' .
+               '(select count(*) from Port where type = dict_key) as refcnt ' .
+               'from Dictionary where chapter_id = 2';
+       $result = useSelectBlade ($query, __FUNCTION__);
+       while ($row = $result->fetch (PDO::FETCH_ASSOC))
+               $dict[$row['chapter_no']]['refcnt'][$row['uint_value']] = $row['refcnt'];
+       unset ($result);
+       // RackObjectType chapter is referenced by AttributeMap and RackObject tables
+       $query = 'select dict_key as uint_value, chapter_id as chapter_no, (select count(*) from AttributeMap where objtype_id = dict_key) + ' .
+               '(select count(*) from RackObject where objtype_id = dict_key) as refcnt from Dictionary where chapter_id = 1';
+       $result = useSelectBlade ($query, __FUNCTION__);
+       while ($row = $result->fetch (PDO::FETCH_ASSOC))
+               $dict[$row['chapter_no']]['refcnt'][$row['uint_value']] = $row['refcnt'];
+       unset ($result);
        return $dict;
 }
 
 function getDictStats ()
 {
-       $stock_chapters = array (1, 2, 11, 12, 13, 14, 16, 17, 18, 19, 20, 21, 22, 23);
+       $stock_chapters = array (1, 2, 11, 12, 13, 14, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26);
        $query =
                "select Chapter.id as chapter_no, Chapter.name as chapter_name, count(dict_key) as wc from " .
                "Chapter left join Dictionary on Chapter.id = Dictionary.chapter_id group by Chapter.id";
@@ -1803,7 +1853,7 @@ function getRackspaceStats ()
        {
                $result = useSelectBlade ($item['q'], __FUNCTION__);
                $row = $result->fetch (PDO::FETCH_NUM);
-               $ret[$item['txt']] = empty ($row[0]) ? 0 : $row[0];
+               $ret[$item['txt']] = !strlen ($row[0]) ? 0 : $row[0];
                $result->closeCursor();
                unset ($result);
        }
@@ -1825,11 +1875,11 @@ function renderTagStats ()
        echo '<th>IPv4 VS</th><th>IPv4 RS pools</th><th>users</th><th>files</th></tr>';
        $pagebyrealm = array
        (
-               'file' => 'filesbylink&entity_type=all',
+               'file' => 'files&tab=default',
                'ipv4net' => 'ipv4space&tab=default',
                'ipv4vs' => 'ipv4vslist&tab=default',
                'ipv4rspool' => 'ipv4rsplist&tab=default',
-               'object' => 'objgroup&group_id=0',
+               'object' => 'depot&tab=default',
                'rack' => 'rackspace&tab=default',
                'user' => 'userlist&tab=default'
        );
@@ -1843,7 +1893,7 @@ function renderTagStats ()
                                echo '&nbsp;';
                        else
                        {
-                               echo "<a href='${root}?page=" . $pagebyrealm[$realm] . "&tagfilter[]=${ref['id']}'>";
+                               echo "<a href='${root}?page=" . $pagebyrealm[$realm] . "&cft[]=${ref['id']}'>";
                                echo $taglist[$ref['id']]['refcnt'][$realm] . '</a>';
                        }
                        echo '</td>';
@@ -1871,13 +1921,16 @@ mysql> select tag_id from TagStorage left join TagTree on tag_id = id where id i
 
 */
 
+// See below why chapter_id is necessary.
 function commitUpdateDictionary ($chapter_no = 0, $dict_key = 0, $dict_value = '')
 {
-       if ($chapter_no <= 0 or $dict_key <= 0 or empty ($dict_value))
-       {
-               showError ('Invalid args', __FUNCTION__);
-               die;
-       }
+       if ($chapter_no <= 0)
+               throw new InvalidArgException ('$chapter_no', $chapter_no);
+       if ($dict_key <= 0)
+               throw new InvalidArgException ('$dict_key', $dict_key);
+       if (!strlen ($dict_value))
+               throw new InvalidArgException ('$dict_value', $dict_value);
+
        global $dbxlink;
        $query =
                "update Dictionary set dict_value = '${dict_value}' where chapter_id=${chapter_no} " .
@@ -1893,11 +1946,10 @@ function commitUpdateDictionary ($chapter_no = 0, $dict_key = 0, $dict_value = '
 
 function commitSupplementDictionary ($chapter_no = 0, $dict_value = '')
 {
-       if ($chapter_no <= 0 or empty ($dict_value))
-       {
-               showError ('Invalid args', __FUNCTION__);
-               die;
-       }
+       if ($chapter_no <= 0)
+               throw new InvalidArgException ('$chapter_no', $chapter_no);
+       if (!strlen ($dict_value))
+               throw new InvalidArgException ('$dict_value', $dict_value);
        return useInsertBlade
        (
                'Dictionary',
@@ -1905,13 +1957,11 @@ function commitSupplementDictionary ($chapter_no = 0, $dict_value = '')
        );
 }
 
+// Technically dict_key is enough to delete, but including chapter_id into
+// WHERE clause makes sure, that the action actually happends for the same
+// chapter, which authorization was granted for.
 function commitReduceDictionary ($chapter_no = 0, $dict_key = 0)
 {
-       if ($chapter_no <= 0 or $dict_key <= 0)
-       {
-               showError ('Invalid args', __FUNCTION__);
-               die;
-       }
        global $dbxlink;
        $query =
                "delete from Dictionary where chapter_id=${chapter_no} " .
@@ -1927,11 +1977,8 @@ function commitReduceDictionary ($chapter_no = 0, $dict_key = 0)
 
 function commitAddChapter ($chapter_name = '')
 {
-       if (empty ($chapter_name))
-       {
-               showError ('Invalid args', __FUNCTION__);
-               die;
-       }
+       if (!strlen ($chapter_name))
+               throw new InvalidArgException ('$chapter_name', $chapter_name);
        return useInsertBlade
        (
                'Chapter',
@@ -1941,11 +1988,8 @@ function commitAddChapter ($chapter_name = '')
 
 function commitUpdateChapter ($chapter_no = 0, $chapter_name = '')
 {
-       if ($chapter_no <= 0 or empty ($chapter_name))
-       {
-               showError ('Invalid args', __FUNCTION__);
-               die;
-       }
+       if (!strlen ($chapter_name))
+               throw new InvalidArgException ('$chapter_name', $chapter_name);
        global $dbxlink;
        $query =
                "update Chapter set name = '${chapter_name}' where id = ${chapter_no} " .
@@ -1961,11 +2005,6 @@ function commitUpdateChapter ($chapter_no = 0, $chapter_name = '')
 
 function commitDeleteChapter ($chapter_no = 0)
 {
-       if ($chapter_no <= 0)
-       {
-               showError ('Invalid args', __FUNCTION__);
-               die;
-       }
        global $dbxlink;
        $query =
                "delete from Chapter where id = ${chapter_no} and sticky = 'no' limit 1";
@@ -1982,11 +2021,8 @@ function commitDeleteChapter ($chapter_no = 0)
 // nice <select> drop-downs.
 function readChapter ($chapter_name = '')
 {
-       if (empty ($chapter_name))
-       {
-               showError ('invalid argument', __FUNCTION__);
-               return NULL;
-       }
+       if (!strlen ($chapter_name))
+               throw new InvalidArgException ('$chapter_name', $chapter_name);
        $query =
                "select dict_key, dict_value from Dictionary join Chapter on Chapter.id = Dictionary.chapter_id " .
                "where Chapter.name = '${chapter_name}'";
@@ -2000,47 +2036,49 @@ function readChapter ($chapter_name = '')
        return $chapter;
 }
 
+// Return a list of all stickers with sticker map applied. Each sticker records will
+// list all its ways on the map with refcnt set.
 function getAttrMap ()
 {
        $query =
-               "select a.id as attr_id, a.type as attr_type, a.name as attr_name, am.objtype_id, " .
-               "d.dict_value as objtype_name, am.chapter_id, c2.name as chapter_name from " .
-               "Attribute as a left join AttributeMap as am on a.id = am.attr_id " .
-               "left join Dictionary as d on am.objtype_id = d.dict_key " .
-               "left join Chapter as c1 on d.chapter_id = c1.id " .
-               "left join Chapter as c2 on am.chapter_id = c2.id " .
-               "where c1.name = 'RackObjectType' or c1.name is null " .
-               "order by a.name";
+               'SELECT id, type, name, chapter_id, (SELECT name FROM Chapter WHERE id = chapter_id) ' .
+               'AS chapter_name, objtype_id, (SELECT dict_value FROM Dictionary WHERE dict_key = objtype_id) ' .
+               'AS objtype_name, (SELECT COUNT(object_id) FROM AttributeValue AS av INNER JOIN RackObject AS ro ' .
+               'ON av.object_id = ro.id WHERE av.attr_id = Attribute.id AND ro.objtype_id = AttributeMap.objtype_id) ' .
+               'AS refcnt FROM Attribute LEFT JOIN AttributeMap ON id = attr_id ORDER BY Attribute.name, objtype_id';
        $result = useSelectBlade ($query, __FUNCTION__);
        $ret = array();
        while ($row = $result->fetch (PDO::FETCH_ASSOC))
        {
-               $attr_id = $row['attr_id'];
-               if (!isset ($ret[$attr_id]))
-               {
-                       $ret[$attr_id]['id'] = $attr_id;
-                       $ret[$attr_id]['type'] = $row['attr_type'];
-                       $ret[$attr_id]['name'] = $row['attr_name'];
-                       $ret[$attr_id]['application'] = array();
-               }
+               if (!isset ($ret[$row['id']]))
+                       $ret[$row['id']] = array
+                       (
+                               'id' => $row['id'],
+                               'type' => $row['type'],
+                               'name' => $row['name'],
+                               'application' => array(),
+                       );
                if ($row['objtype_id'] == '')
                        continue;
-               $application['objtype_id'] = $row['objtype_id'];
-               $application['objtype_name'] = $row['objtype_name'];
-               if ($row['attr_type'] == 'dict')
+               $application = array
+               (
+                       'objtype_id' => $row['objtype_id'],
+                       'objtype_name' => $row['objtype_name'],
+                       'refcnt' => $row['refcnt'],
+               );
+               if ($row['type'] == 'dict')
                {
-                       $application['chapter_no'] = $row['chapter_no'];
+                       $application['chapter_no'] = $row['chapter_id'];
                        $application['chapter_name'] = $row['chapter_name'];
                }
-               $ret[$attr_id]['application'][] = $application;
+               $ret[$row['id']]['application'][] = $application;
        }
-       $result->closeCursor();
        return $ret;
 }
 
 function commitUpdateAttribute ($attr_id = 0, $attr_name = '')
 {
-       if ($attr_id <= 0 or empty ($attr_name))
+       if ($attr_id <= 0 or !strlen ($attr_name))
        {
                showError ('Invalid args', __FUNCTION__);
                die;
@@ -2060,7 +2098,7 @@ function commitUpdateAttribute ($attr_id = 0, $attr_name = '')
 
 function commitAddAttribute ($attr_name = '', $attr_type = '')
 {
-       if (empty ($attr_name))
+       if (!strlen ($attr_name))
        {
                showError ('Invalid args', __FUNCTION__);
                die;
@@ -2210,7 +2248,7 @@ function commitUpdateAttrValue ($object_id = 0, $attr_id = 0, $value = '')
                showError ('Invalid arguments', __FUNCTION__);
                die;
        }
-       if (empty ($value))
+       if (!strlen ($value))
                return commitResetAttrValue ($object_id, $attr_id);
        global $dbxlink;
        $query1 = "select type as attr_type from Attribute where id = ${attr_id}";
@@ -2340,11 +2378,10 @@ function loadConfigCache ()
 function storeConfigVar ($varname = NULL, $varvalue = NULL)
 {
        global $dbxlink;
-       if (empty ($varname) || $varvalue === NULL)
-       {
-               showError ('Invalid arguments', __FUNCTION__);
-               return FALSE;
-       }
+       if (!strlen ($varname))
+               throw new InvalidArgException ('$varname', $varname);
+       if ($varvalue === NULL)
+               throw new InvalidArgException ('$varvalue', $varvalue);
        $query = "update Config set varvalue='${varvalue}' where varname='${varname}' limit 1";
        $result = $dbxlink->query ($query);
        if ($result == NULL)
@@ -2375,7 +2412,7 @@ function getDatabaseVersion ()
                die (__FUNCTION__ . ': SQL query #1 failed with error ' . $errorInfo[2]);
        }
        $rows = $result->fetchAll (PDO::FETCH_NUM);
-       if (count ($rows) != 1 || empty ($rows[0][0]))
+       if (count ($rows) != 1 || !strlen ($rows[0][0]))
        {
                $result->closeCursor();
                die (__FUNCTION__ . ': Cannot guess database version. Config table is present, but DB_VERSION is missing or invalid. Giving up.');
@@ -2420,97 +2457,11 @@ function getSLBSummary ()
        return $ret;
 }
 
-// Get the detailed composition of a particular virtual service, namely the list
-// of all pools, each shown with the list of objects servicing it. VS/RS configs
-// will be returned as well.
-function getVServiceInfo ($vsid = 0)
-{
-       $query1 = "select inet_ntoa(vip) as vip, vport, proto, name, vsconfig, rsconfig " .
-               "from IPv4VS where id = ${vsid}";
-       $result = useSelectBlade ($query1, __FUNCTION__);
-       $vsinfo = array ();
-       $row = $result->fetch (PDO::FETCH_ASSOC);
-       if (!$row)
-               return NULL;
-       foreach (array ('vip', 'vport', 'proto', 'name', 'vsconfig', 'rsconfig') as $cname)
-               $vsinfo[$cname] = $row[$cname];
-       $vsinfo['rspool'] = array();
-       $result->closeCursor();
-       unset ($result);
-       $query2 = "select pool.id, name, pool.vsconfig, pool.rsconfig, object_id, " .
-               "lb.vsconfig as lb_vsconfig, lb.rsconfig as lb_rsconfig from " .
-               "IPv4RSPool as pool left join IPv4LB as lb on pool.id = lb.rspool_id " .
-               "where vs_id = ${vsid} order by pool.name, object_id";
-       $result = useSelectBlade ($query2, __FUNCTION__);
-       while ($row = $result->fetch (PDO::FETCH_ASSOC))
-       {
-               if (!isset ($vsinfo['rspool'][$row['id']]))
-               {
-                       $vsinfo['rspool'][$row['id']]['name'] = $row['name'];
-                       $vsinfo['rspool'][$row['id']]['vsconfig'] = $row['vsconfig'];
-                       $vsinfo['rspool'][$row['id']]['rsconfig'] = $row['rsconfig'];
-                       $vsinfo['rspool'][$row['id']]['lblist'] = array();
-               }
-               if ($row['object_id'] == NULL)
-                       continue;
-               $vsinfo['rspool'][$row['id']]['lblist'][$row['object_id']] = array
-               (
-                       'vsconfig' => $row['lb_vsconfig'],
-                       'rsconfig' => $row['lb_rsconfig']
-               );
-       }
-       $result->closeCursor();
-       return $vsinfo;
-}
-
-// Collect and return the following info about the given real server pool:
-// basic information
-// parent virtual service information
-// load balancers list (each with a list of VSes)
-// real servers list
-
-function getRSPoolInfo ($id = 0)
-{
-       $query1 = "select id, name, vsconfig, rsconfig from " .
-               "IPv4RSPool where id = ${id}";
-       $result = useSelectBlade ($query1, __FUNCTION__);
-       $ret = array();
-       $row = $result->fetch (PDO::FETCH_ASSOC);
-       if (!$row)
-               return NULL;
-       foreach (array ('id', 'name', 'vsconfig', 'rsconfig') as $c)
-               $ret[$c] = $row[$c];
-       $result->closeCursor();
-       unset ($result);
-       $ret['lblist'] = array();
-       $ret['rslist'] = array();
-       $query2 = "select object_id, vs_id, lb.vsconfig, lb.rsconfig from " .
-               "IPv4LB as lb inner join IPv4VS as vs on lb.vs_id = vs.id " .
-               "where rspool_id = ${id} order by object_id, vip, vport";
-       $result = useSelectBlade ($query2, __FUNCTION__);
-       while ($row = $result->fetch (PDO::FETCH_ASSOC))
-               foreach (array ('vsconfig', 'rsconfig') as $c)
-                       $ret['lblist'][$row['object_id']][$row['vs_id']][$c] = $row[$c];
-       $result->closeCursor();
-       unset ($result);
-       $query3 = "select id, inservice, inet_ntoa(rsip) as rsip, rsport, rsconfig from " .
-               "IPv4RS where rspool_id = ${id} order by IPv4RS.rsip, rsport";
-       $result = useSelectBlade ($query3, __FUNCTION__);
-       while ($row = $result->fetch (PDO::FETCH_ASSOC))
-               foreach (array ('inservice', 'rsip', 'rsport', 'rsconfig') as $c)
-                       $ret['rslist'][$row['id']][$c] = $row[$c];
-       $result->closeCursor();
-       return $ret;
-}
-
 function addRStoRSPool ($pool_id = 0, $rsip = '', $rsport = 0, $inservice = 'no', $rsconfig = '')
 {
        if ($pool_id <= 0)
-       {
-               showError ('Invalid arguments', __FUNCTION__);
-               die;
-       }
-       if (empty ($rsport) or $rsport == 0)
+               throw new InvalidArgException ('$pool_id', $pool_id);
+       if (!strlen ($rsport) or $rsport === 0)
                $rsport = 'NULL';
        return useInsertBlade
        (
@@ -2521,15 +2472,19 @@ function addRStoRSPool ($pool_id = 0, $rsip = '', $rsport = 0, $inservice = 'no'
                        'rsport' => $rsport,
                        'rspool_id' => $pool_id,
                        'inservice' => ($inservice == 'yes' ? "'yes'" : "'no'"),
-                       'rsconfig' => (empty ($rsconfig) ? 'NULL' : "'${rsconfig}'")
+                       'rsconfig' => (!strlen ($rsconfig) ? 'NULL' : "'${rsconfig}'")
                )
        );
 }
 
 function commitCreateVS ($vip = '', $vport = 0, $proto = '', $name = '', $vsconfig, $rsconfig, $taglist = array())
 {
-       if (empty ($vip) or $vport <= 0 or empty ($proto))
-               return __FUNCTION__ . ': invalid arguments';
+       if (!strlen ($vip))
+               throw new InvalidArgException ('$vip', $vip);
+       if ($vport <= 0)
+               throw new InvalidArgException ('$vport', $vport);
+       if (!strlen ($proto))
+               throw new InvalidArgException ('$proto', $proto);
        if (!useInsertBlade
        (
                'IPv4VS',
@@ -2538,9 +2493,9 @@ function commitCreateVS ($vip = '', $vport = 0, $proto = '', $name = '', $vsconf
                        'vip' => "inet_aton('${vip}')",
                        'vport' => $vport,
                        'proto' => "'${proto}'",
-                       'name' => (empty ($name) ? 'NULL' : "'${name}'"),
-                       'vsconfig' => (empty ($vsconfig) ? 'NULL' : "'${vsconfig}'"),
-                       'rsconfig' => (empty ($rsconfig) ? 'NULL' : "'${rsconfig}'")
+                       'name' => (!strlen ($name) ? 'NULL' : "'${name}'"),
+                       'vsconfig' => (!strlen ($vsconfig) ? 'NULL' : "'${vsconfig}'"),
+                       'rsconfig' => (!strlen ($rsconfig) ? 'NULL' : "'${rsconfig}'")
                )
        ))
                return __FUNCTION__ . ': SQL insertion failed';
@@ -2549,11 +2504,12 @@ function commitCreateVS ($vip = '', $vport = 0, $proto = '', $name = '', $vsconf
 
 function addLBtoRSPool ($pool_id = 0, $object_id = 0, $vs_id = 0, $vsconfig = '', $rsconfig = '')
 {
-       if ($pool_id <= 0 or $object_id <= 0 or $vs_id <= 0)
-       {
-               showError ('Invalid arguments', __FUNCTION__);
-               die;
-       }
+       if ($pool_id <= 0)
+               throw new InvalidArgException ('$pool_id', $pool_id);
+       if ($object_id <= 0)
+               throw new InvalidArgException ('$object_id', $object_id);
+       if ($vs_id <= 0)
+               throw new InvalidArgException ('$vs_id', $vs_id);
        return useInsertBlade
        (
                'IPv4LB',
@@ -2562,8 +2518,8 @@ function addLBtoRSPool ($pool_id = 0, $object_id = 0, $vs_id = 0, $vsconfig = ''
                        'object_id' => $object_id,
                        'rspool_id' => $pool_id,
                        'vs_id' => $vs_id,
-                       'vsconfig' => (empty ($vsconfig) ? 'NULL' : "'${vsconfig}'"),
-                       'rsconfig' => (empty ($rsconfig) ? 'NULL' : "'${rsconfig}'")
+                       'vsconfig' => (!strlen ($vsconfig) ? 'NULL' : "'${vsconfig}'"),
+                       'rsconfig' => (!strlen ($rsconfig) ? 'NULL' : "'${rsconfig}'")
                )
        );
 }
@@ -2577,8 +2533,7 @@ function commitDeleteRS ($id = 0)
 
 function commitDeleteVS ($id = 0)
 {
-       if ($id <= 0)
-               return FALSE;
+       releaseFiles ('ipv4vs', $id);
        return useDeleteBlade ('IPv4VS', 'id', $id) && destroyTagsForEntity ('ipv4vs', $id);
 }
 
@@ -2600,22 +2555,14 @@ function commitDeleteLB ($object_id = 0, $pool_id = 0, $vs_id = 0)
 
 function commitUpdateRS ($rsid = 0, $rsip = '', $rsport = 0, $rsconfig = '')
 {
-       if ($rsid <= 0)
-       {
-               showError ('Invalid args', __FUNCTION__);
-               die;
-       }
        if (long2ip (ip2long ($rsip)) !== $rsip)
-       {
-               showError ("Invalid IP address '${rsip}'", __FUNCTION__);
-               die;
-       }
-       if (empty ($rsport) or $rsport == 0)
+               throw new InvalidArgException ('$rsip', $rsip);
+       if (!strlen ($rsport) or $rsport === 0)
                $rsport = 'NULL';
        global $dbxlink;
        $query =
                "update IPv4RS set rsip = inet_aton('${rsip}'), rsport = ${rsport}, rsconfig = " .
-               (empty ($rsconfig) ? 'NULL' : "'${rsconfig}'") .
+               (!strlen ($rsconfig) ? 'NULL' : "'${rsconfig}'") .
                " where id = ${rsid} limit 1";
        $result = $dbxlink->query ($query);
        if ($result == NULL)
@@ -2628,17 +2575,12 @@ function commitUpdateRS ($rsid = 0, $rsip = '', $rsport = 0, $rsconfig = '')
 
 function commitUpdateLB ($object_id = 0, $pool_id = 0, $vs_id = 0, $vsconfig = '', $rsconfig = '')
 {
-       if ($object_id <= 0 or $pool_id <= 0 or $vs_id <= 0)
-       {
-               showError ('Invalid args', __FUNCTION__);
-               die;
-       }
        global $dbxlink;
        $query =
                "update IPv4LB set vsconfig = " .
-               (empty ($vsconfig) ? 'NULL' : "'${vsconfig}'") .
+               (!strlen ($vsconfig) ? 'NULL' : "'${vsconfig}'") .
                ', rsconfig = ' .
-               (empty ($rsconfig) ? 'NULL' : "'${rsconfig}'") .
+               (!strlen ($rsconfig) ? 'NULL' : "'${rsconfig}'") .
                " where object_id = ${object_id} and rspool_id = ${pool_id} " .
                "and vs_id = ${vs_id} limit 1";
        $result = $dbxlink->exec ($query);
@@ -2650,19 +2592,20 @@ function commitUpdateLB ($object_id = 0, $pool_id = 0, $vs_id = 0, $vsconfig = '
 
 function commitUpdateVS ($vsid = 0, $vip = '', $vport = 0, $proto = '', $name = '', $vsconfig = '', $rsconfig = '')
 {
-       if ($vsid <= 0 or empty ($vip) or $vport <= 0 or empty ($proto))
-       {
-               showError ('Invalid args', __FUNCTION__);
-               die;
-       }
+       if (!strlen ($vip))
+               throw new InvalidArgException ('$vip', $vip);
+       if ($vport <= 0)
+               throw new InvalidArgException ('$vport', $vport);
+       if (!strlen ($proto))
+               throw new InvalidArgException ('$proto', $proto);
        global $dbxlink;
        $query = "update IPv4VS set " .
                "vip = inet_aton('${vip}'), " .
                "vport = ${vport}, " .
                "proto = '${proto}', " .
-               'name = ' . (empty ($name) ? 'NULL,' : "'${name}', ") .
-               'vsconfig = ' . (empty ($vsconfig) ? 'NULL,' : "'${vsconfig}', ") .
-               'rsconfig = ' . (empty ($rsconfig) ? 'NULL' : "'${rsconfig}'") .
+               'name = ' . (!strlen ($name) ? 'NULL,' : "'${name}', ") .
+               'vsconfig = ' . (!strlen ($vsconfig) ? 'NULL,' : "'${vsconfig}', ") .
+               'rsconfig = ' . (!strlen ($rsconfig) ? 'NULL' : "'${rsconfig}'") .
                " where id = ${vsid} limit 1";
        $result = $dbxlink->exec ($query);
        if ($result === NULL)
@@ -2671,41 +2614,6 @@ function commitUpdateVS ($vsid = 0, $vip = '', $vport = 0, $proto = '', $name =
                return TRUE;
 }
 
-// Return the list of virtual services, indexed by vs_id.
-// Each record will be shown with its basic info plus RS pools counter.
-function getVSList ($tagfilter = array(), $tfmode = 'any')
-{
-       $whereclause = getWhereClause ($tagfilter);
-       $query = "select vs.id, inet_ntoa(vip) as vip, vport, proto, vs.name, vs.vsconfig, vs.rsconfig, count(rspool_id) as poolcount " .
-               "from IPv4VS as vs left join IPv4LB as lb on vs.id = lb.vs_id " .
-               "left join TagStorage on vs.id = TagStorage.entity_id and entity_realm = 'ipv4vs' " . 
-               "where true ${whereclause} group by vs.id order by vs.vip, proto, vport";
-       $result = useSelectBlade ($query, __FUNCTION__);
-       $ret = array ();
-       while ($row = $result->fetch (PDO::FETCH_ASSOC))
-               foreach (array ('vip', 'vport', 'proto', 'name', 'vsconfig', 'rsconfig', 'poolcount') as $cname)
-                       $ret[$row['id']][$cname] = $row[$cname];
-       $result->closeCursor();
-       return $ret;
-}
-
-// Return the list of RS pool, indexed by pool id.
-function getRSPoolList ($tagfilter = array(), $tfmode = 'any')
-{
-       $whereclause = getWhereClause ($tagfilter);
-       $query = "select pool.id, pool.name, count(rspool_id) as refcnt, pool.vsconfig, pool.rsconfig " .
-               "from IPv4RSPool as pool left join IPv4LB as lb on pool.id = lb.rspool_id " .
-               "left join TagStorage on pool.id = TagStorage.entity_id and entity_realm = 'ipv4rspool' " .
-               "where true ${whereclause} group by pool.id order by pool.name, pool.id";
-       $result = useSelectBlade ($query, __FUNCTION__);
-       $ret = array ();
-       while ($row = $result->fetch (PDO::FETCH_ASSOC))
-               foreach (array ('name', 'refcnt', 'vsconfig', 'rsconfig') as $cname)
-                       $ret[$row['id']][$cname] = $row[$cname];
-       $result->closeCursor();
-       return $ret;
-}
-
 function loadThumbCache ($rack_id = 0)
 {
        $ret = NULL;
@@ -2721,11 +2629,8 @@ function loadThumbCache ($rack_id = 0)
 function saveThumbCache ($rack_id = 0, $cache = NULL)
 {
        global $dbxlink;
-       if ($rack_id == 0 or $cache == NULL)
-       {
-               showError ('Invalid arguments', __FUNCTION__);
-               return;
-       }
+       if ($cache == NULL)
+               throw new InvalidArgException ('$cache', $cache);
        $data = base64_encode ($cache);
        $query = "update Rack set thumb_data = '${data}' where id = ${rack_id} limit 1";
        $result = $dbxlink->exec ($query);
@@ -2734,11 +2639,6 @@ function saveThumbCache ($rack_id = 0, $cache = NULL)
 function resetThumbCache ($rack_id = 0)
 {
        global $dbxlink;
-       if ($rack_id == 0)
-       {
-               showError ('Invalid argument', __FUNCTION__);
-               return;
-       }
        $query = "update Rack set thumb_data = NULL where id = ${rack_id} limit 1";
        $result = $dbxlink->exec ($query);
 }
@@ -2749,11 +2649,6 @@ function resetThumbCache ($rack_id = 0)
 // current object.
 function getRSPoolsForObject ($object_id = 0)
 {
-       if ($object_id <= 0)
-       {
-               showError ('Invalid object_id', __FUNCTION__);
-               return NULL;
-       }
        $query = 'select vs_id, inet_ntoa(vip) as vip, vport, proto, vs.name, pool.id as pool_id, ' .
                'pool.name as pool_name, count(rsip) as rscount, lb.vsconfig, lb.rsconfig from ' .
                'IPv4LB as lb inner join IPv4RSPool as pool on lb.rspool_id = pool.id ' .
@@ -2772,16 +2667,16 @@ function getRSPoolsForObject ($object_id = 0)
 
 function commitCreateRSPool ($name = '', $vsconfig = '', $rsconfig = '', $taglist = array())
 {
-       if (empty ($name))
-               return __FUNCTION__ . ': invalid arguments';
+       if (!strlen ($name))
+               throw new InvalidArgException ('$name', $name);
        if (!useInsertBlade
        (
                'IPv4RSPool',
                array
                (
-                       'name' => (empty ($name) ? 'NULL' : "'${name}'"),
-                       'vsconfig' => (empty ($vsconfig) ? 'NULL' : "'${vsconfig}'"),
-                       'rsconfig' => (empty ($rsconfig) ? 'NULL' : "'${rsconfig}'")
+                       'name' => (!strlen ($name) ? 'NULL' : "'${name}'"),
+                       'vsconfig' => (!strlen ($vsconfig) ? 'NULL' : "'${vsconfig}'"),
+                       'rsconfig' => (!strlen ($rsconfig) ? 'NULL' : "'${rsconfig}'")
                )
        ))
                return __FUNCTION__ . ': SQL insertion failed';
@@ -2793,21 +2688,17 @@ function commitDeleteRSPool ($pool_id = 0)
        global $dbxlink;
        if ($pool_id <= 0)
                return FALSE;
+       releaseFiles ('ipv4rspool', $pool_id);
        return useDeleteBlade ('IPv4RSPool', 'id', $pool_id) && destroyTagsForEntity ('ipv4rspool', $pool_id);
 }
 
 function commitUpdateRSPool ($pool_id = 0, $name = '', $vsconfig = '', $rsconfig = '')
 {
-       if ($pool_id <= 0)
-       {
-               showError ('Invalid arg', __FUNCTION__);
-               die;
-       }
        global $dbxlink;
        $query = "update IPv4RSPool set " .
-               'name = ' . (empty ($name) ? 'NULL,' : "'${name}', ") .
-               'vsconfig = ' . (empty ($vsconfig) ? 'NULL,' : "'${vsconfig}', ") .
-               'rsconfig = ' . (empty ($rsconfig) ? 'NULL' : "'${rsconfig}'") .
+               'name = ' . (!strlen ($name) ? 'NULL,' : "'${name}', ") .
+               'vsconfig = ' . (!strlen ($vsconfig) ? 'NULL,' : "'${vsconfig}', ") .
+               'rsconfig = ' . (!strlen ($rsconfig) ? 'NULL' : "'${rsconfig}'") .
                " where id = ${pool_id} limit 1";
        $result = $dbxlink->exec ($query);
        if ($result === NULL)
@@ -2844,17 +2735,12 @@ function getLBList ()
        return $ret;
 }
 
-// For the given object return: it vsconfig/rsconfig; the list of RS pools
+// For the given object return: its vsconfig/rsconfig; the list of RS pools
 // attached (each with vsconfig/rsconfig in turn), each with the list of
 // virtual services terminating the pool. Each pool also lists all real
 // servers with rsconfig.
 function getSLBConfig ($object_id)
 {
-       if ($object_id <= 0)
-       {
-               showError ('Invalid arg', __FUNCTION__);
-               return NULL;
-       }
        $ret = array();
        $query = 'select vs_id, inet_ntoa(vip) as vip, vport, proto, vs.name as vs_name, ' .
                'vs.vsconfig as vs_vsconfig, vs.rsconfig as vs_rsconfig, ' .
@@ -2885,11 +2771,10 @@ function getSLBConfig ($object_id)
 
 function commitSetInService ($rs_id = 0, $inservice = '')
 {
-       if ($rs_id <= 0 or empty ($inservice))
-       {
-               showError ('Invalid args', __FUNCTION__);
-               return NULL;
-       }
+       if (!strlen ($inservice))
+               throw new InvalidArgException ('$inservice', $inservice);
+       if ($rs_id <= 0)
+               throw new InvalidArgException ('$rs_id', $rs_id);
        global $dbxlink;
        $query = "update IPv4RS set inservice = '${inservice}' where id = ${rs_id} limit 1";
        $result = $dbxlink->exec ($query);
@@ -2903,11 +2788,10 @@ function commitSetInService ($rs_id = 0, $inservice = '')
 
 function executeAutoPorts ($object_id = 0, $type_id = 0)
 {
-       if ($object_id == 0 or $type_id == 0)
-       {
-               showError ('Invalid arguments', __FUNCTION__);
-               die;
-       }
+       if ($object_id == 0)
+               throw new InvalidArgException ('$object_id', $object_id);
+       if ($type_id == 0)
+               throw new InvalidArgException ('$type_id', $type_id);
        $ret = TRUE;
        foreach (getAutoPorts ($type_id) as $autoport)
                $ret = $ret and '' == commitAddPort ($object_id, $autoport['name'], $autoport['type'], '', '');
@@ -2919,12 +2803,9 @@ function executeAutoPorts ($object_id = 0, $type_id = 0)
 // Result is a chain: randomly indexed taginfo list.
 function loadEntityTags ($entity_realm = '', $entity_id = 0)
 {
-       if ($entity_realm == '' or $entity_id <= 0)
-       {
-               showError ('Invalid or missing arguments', __FUNCTION__);
-               return NULL;
-       }
        $ret = array();
+       if (!in_array ($entity_realm, array ('file', 'ipv4net', 'ipv4vs', 'ipv4rspool', 'object', 'rack', 'user')))
+               return $ret;
        $query = "select tt.id, tag from " .
                "TagStorage as ts inner join TagTree as tt on ts.tag_id = tt.id " .
                "where entity_realm = '${entity_realm}' and entity_id = ${entity_id} " .
@@ -2936,41 +2817,6 @@ function loadEntityTags ($entity_realm = '', $entity_id = 0)
        return getExplicitTagsOnly ($ret);
 }
 
-function loadFileTags ($id)
-{
-       return loadEntityTags ('file', $id);
-}
-
-function loadRackObjectTags ($id)
-{
-       return loadEntityTags ('object', $id);
-}
-
-function loadIPv4PrefixTags ($id)
-{
-       return loadEntityTags ('ipv4net', $id);
-}
-
-function loadRackTags ($id)
-{
-       return loadEntityTags ('rack', $id);
-}
-
-function loadIPv4VSTags ($id)
-{
-       return loadEntityTags ('ipv4vs', $id);
-}
-
-function loadIPv4RSPoolTags ($id)
-{
-       return loadEntityTags ('ipv4rspool', $id);
-}
-
-function loadUserTags ($user_id)
-{
-       return loadEntityTags ('user', $user_id);
-}
-
 // Return a tag chain with all DB tags on it.
 function getTagList ()
 {
@@ -3012,8 +2858,11 @@ function commitCreateTag ($tagname = '', $parent_id = 0)
                        'parent_id' => $parent_id
                )
        );
+       global $dbxlink;
        if ($result)
                return '';
+       elseif ($dbxlink->errorCode() == 23000)
+               return "name '${tag_name}' is already used";
        else
                return "SQL query failed in " . __FUNCTION__;
 }
@@ -3036,9 +2885,12 @@ function commitUpdateTag ($tag_id, $tag_name, $parent_id)
        $query = "update TagTree set tag = '${tag_name}', parent_id = ${parent_id} " .
                "where id = ${tag_id} limit 1";
        $result = $dbxlink->exec ($query);
-       if ($result === NULL)
+       if ($result !== FALSE)
+               return '';
+       elseif ($dbxlink->errorCode() == 23000)
+               return "name '${tag_name}' is already used";
+       else
                return 'SQL query failed in ' . __FUNCTION__;
-       return '';
 }
 
 // Drop the whole chain stored.
@@ -3054,6 +2906,8 @@ function destroyTagsForEntity ($entity_realm, $entity_id)
 }
 
 // Drop only one record. This operation doesn't involve retossing other tags, unlike when adding.
+// FIXME: this function could be used by 3rd-party scripts, dismiss it at some later point,
+// but not now.
 function deleteTagForEntity ($entity_realm, $entity_id, $tag_id)
 {
        global $dbxlink;
@@ -3068,7 +2922,7 @@ function deleteTagForEntity ($entity_realm, $entity_id, $tag_id)
 // Push a record into TagStorage unconditionally.
 function addTagForEntity ($realm = '', $entity_id, $tag_id)
 {
-       if (empty ($realm))
+       if (!strlen ($realm))
                return FALSE;
        return useInsertBlade
        (
@@ -3135,7 +2989,7 @@ function createIPv4Prefix ($range = '', $name = '', $is_bcast = FALSE, $taglist
        $ip = $rangeArray[0];
        $mask = $rangeArray[1];
 
-       if (empty ($ip) or empty ($mask))
+       if (!strlen ($ip) or !strlen ($mask))
                return "Invalid IPv4 prefix '${range}'";
        $ipL = ip2long($ip);
        $maskL = ip2long($mask);
@@ -3193,6 +3047,7 @@ function destroyIPv4Prefix ($id = 0)
 {
        if ($id <= 0)
                return __FUNCTION__ . ': Invalid IPv4 prefix ID';
+       releaseFiles ('ipv4net', $id);
        if (!useDeleteBlade ('IPv4Network', 'id', $id))
                return __FUNCTION__ . ': SQL query #1 failed';
        if (!destroyTagsForEntity ('ipv4net', $id))
@@ -3210,13 +3065,10 @@ function loadScript ($name)
                return NULL;
 }
 
-function saveScript ($name, $text)
+function saveScript ($name = '', $text)
 {
-       if (empty ($name))
-       {
-               showError ('Invalid argument');
-               return FALSE;
-       }
+       if (!strlen ($name))
+               throw new InvalidArgException ('$name', $name);
        // delete regardless of existence
        useDeleteBlade ('Script', 'script_name', "'${name}'");
        return useInsertBlade
@@ -3230,70 +3082,6 @@ function saveScript ($name, $text)
        );
 }
 
-function saveUserPassword ($user_id, $newp)
-{
-       $newhash = hash (PASSWORD_HASH, $newp);
-       $query = "update UserAccount set user_password_hash = ${newhash} where user_id = ${user_id} limit 1";
-}
-
-function objectIsPortless ($id = 0)
-{
-       if ($id <= 0)
-       {
-               showError ('Invalid argument', __FUNCTION__);
-               return;
-       }
-       if (($result = useSelectBlade ("select count(id) from Port where object_id = ${id}", __FUNCTION__)) == NULL) 
-       {
-               showError ('SQL query failed', __FUNCTION__);
-               return;
-       }
-       $row = $result->fetch (PDO::FETCH_NUM);
-       $count = $row[0];
-       $result->closeCursor();
-       unset ($result);
-       return $count === '0';
-}
-
-function recordExists ($id = 0, $realm = 'object')
-{
-       if ($id <= 0)
-               return FALSE;
-       $table = array
-       (
-               'object' => 'RackObject',
-               'ipv4net' => 'IPv4Network',
-               'user' => 'UserAccount',
-       );
-       $idcol = array
-       (
-               'object' => 'id',
-               'ipv4net' => 'id',
-               'user' => 'user_id',
-       );
-       $query = 'select count(*) from ' . $table[$realm] . ' where ' . $idcol[$realm] . ' = ' . $id;
-       if (($result = useSelectBlade ($query, __FUNCTION__)) == NULL) 
-       {
-               showError ('SQL query failed', __FUNCTION__);
-               return FALSE;
-       }
-       $row = $result->fetch (PDO::FETCH_NUM);
-       $count = $row[0];
-       $result->closeCursor();
-       unset ($result);
-       return $count === '1';
-}
-
-function tagExistsInDatabase ($tname)
-{
-       $result = useSelectBlade ("select count(*) from TagTree where lower(tag) = lower('${tname}')");
-       $row = $result->fetch (PDO::FETCH_NUM);
-       $count = $row[0];
-       $result->closeCursor();
-       unset ($result);
-       return $count !== '0';
-}
-
 function newPortForwarding ($object_id, $localip, $localport, $remoteip, $remoteport, $proto, $description)
 {
        if (NULL === getIPv4AddressNetworkId ($localip))
@@ -3422,8 +3210,6 @@ function mergeSearchResults (&$objects, $terms, $fieldname)
        }
        $query .= " order by ${fieldname}";
        $result = useSelectBlade ($query, __FUNCTION__);
-// FIXME: this dead call was executed 4 times per 1 object search!
-//     $typeList = getObjectTypeList();
        $clist = array ('id', 'name', 'label', 'asset_no', 'barcode', 'objtype_id', 'objtype_name');
        while ($row = $result->fetch (PDO::FETCH_ASSOC))
        {
@@ -3444,43 +3230,14 @@ function mergeSearchResults (&$objects, $terms, $fieldname)
        return $objects;
 }
 
-function getLostIPv4Addresses ()
-{
-       dragon();
-}
-
-// File-related functions
-function getAllFiles ()
-{
-       $query = "SELECT id, name, type, size, ctime, mtime, atime, comment FROM File ORDER BY name";
-       $result = useSelectBlade ($query, __FUNCTION__);
-       $ret=array();
-       $count=0;
-       while ($row = $result->fetch (PDO::FETCH_ASSOC))
-       {
-               $ret[$count]['id'] = $row['id'];
-               $ret[$count]['name'] = $row['name'];
-               $ret[$count]['type'] = $row['type'];
-               $ret[$count]['size'] = $row['size'];
-               $ret[$count]['ctime'] = $row['ctime'];
-               $ret[$count]['mtime'] = $row['mtime'];
-               $ret[$count]['atime'] = $row['atime'];
-               $ret[$count]['comment'] = $row['comment'];
-               $count++;
-       }
-       $result->closeCursor();
-       return $ret;
-}
-
-// Return a list of files which are not linked to the specified record. This list
+// Return a list of files, which are not linked to the specified record. This list
 // will be used by printSelect().
 function getAllUnlinkedFiles ($entity_type = NULL, $entity_id = 0)
 {
-       if ($entity_type == NULL || $entity_id == 0)
-       {
-               showError ('Invalid parameters', __FUNCTION__);
-               return NULL;
-       }
+       if ($entity_type == NULL)
+               throw new InvalidArgException ('$entity_type', $entity_type);
+       if ($entity_id == 0)
+               throw new InvalidArgException ('$entity_id', $entity_id);
        global $dbxlink;
        $sql =
                'SELECT id, name FROM File ' .
@@ -3496,54 +3253,14 @@ function getAllUnlinkedFiles ($entity_type = NULL, $entity_id = 0)
        return $ret;
 }
 
-// Return a filtered, detailed file list.  Used on the main Files listing page.
-function getFileList ($entity_type = NULL, $tagfilter = array(), $tfmode = 'any')
-{
-       $whereclause = getWhereClause ($tagfilter);
-
-       if ($entity_type == 'no_links')
-               $whereclause .= 'AND File.id NOT IN (SELECT file_id FROM FileLink) ';
-       elseif ($entity_type != 'all')
-               $whereclause .= "AND entity_type = '${entity_type}' ";
-
-       $query =
-               'SELECT File.id, name, type, size, ctime, mtime, atime, comment ' .
-               'FROM File ' .
-               'LEFT JOIN FileLink ' .
-               'ON File.id = FileLink.file_id ' .
-               'LEFT JOIN TagStorage ' .
-               "ON File.id = TagStorage.entity_id AND entity_realm = 'file' " .
-               'WHERE size >= 0 ' .
-               $whereclause .
-               'ORDER BY name';
-
-       $result = useSelectBlade ($query, __FUNCTION__);
-       $ret = array();
-       while ($row = $result->fetch (PDO::FETCH_ASSOC))
-       {
-               foreach (array (
-                       'id',
-                       'name',
-                       'type',
-                       'size',
-                       'ctime',
-                       'mtime',
-                       'atime',
-                       'comment'
-                       ) as $cname)
-                       $ret[$row['id']][$cname] = $row[$cname];
-       }
-       $result->closeCursor();
-       return $ret;
-}
-
+// FIXME: return a standard cell list, so upper layer can iterate over
+// it conveniently.
 function getFilesOfEntity ($entity_type = NULL, $entity_id = 0)
 {
-       if ($entity_type == NULL || $entity_id == 0)
-       {
-               showError ('Invalid parameters', __FUNCTION__);
-               return NULL;
-       }
+       if ($entity_type === NULL)
+               throw new InvalidArgException ('$entity_type', $entity_type);
+       if ($entity_id <= 0)
+               throw new InvalidArgException ('$entity_id', $entity_id);
        global $dbxlink;
        $sql =
                'SELECT FileLink.file_id, FileLink.id AS link_id, name, type, size, ctime, mtime, atime, comment ' .
@@ -3571,11 +3288,6 @@ function getFilesOfEntity ($entity_type = NULL, $entity_id = 0)
 
 function getFile ($file_id = 0)
 {
-       if ($file_id == 0)
-       {
-               showError ('Invalid file_id', __FUNCTION__);
-               return NULL;
-       }
        global $dbxlink;
        $query = $dbxlink->prepare('SELECT * FROM File WHERE id = ?');
        $query->bindParam(1, $file_id);
@@ -3608,48 +3320,10 @@ function getFile ($file_id = 0)
        return $ret;
 }
 
-function getFileInfo ($file_id = 0)
-{
-       if ($file_id == 0)
-       {
-               showError ('Invalid file_id', __FUNCTION__);
-               return NULL;
-       }
-       global $dbxlink;
-       $query = $dbxlink->prepare('SELECT id, name, type, size, ctime, mtime, atime, comment FROM File WHERE id = ?');
-       $query->bindParam(1, $file_id);
-       $query->execute();
-       if (($row = $query->fetch (PDO::FETCH_ASSOC)) == NULL)
-       {
-               showError ('Query succeeded, but returned no data', __FUNCTION__);
-               $ret = NULL;
-       }
-       else
-       {
-               $ret = array();
-               $ret['id'] = $row['id'];
-               $ret['name'] = $row['name'];
-               $ret['type'] = $row['type'];
-               $ret['size'] = $row['size'];
-               $ret['ctime'] = $row['ctime'];
-               $ret['mtime'] = $row['mtime'];
-               $ret['atime'] = $row['atime'];
-               $ret['comment'] = $row['comment'];
-               $query->closeCursor();
-       }
-       return $ret;
-}
-
 function getFileLinks ($file_id = 0)
 {
-       if ($file_id <= 0)
-       {
-               showError ('Invalid file_id', __FUNCTION__);
-               return NULL;
-       }
-
        global $dbxlink;
-       $query = $dbxlink->prepare('SELECT * FROM FileLink WHERE file_id = ?');
+       $query = $dbxlink->prepare('SELECT * FROM FileLink WHERE file_id = ? ORDER BY entity_type, entity_id');
        $query->bindParam(1, $file_id);
        $query->execute();
        $rows = $query->fetchAll (PDO::FETCH_ASSOC);
@@ -3662,45 +3336,43 @@ function getFileLinks ($file_id = 0)
                        case 'ipv4net':
                                $page = 'ipv4net';
                                $id_name = 'id';
-                               $parent = getIPv4NetworkInfo($row['entity_id']);
+                               $parent = spotEntity ($row['entity_type'], $row['entity_id']);
                                $name = sprintf("%s (%s/%s)", $parent['name'], $parent['ip'], $parent['mask']);
                                break;
                        case 'ipv4rspool':
                                $page = 'ipv4rspool';
                                $id_name = 'pool_id';
-                               $parent = getRSPoolInfo($row['entity_id']);
+                               $parent = spotEntity ($row['entity_type'], $row['entity_id']);
                                $name = $parent['name'];
                                break;
                        case 'ipv4vs':
                                $page = 'ipv4vs';
                                $id_name = 'vs_id';
-                               $parent = getVServiceInfo($row['entity_id']);
+                               $parent = spotEntity ($row['entity_type'], $row['entity_id']);
                                $name = $parent['name'];
                                break;
                        case 'object':
                                $page = 'object';
                                $id_name = 'object_id';
-                               $parent = getObjectInfo($row['entity_id']);
-                               $name = $parent['name'];
+                               $parent = spotEntity ($row['entity_type'], $row['entity_id']);
+                               $name = $parent['dname'];
                                break;
                        case 'rack':
                                $page = 'rack';
                                $id_name = 'rack_id';
-                               $parent = getRackData($row['entity_id']);
+                               $parent = spotEntity ($row['entity_type'], $row['entity_id']);
                                $name = $parent['name'];
                                break;
                        case 'user':
                                $page = 'user';
                                $id_name = 'user_id';
-                               global $accounts;
-                               foreach ($accounts as $account)
-                                       if ($account['user_id'] == $row['entity_id'])
-                                               $name = $account['user_name'];
+                               $parent = spotEntity ($row['entity_type'], $row['entity_id']);
+                               $name = $parent['user_name'];
                                break;
                }
 
                // name needs to have some value for hrefs to work
-        if (empty($name))
+        if (!strlen ($name))
                        $name = sprintf("[Unnamed %s]", formatEntityName($row['entity_type']));
 
                $ret[$row['id']] = array(
@@ -3714,46 +3386,30 @@ function getFileLinks ($file_id = 0)
        return $ret;
 }
 
-// Return list of possible file parents along with the number of children.
-// Used on main Files listing page.
-function getFileLinkInfo ()
+function getFileStats ()
 {
-       global $dbxlink;
        $query = 'SELECT entity_type, COUNT(*) AS count FROM FileLink GROUP BY entity_type';
-
        $result = useSelectBlade ($query, __FUNCTION__);
        $ret = array();
-       $ret[0] = array ('entity_type' => 'all', 'name' => 'ALL files');
-       $clist = array ('entity_type', 'name', 'count');
-       $total = 0;
-       $i=2;
        while ($row = $result->fetch (PDO::FETCH_ASSOC))
                if ($row['count'] > 0)
-               {
-                       $total += $row['count'];
-                       $row['name'] = formatEntityName ($row['entity_type']);
-                       foreach ($clist as $cname)
-                               $ret[$i][$cname] = $row[$cname];
-                               $i++;
-               }
-       $result->closeCursor();
+                       $ret["Links in realm '${row['entity_type']}'"] = $row['count'];
+       unset ($result);
 
        // Find number of files without any linkage
        $linkless_sql =
                'SELECT COUNT(*) ' .
                'FROM File ' .
                'WHERE id NOT IN (SELECT file_id FROM FileLink)';
-       $q_linkless = useSelectBlade ($linkless_sql, __FUNCTION__);
-       $ret[1] = array ('entity_type' => 'no_links', 'name' => 'Files w/no links', 'count' => $q_linkless->fetchColumn ());
-       $q_linkless->closeCursor();
+       $result = useSelectBlade ($linkless_sql, __FUNCTION__);
+       $ret["Unattached files"] = $result->fetchColumn ();
+       unset ($result);
 
        // Find total number of files
        $total_sql = 'SELECT COUNT(*) FROM File';
-       $q_total = useSelectBlade ($total_sql, __FUNCTION__);
-       $ret[0]['count'] = $q_total->fetchColumn ();
-       $q_total->closeCursor();
+       $result = useSelectBlade ($total_sql, __FUNCTION__);
+       $ret["Total files"] = $result->fetchColumn ();
 
-       ksort($ret);
        return $ret;
 }
 
@@ -3777,6 +3433,8 @@ function commitAddFile ($name, $type, $size, $contents, $comment)
 
        if ($result)
                return '';
+       elseif ($query->errorCode() == 23000)
+               return "commitAddFile: File named '${name}' already exists";
        else
                return 'commitAddFile: SQL query failed';
 }
@@ -3797,21 +3455,12 @@ function commitLinkFile ($file_id, $entity_type, $entity_id)
                return 'commitLinkFile: SQL query failed';
 }
 
-function commitReplaceFile ($file_id = 0, $size, $contents)
+function commitReplaceFile ($file_id = 0, $contents)
 {
-       if ($file_id == 0)
-       {
-               showError ('Not all required args are present.', __FUNCTION__);
-               return FALSE;
-       }
-       $now = date('YmdHis');
-
        global $dbxlink;
-       $query = $dbxlink->prepare('UPDATE File SET size= ?, mtime = ?, contents = ? WHERE id = ?');
-       $query->bindParam(1, $size);
-       $query->bindParam(2, $now);
-       $query->bindParam(3, $contents, PDO::PARAM_LOB);
-       $query->bindParam(4, $file_id);
+       $query = $dbxlink->prepare('UPDATE File SET mtime = NOW(), contents = ?, size = LENGTH(contents) WHERE id = ?');
+       $query->bindParam(1, $contents, PDO::PARAM_LOB);
+       $query->bindParam(2, $file_id);
 
        $result = $query->execute();
        if (!$result)
@@ -3824,11 +3473,10 @@ function commitReplaceFile ($file_id = 0, $size, $contents)
 
 function commitUpdateFile ($file_id = 0, $new_name = '', $new_type = '', $new_comment = '')
 {
-       if ($file_id <= 0 or empty ($new_name) or empty ($new_type))
-       {
-               showError ('Not all required args are present.', __FUNCTION__);
-               return FALSE;
-       }
+       if (!strlen ($new_name))
+               throw new InvalidArgException ('$new_name', $new_name);
+       if (!strlen ($new_type))
+               throw new InvalidArgException ('$new_type', $new_type);
        global $dbxlink;
        $query = $dbxlink->prepare('UPDATE File SET name = ?, type = ?, comment = ? WHERE id = ?');
        $query->bindParam(1, $new_name);
@@ -3842,17 +3490,6 @@ function commitUpdateFile ($file_id = 0, $new_name = '', $new_type = '', $new_co
        return '';
 }
 
-// This is a temporary copy of commitReplaceFile() to work around escaping issues.
-function commitUpdateFileText ($file_id = 0, $newtext = '')
-{
-       if ($file_id <= 0)
-               return 'Invalid key in ' . __FUNCTION__;
-
-       global $dbxlink;
-       $query = "UPDATE File SET mtime = NOW(), contents = '${newtext}', size = LENGTH(contents) WHERE id = ${file_id} limit 1";
-       return (FALSE === $dbxlink->exec ($query)) ? ('SQL query failed in ' . __FUNCTION__) : '';
-}
-
 function commitUnlinkFile ($link_id)
 {
        if (useDeleteBlade ('FileLink', 'id', $link_id) != TRUE)
@@ -3862,6 +3499,7 @@ function commitUnlinkFile ($link_id)
 
 function commitDeleteFile ($file_id)
 {
+       destroyTagsForEntity ('file', $file_id);
        if (useDeleteBlade ('File', 'id', $file_id) != TRUE)
                return __FUNCTION__ . ': useDeleteBlade() failed';
        return '';
@@ -3870,10 +3508,93 @@ function commitDeleteFile ($file_id)
 function getChapterList ()
 {
        $ret = array();
-       $result = useSelectBlade ('select id as chapter_no, name as chapter_name from Chapter order by Chapter.name');
+       $result = useSelectBlade ('SELECT id, sticky, name, count(chapter_id) as wordc FROM Chapter LEFT JOIN Dictionary ON Chapter.id = chapter_id GROUP BY id ORDER BY name');
        while ($row = $result->fetch (PDO::FETCH_ASSOC))
-               $ret[$row['chapter_no']] = $row['chapter_name'];
+               $ret[$row['id']] = $row;
        return $ret;
 }
 
+// Return file id by file name.
+function findFileByName ($filename)
+{
+       global $dbxlink;
+       $query = $dbxlink->prepare('SELECT id FROM File WHERE name = ?');
+       $query->bindParam(1, $filename);
+       $query->execute();
+       if (($row = $query->fetch (PDO::FETCH_ASSOC)))
+               return $row['id'];
+
+       return NULL;
+}
+
+function acquireLDAPCache ($form_username, $password_hash, $expiry = 0)
+{
+       global $dbxlink;
+       $dbxlink->beginTransaction();
+       $query = "select now() - first_success as success_age, now() - last_retry as retry_age, displayed_name, memberof " .
+               "from LDAPCache where presented_username = '${form_username}' and successful_hash = '${password_hash}' " .
+               "having success_age < ${expiry} for update";
+       $result = useSelectBlade ($query);
+       if ($row = $result->fetch (PDO::FETCH_ASSOC))
+       {
+               $row['memberof'] = unserialize (base64_decode ($row['memberof']));
+               return $row;
+       }
+       return NULL;
+}
+
+function releaseLDAPCache ()
+{
+       global $dbxlink;
+       $dbxlink->commit();
+}
+
+// This actually changes only last_retry.
+function touchLDAPCacheRecord ($form_username)
+{
+       global $dbxlink;
+       $query = "update LDAPCache set last_retry = NOW() where presented_username = '${form_username}'";
+       $dbxlink->exec ($query);
+}
+
+function replaceLDAPCacheRecord ($form_username, $password_hash, $dname, $memberof)
+{
+       deleteLDAPCacheRecord ($form_username);
+       useInsertBlade ('LDAPCache',
+               array
+               (
+                       'presented_username' => "'${form_username}'",
+                       'successful_hash' => "'${password_hash}'",
+                       'displayed_name' => "'${dname}'",
+                       'memberof' => "'" . base64_encode (serialize ($memberof)) . "'"
+               )
+       );
+}
+
+function deleteLDAPCacheRecord ($form_username)
+{
+       return useDeleteBlade ('LDAPCache', 'presented_username', "'${form_username}'");
+}
+
+// Age all records older, than cache_expiry seconds, and all records made in future.
+// Calling this function w/o argument purges the whole LDAP cache.
+function discardLDAPCache ($maxage = 0)
+{
+       global $dbxlink;
+       $dbxlink->exec ("DELETE from LDAPCache WHERE NOW() - first_success >= ${maxage} or NOW() < first_success");
+}
+
+function getUserIDByUsername ($username)
+{
+       $query = "select user_id from UserAccount where user_name = '${username}'";
+       if (($result = useSelectBlade ($query, __FUNCTION__)) == NULL) 
+       {
+               showError ('SQL query failed', __FUNCTION__);
+               die;
+       }
+       if ($row = $result->fetch (PDO::FETCH_ASSOC))
+               return $row['user_id'];
+       return NULL;
+}
+
 ?>