r1106 importing RackTables trunk
authorDenis Ovsienko <infrastation@yandex.ru>
Mon, 9 Jul 2007 20:26:36 +0000 (20:26 +0000)
committerDenis Ovsienko <infrastation@yandex.ru>
Mon, 9 Jul 2007 20:26:36 +0000 (20:26 +0000)
43 files changed:
COPYING [new file with mode: 0644]
ChangeLog [new file with mode: 0644]
INSTALL [new file with mode: 0644]
LICENSE [new file with mode: 0644]
TODO [new file with mode: 0644]
find_object_ip_helper.php [new file with mode: 0644]
inc/auth.php [new file with mode: 0644]
inc/config.php [new file with mode: 0644]
inc/database.php [new file with mode: 0644]
inc/functions.php [new file with mode: 0644]
inc/init.php [new file with mode: 0644]
inc/interface.php [new file with mode: 0644]
inc/navigation.php [new file with mode: 0644]
inc/ophandlers.php [new file with mode: 0644]
inc/pagehandlers.php [new file with mode: 0644]
inc/pagetitles.php [new file with mode: 0644]
inc/secret-sample.php [new file with mode: 0644]
index.php [new file with mode: 0644]
install/dbadjust-0.14.4-0.14.5.sql [new file with mode: 0644]
install/init-auth.sql [new file with mode: 0644]
install/init-dictbase.sql [new file with mode: 0644]
install/init-dictvendors.sql [new file with mode: 0644]
install/init-sample-racks.sql [new file with mode: 0644]
install/init-structure.sql [new file with mode: 0644]
link_helper.php [new file with mode: 0644]
pi.css [new file with mode: 0644]
pix/addressspace.png [new file with mode: 0644]
pix/configuration.png [new file with mode: 0644]
pix/defaultlogo.png [new file with mode: 0644]
pix/delete_s.gif [new file with mode: 0644]
pix/error.png [new file with mode: 0644]
pix/find.png [new file with mode: 0644]
pix/go.png [new file with mode: 0644]
pix/greenplus.png [new file with mode: 0644]
pix/link.png [new file with mode: 0644]
pix/racks.png [new file with mode: 0644]
pix/racktables.ico [new file with mode: 0644]
pix/report.png [new file with mode: 0644]
pix/server.png [new file with mode: 0644]
pix/stop.png [new file with mode: 0644]
pix/unlink.png [new file with mode: 0644]
process.php [new file with mode: 0644]
render_rack_thumb.php [new file with mode: 0644]

diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..928d466
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,18 @@
+RackTables - datacenter and server room management framework
+
+Copyright (c) 2006-2007 Denis Ovsienko & Denis Yeldandi
+<info/=@=/racktables.org>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
diff --git a/ChangeLog b/ChangeLog
new file mode 100644 (file)
index 0000000..d498ae5
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,15 @@
+0.14.6
+       bugfix: don't hide IPv4 address name for a free address
+0.14.5 2007-03-08
+       bugfix: lots of adjustments to allow database be MySQL 4.0
+0.14.4 2007-02-21
+       bugfix: provide proper SQL init files
+       bugfix: produce less PHP warnings
+       bugfix: corrected error messages
+       bugfix: don't fail on an empty database
+       bugfix: multi-object form works again
+       bugfix: fixed CSS errors
+       bugfix: don't list the same port more than once in pop-up list
+       bugfix: don't allow to ban admin access
+0.14.3 2007-02-15
+       initial release
diff --git a/INSTALL b/INSTALL
new file mode 100644 (file)
index 0000000..e4217fa
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,26 @@
+This file describes installation process for RackTables 0.14.5
+
+I. Dependencies.
+
+For database you must have a MySQL server installation version 4.0 or 5.0:
+Fedora6: # yum install mysql-server mysql
+ALTLinux Compact 3.0: apt-get install MySQL-server MySQL-client
+
+For web-frontend you must have Apache and PHP 5 with PDO extension:
+Fedora 6: # yum install httpd php php-mysql php-pdo
+
+II. Layout
+1. Unpack the tarball and make it web-accessible, e.g. http://yourcompany.com/racktables
+or http://racktables.mysite.org.
+2. Go into 'inc' directory, copy secret-sample.php to secret.php and modify accordingly.
+3. Edit install/init-auth.sql and change admin password
+4. Change $enterprise in inc/config.php
+
+III. Database setup
+mysql> use database_name;
+mysql> source install/init-structure.sql
+mysql> source install/init-auth.sql
+mysql> source install/init-dictbase.sql
+mysql> source install/init-dictvendors.sql
+
+IV. Remove install/init-auth.sql
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..d511905
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,339 @@
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                   GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                           NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/TODO b/TODO
new file mode 100644 (file)
index 0000000..c009063
--- /dev/null
+++ b/TODO
@@ -0,0 +1,21 @@
+- use printLog in assertion functions
+- use a separate flag for rail problems in RackObject
+- object deletion
+- rack picture attachments
+- bulk address reservation
+- helper to find unused addresses when binding them from object page
+- reports page:
+  - connected objects w/o rackspace
+       - objects of certain types w/o asset tag
+       - connected objects in distinct rack rows
+       - last history entries
+       - warranty expiration
+- bug: can't view/click on remote side in port list, if remote side is nameless
+- bug: cannot update port 'e4/4', if port 'e 4/4' exists
+- use user ID instead of username where possible
+- eliminate default_port_type
+- keep all configuration variables in a single array
+- check words usage before deletion from a chapter
+- bug: search function is called 4 times per 1 search (see functions.php)
+- bug: non-existent dictionary words result in empty selects. workaround: use left join
+- fixme: don't permit rack resize to cross non-free units
diff --git a/find_object_ip_helper.php b/find_object_ip_helper.php
new file mode 100644 (file)
index 0000000..6f2df53
--- /dev/null
@@ -0,0 +1,33 @@
+<?
+       require 'inc/init.php';
+       // This is our context.
+       $pageno = 'objects';
+       $tabno = 'default';
+       authorize();
+?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" style="height: 100%;">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<?
+echo '<title>' . getTitle ($pageno, $tabno) . "</title>\n";
+echo "<link rel=stylesheet type='text/css' href=pi.css />\n";
+echo "<link rel=icon href='" . getFaviconURL() . "' type='image/x-icon' />";
+?>
+</head>
+<body style="height: 100%;">
+<form action="javascript:;">
+<div style="background-color: #f0f0f0; border: 1px solid #3c78b5; padding: 10px; height: 100%; text-align: center; margin: 5px;">
+<h2>Choose a port:</h2><br><br>
+<input type=hidden id='ip'>
+<select size="30" id="addresses">
+<?
+       authorize();
+       renderObjectAddressesAndNames ();
+?>
+</select><br><br>
+<input type='submit' value='Proceed' onclick='if (getElementById("ip")!="") { opener.document.getElementById("remoteip").value=getElementById("ip").value; window.close();}'>
+</div>
+</form>
+</body>
+</html>
diff --git a/inc/auth.php b/inc/auth.php
new file mode 100644 (file)
index 0000000..e7b141f
--- /dev/null
@@ -0,0 +1,91 @@
+<?
+/*
+
+Authentication library for RackTables.
+
+*/
+
+// This function ensures that we don't continue without a legitimate
+// username and password.
+function authenticate ()
+{
+       if (array_search (PASSWORD_HASH, hash_algos()) === FALSE)
+       {
+               showError ('Password hash not supported, authentication impossible.');
+               die();
+       }
+       global $enterprise;
+       if
+       (
+               !isset ($_SERVER['PHP_AUTH_USER']) or
+               !isset ($_SERVER['PHP_AUTH_PW']) or
+               !authenticated ($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])
+       )
+       {
+               header ("WWW-Authenticate: Basic realm=\"${enterprise} RackTables access\"");
+               header ('HTTP/1.0 401 Unauthorized');
+               showError ('This system requires authentication. You should use a username and a password.');
+               die();
+       }
+}
+
+// Show error unless the user is allowed access here.
+function authorize ()
+{
+       global $remote_username, $pageno, $tabno;
+       if (!authorized ($remote_username, $pageno, $tabno))
+       {
+               showError ("User '${remote_username}' is not allowed to access here.");
+               die();
+       }
+}
+
+// This function returns TRUE, if username and password are valid.
+function authenticated ($username, $password)
+{
+       global $accounts;
+       if (!isset ($accounts[$username]['user_password_hash']))
+               return FALSE;
+       if ($accounts[$username]['user_enabled'] != 'yes')
+               return FALSE;
+       if ($accounts[$username]['user_password_hash'] == hash (PASSWORD_HASH, $password))
+               return TRUE;
+       return FALSE;
+}
+
+// This function returns TRUE, if specified user has access to the
+// page and tab.
+function authorized ($username, $pageno, $tabno)
+{
+       global $perms;
+       // Deny access by default, then accumulate all corrections from database.
+       // Order of nested cycles is important here!
+       // '%' as page or tab name has a special value and means "any".
+       // 0 as user_id means "any user".
+       $answer = 'no';
+       foreach (array ('%', $username) as $u)
+               foreach (array ('%', $tabno) as $t)
+                       foreach (array ('%', $pageno) as $p)
+                               if (isset ($perms[$u][$p][$t]))
+                                       $answer = $perms[$u][$p][$t];
+       if ($answer == 'yes')
+               return TRUE;
+       return FALSE;
+}
+
+// This function returns password hash for given user ID.
+function getHashByID ($user_id = 0)
+{
+       if ($user_id <= 0)
+       {
+               showError ('Invalid user_id in getHashByID()');
+               return NULL;
+       }
+       global $accounts;
+       foreach ($accounts as $account)
+               if ($account['user_id'] == $user_id)
+                       return $account['user_password_hash'];
+       return NULL;
+}
+
+?>
diff --git a/inc/config.php b/inc/config.php
new file mode 100644 (file)
index 0000000..115ca1f
--- /dev/null
@@ -0,0 +1,114 @@
+<?
+/*
+*
+*  This is RackTables public configuration file.
+*
+*/
+
+$enterprise = 'MyCompanyName';
+// This is the name of hash used to store account password hashes in the database.
+define ('PASSWORD_HASH', 'sha1');
+define ('VERSION', '0.14.6');
+
+$rtwidth[0] = 9;
+$rtwidth[1] = 21;
+$rtwidth[2] = 9;
+
+// Free atoms. They are available for allocation to objects.
+// They are not stored in the database.
+// HSV: 180-25-75
+$color['F'] = '8fbfbf';
+
+// Absent atoms.
+// HSV: 0-0-75
+$color['A'] = 'bfbfbf';
+
+// Unusable atoms. Some problems keep them to be 'F'.
+// HSV: 0-25-75
+$color['U'] = 'bf8f8f';
+
+// Taken atoms. object_id should be present then.
+// HSV: 180-50-50
+$color['T'] = '408080';
+
+// Taken atoms with highlight. They are not stored in the database and
+// are only used for highlighting.
+// HSV: 180-50-100
+$color['Th'] = '80ffff';
+
+// Taken atoms with object problem. This is detected at runtime.
+// HSV: 0-50-50
+$color['Tw'] = '804040';
+
+// An object can be both current and problematic. We run highlightObject() first
+// and markupObjectProblems() second.
+// HSV: 0-50-100
+$color['Thw'] = 'ff8080';
+
+$nextorder['odd'] = 'even';
+$nextorder['even'] = 'odd';
+
+// Taken from the database, RJ-45/100Base-TX
+$default_port_type = 6;
+
+// Number of lines in object mass-adding form.
+define ('MASSCOUNT', 15);
+define ('MAXSELSIZE', 30);
+
+// FIXME: This is taken right from the database.
+define ('TYPE_SERVER', 4);
+define ('TYPE_SWITCH', 8);
+define ('TYPE_ROUTER', 7);
+
+// Row-scope picture scale factor.
+define ('ROW_SCALE', 2);
+
+// New-style image declarations.
+$image['error']['path'] = 'pix/error.png';
+$image['error']['width'] = 76;
+$image['error']['height'] = 17;
+$image['favicon']['path'] = 'pix/racktables.ico';
+$image['favicon']['width'] = 16;
+$image['favicon']['height'] = 16;
+$image['logo']['path'] = 'pix/defaultlogo.png';
+$image['logo']['width'] = 210;
+$image['logo']['height'] = 40;
+$image['rackspace']['path'] = 'pix/racks.png';
+$image['rackspace']['width'] = 218;
+$image['rackspace']['height'] = 200;
+$image['objects']['path'] = 'pix/server.png';
+$image['objects']['width'] = 218;
+$image['objects']['height'] = 200;
+$image['ipv4space']['path'] = 'pix/addressspace.png';
+$image['ipv4space']['width'] = 218;
+$image['ipv4space']['height'] = 200;
+$image['config']['path'] = 'pix/configuration.png';
+$image['config']['width'] = 218;
+$image['config']['height'] = 200;
+$image['reports']['path'] = 'pix/report.png';
+$image['reports']['width'] = 218;
+$image['reports']['height'] = 200;
+$image['reserve']['path'] = 'pix/stop.png';
+$image['reserve']['width'] = 16;
+$image['reserve']['height'] = 16;
+$image['useup']['path'] = 'pix/go.png';
+$image['useup']['width'] = 16;
+$image['useup']['height'] = 16;
+$image['blockuser'] = $image['reserve'];
+$image['unblockuser'] = $image['useup'];
+$image['link']['path'] = 'pix/link.png';
+$image['link']['width'] = 24;
+$image['link']['height'] = 24;
+$image['unlink']['path'] = 'pix/unlink.png';
+$image['unlink']['width'] = 24;
+$image['unlink']['height'] = 24;
+$image['add']['path'] = 'pix/greenplus.png';
+$image['add']['width'] = 16;
+$image['add']['height'] = 16;
+$image['delete']['path'] = 'pix/delete_s.gif';
+$image['delete']['width'] = 16;
+$image['delete']['height'] = 16;
+$image['grant'] = $image['add'];
+$image['revoke'] = $image['delete'];
+
+?>
diff --git a/inc/database.php b/inc/database.php
new file mode 100644 (file)
index 0000000..80fcc32
--- /dev/null
@@ -0,0 +1,1802 @@
+<?
+/*
+*
+*  This file is a library of database access functions for RackTables.
+*
+*/
+
+function escapeString ($value)
+{
+       global $dbxlink;
+       return substr ($dbxlink->quote (htmlentities ($value)), 1, -1);
+}
+
+// This function returns detailed information about either all or one
+// rack row depending on its argument.
+function getRackRowInfo ($rackrow_id = 0)
+{
+       global $dbxlink;
+       $query =
+               "select dict_key, dict_value, count(Rack.id) as count, sum(Rack.height) as sum " .
+               "from Chapter natural join Dictionary left join Rack on Rack.id = dict_key " .
+               "where chapter_name = 'RackRow' " .
+               ($rackrow_id > 0 ? "and dict_key = ${rackrow_id} " : '') .
+               "group by dict_key order by dict_value";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failed in getRackRowInfo()');
+               return NULL;
+       }
+       $ret = array();
+       $clist = array ('dict_key', 'dict_value', 'count', 'sum');
+       while ($row = $result->fetch (PDO::FETCH_ASSOC))
+               foreach ($clist as $dummy => $cname)
+                       $ret[$row['dict_key']][$cname] = $row[$cname];
+       $result->closeCursor();
+       if ($rackrow_id > 0)
+               return current ($ret);
+       else
+               return $ret;
+}
+
+// This function returns id->name map for all object types. The map is used
+// to build <select> input for objects.
+function getObjectTypeList ()
+{
+       return readChapter ('RackObjectType');
+}
+
+function getObjectList ($type_id = 0)
+{
+       global $dbxlink;
+       $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 from " .
+               "((RackObject inner join Dictionary on objtype_id=dict_key natural join Chapter) " .
+               "left join RackSpace on RackObject.id = object_id) " .
+               "left join Rack on rack_id = Rack.id " .
+               "where objtype_id = '${type_id}' and RackObject.deleted = 'no' " .
+               "and chapter_name = 'RackObjectType' order by name";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failed in getObjectList()');
+               return;
+       }
+       $ret = array();
+       $clist = array ('id', 'name', 'label', 'barcode', 'objtype_name', 'objtype_id', 'asset_no', 'rack_id', 'Rack_name');
+       while ($row = $result->fetch (PDO::FETCH_ASSOC))
+       {
+               foreach ($clist as $dummy => $cname)
+                       $ret[$row['id']][$cname] = $row[$cname];
+               $ret[$row['id']]['dname'] = displayedName ($ret[$row['id']]);
+       }
+       $result->closeCursor();
+       return $ret;
+}
+
+function getRacksForRow ($row_id = 0)
+{
+       global $dbxlink;
+       $query =
+               "select Rack.id, Rack.name, height, Rack.comment, row_id, dict_value as row_name " .
+               "from Rack left join Dictionary on row_id = dict_key natural join Chapter " .
+               "where chapter_name = 'RackRow' and Rack.deleted = 'no' " .
+               (($row_id == 0) ? "" : "and row_id = ${row_id} ") .
+               "order by row_name, Rack.id";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failed in getRacksForRow()');
+               return;
+       }
+       $ret = array();
+       $clist = array ('id', 'name', 'height', 'comment', 'row_id', 'row_name');
+       while ($row = $result->fetch (PDO::FETCH_ASSOC))
+               foreach ($clist as $dummy => $cname)
+                       $ret[$row['id']][$cname] = $row[$cname];
+       $result->closeCursor();
+       usort ($ret, 'sortByName');
+       $ret = restoreRackIDs ($ret);
+       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 in getRackData()');
+               return NULL;
+       }
+       global $dbxlink;
+       $query =
+               "select Rack.id, Rack.name, row_id, height, Rack.comment, dict_value as row_name from " .
+               "Rack left join Dictionary on Rack.row_id = dict_key natural join Chapter " .
+               "where chapter_name = 'RackRow' and Rack.id='${rack_id}' and Rack.deleted = 'no' limit 1";
+       $result1 = $dbxlink->query ($query);
+       if ($result1 == NULL)
+       {
+               if ($silent == FALSE)
+                       showError ("SQL query #1 failed in getRackData()");
+               return NULL;
+       }
+       if (($row = $result1->fetch (PDO::FETCH_ASSOC)) == NULL)
+       {
+               if ($silent == FALSE)
+                       showError ('Query #1 succeded, but returned no data in getRackData()');
+               return NULL;
+       }
+
+       // load metadata
+       $rack['id'] = $row['id'];
+       $rack['name'] = $row['name'];
+       $rack['height'] = $row['height'];
+       $rack['comment'] = $row['comment'];
+       $rack['row_id'] = $row['row_id'];
+       $rack['row_name'] = $row['row_name'];
+       $result1->closeCursor();
+
+       // 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";
+       $result2 = $dbxlink->query ($query);
+       if ($result2 == NULL)
+       {
+               if ($silent == FALSE)
+                       showError ('SQL query failure #2 in getRackData()');
+               return NULL;
+       }
+       global $loclist;
+       while ($row = $result2->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'];
+       }
+       $result2->closeCursor();
+       return $rack;
+}
+
+// This is a popular helper.
+function getObjectInfo ($object_id = 0)
+{
+       if ($object_id == 0)
+       {
+               showError ('Invalid object_id in getObjectInfo()');
+               return;
+       }
+       global $dbxlink;
+       $query =
+               "select id, 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 natural join Chapter " .
+               "where id = '${object_id}' and deleted = 'no' and chapter_name = 'RackObjectType' limit 1";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failure in getObjectInfo()');
+               return NULL;
+       }
+       if (($row = $result->fetch (PDO::FETCH_ASSOC)) == NULL)
+       {
+               showError ('Query succeded, but returned no data in getObjectInfo()');
+               $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'];
+       }
+       $result->closeCursor();
+       return $ret;
+}
+
+function getPortTypes ()
+{
+       $chapter = readChapter ('PortType');
+       $ret = array();
+       foreach ($chapter as $entry)
+               $ret[$entry['dict_key']] = $entry['dict_value'];
+       return $ret;
+}
+
+function getObjectPortsAndLinks ($object_id = 0)
+{
+       if ($object_id == 0)
+       {
+               showError ('Invalid object_id in getObjectPorts()');
+               return;
+       }
+       global $dbxlink;
+       $query =
+               "select Port.id as Port_id, ".
+               "Port.name as Port_name, ".
+               "Port.label as Port_label, ".
+               "Port.l2address as Port_l2address, ".
+               "Port.type as Port_type, ".
+               "Port.reservation_comment as Port_reservation_comment, " .
+               "dict_value as PortType_name, ".
+               "RemotePort.id as RemotePort_id, ".
+               "RemotePort.name as RemotePort_name, ".
+               "RemotePort.object_id as RemotePort_object_id, ".
+               "RackObject.name as RackObject_name ".
+               "from (".
+                       "(".
+                               "(".
+                                       "Port inner join Dictionary on Port.type = dict_key natural join Chapter".
+                               ") ".
+                               "left join Link on Port.id=Link.porta or Port.id=Link.portb ".
+                       ") ".
+                       "left join Port as RemotePort on Link.portb=RemotePort.id or Link.porta=RemotePort.id ".
+               ") ".
+               "left join RackObject on RemotePort.object_id=RackObject.id ".
+               "where chapter_name = 'PortType' and Port.object_id=${object_id} ".
+               "and (Port.id != RemotePort.id or RemotePort.id is null) ".
+               "order by Port_name";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               return NULL;
+       }
+       else
+       {
+               $ret=array();
+               $count=0;
+               while ($row = $result->fetch (PDO::FETCH_ASSOC))
+               {
+                       $ret[$count]['id'] = $row['Port_id'];
+                       $ret[$count]['name'] = $row['Port_name'];
+                       $ret[$count]['l2address'] = l2addressFromDatabase ($row['Port_l2address']);
+                       $ret[$count]['label'] = $row['Port_label'];
+                       $ret[$count]['type_id'] = $row['Port_type'];
+                       $ret[$count]['type'] = $row['PortType_name'];
+                       $ret[$count]['reservation_comment'] = $row['Port_reservation_comment'];
+                       $ret[$count]['remote_id'] = $row['RemotePort_id'];
+                       $ret[$count]['remote_name'] = htmlentities ($row['RemotePort_name'], ENT_QUOTES);
+                       $ret[$count]['remote_object_id'] = $row['RemotePort_object_id'];
+                       $ret[$count]['remote_object_name'] = $row['RackObject_name'];
+                       $count++;
+               }
+       }
+       $result->closeCursor();
+       return $ret;
+}
+
+function commitAddRack ($name, $height, $row_id, $comment)
+{
+       global $dbxlink;
+       $query = "insert into Rack(row_id, name, height, comment) values('${row_id}', '${name}', '${height}', '${comment}')";
+       $result1 = $dbxlink->query ($query);
+       if ($result1 == NULL)
+       {
+               showError ('SQL query failed in commitAddRack()');
+               return FALSE;
+       }
+       // last_insert_id() is MySQL-specific
+       $query = 'select last_insert_id()';
+       $result2 = $dbxlink->query ($query);
+       if ($result2 == NULL)
+       {
+               showError ('Cannot get last ID in commitAddRack()');
+               return FALSE;
+       }
+       // we always have a row
+       $row = $result2->fetch (PDO::FETCH_NUM);
+       $last_insert_id = $row[0];
+       $result2->closeCursor();
+       return recordHistory ('Rack', "id = ${last_insert_id}");
+}
+
+function commitAddObject ($new_name, $new_label, $new_barcode, $new_type_id, $new_asset_no)
+{
+       global $dbxlink;
+       // Maintain UNIQUE INDEX for common names and asset tags.
+       $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}'";
+       $query =
+               "insert into RackObject(name, label, barcode, objtype_id, asset_no) " .
+               "values(${new_name}, '${new_label}', ${new_barcode}, '${new_type_id}', ${new_asset_no})";
+       $result1 = $dbxlink->query ($query);
+       if ($result1 == NULL)
+       {
+               $errorInfo = $dbxlink->errorInfo();
+               showError ("SQL query '${query}' failed in commitAddObject(): " . $errorInfo[2]);
+               die;
+       }
+       if ($result1->rowCount() != 1)
+       {
+               showError ('Adding new object failed in commitAddObject()');
+               return FALSE;
+       }
+       $query = 'select last_insert_id()';
+       $result2 = $dbxlink->query ($query);
+       if ($result2 == NULL)
+       {
+               $errorInfo = $dbxlink->errorInfo();
+               showError ("SQL query '${query}' failed in commitAddObject(): " . $errorInfo[2]);
+               die;
+       }
+       // we always have a row
+       $row = $result2->fetch (PDO::FETCH_NUM);
+       $last_insert_id = $row[0];
+       $result2->closeCursor();
+       return recordHistory ('RackObject', "id = ${last_insert_id}");
+}
+
+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 to commitUpdateObject() are present.');
+               return FALSE;
+       }
+       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}'";
+       $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";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ("SQL query '${query}' failed in commitUpdateObject");
+               return FALSE;
+       }
+       if ($result->rowCount() != 1)
+       {
+               showError ('Error updating object information in commitUpdateObject()');
+               return FALSE;
+       }
+       $result->closeCursor();
+       return recordHistory ('RackObject', "id = ${object_id}");
+}
+
+function commitUpdateRack ($rack_id, $new_name, $new_height, $new_row_id, $new_comment)
+{
+       if (empty ($rack_id) || empty ($new_name) || empty ($new_height))
+       {
+               showError ('Not all required args to commitUpdateRack() are present.');
+               return FALSE;
+       }
+       global $dbxlink;
+       $query = "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)
+       {
+               showError ('Error updating rack information in commitUpdateRack()');
+               return FALSE;
+       }
+       return recordHistory ('Rack', "id = ${rack_id}");
+}
+
+// This function accepts rack data returned by getRackData(), 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.
+// If 4th arg is present, object_id fields will be updated accordingly to the new state.
+// The function returns the modified rack upon success.
+function processGridForm (&$rackData, $unchecked_state, $checked_state, $object_id = 0)
+{
+       global $loclist, $dbxlink;
+       $rack_id = $rackData['id'];
+       $rack_name = $rackData['name'];
+       $rackchanged = FALSE;
+       for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
+       {
+               for ($locidx = 0; $locidx < 3; $locidx++)
+               {
+                       if ($rackData[$unit_no][$locidx]['enabled'] != TRUE)
+                               continue;
+                       // detect a change
+                       $state = $rackData[$unit_no][$locidx]['state'];
+                       if (isset ($_REQUEST["atom_${rack_id}_${unit_no}_${locidx}"]) and $_REQUEST["atom_${rack_id}_${unit_no}_${locidx}"] == 'on')
+                               $newstate = $checked_state;
+                       else
+                               $newstate = $unchecked_state;
+                       if ($state == $newstate)
+                               continue;
+                       $rackchanged = TRUE;
+                       // and validate
+                       $atom = $loclist[$locidx];
+                       // The only changes allowed are those introduced by checkbox grid.
+                       if
+                       (
+                               !($state == $checked_state && $newstate == $unchecked_state) &&
+                               !($state == $unchecked_state && $newstate == $checked_state)
+                       )
+                               return array ('code' => 500, 'message' => "${rack_name}: Rack ID ${rack_id}, unit ${unit_no}, 'atom ${atom}', cannot change state from '${state}' to '${newstate}'");
+                       // Here we avoid using ON DUPLICATE KEY UPDATE by first performing DELETE
+                       // anyway and then looking for probable need of INSERT.
+                       $query =
+                               "delete from RackSpace where rack_id = ${rack_id} and " .
+                               "unit_no = ${unit_no} and atom = '${atom}' limit 1";
+                       $r = $dbxlink->query ($query);
+                       if ($r == NULL)
+                               return array ('code' => 500, 'message' => "${rack_name}: SQL DELETE query failed in processGridForm()");
+                       if ($newstate != 'F')
+                       {
+                               $query =
+                                       "insert into RackSpace(rack_id, unit_no, atom, state) " .
+                                       "values(${rack_id}, ${unit_no}, '${atom}', '${newstate}') ";
+                               $r = $dbxlink->query ($query);
+                               if ($r == NULL)
+                                       return array ('code' => 500, 'message' => "${rack_name}: SQL INSERT query failed in processGridForm()");
+                       }
+                       if ($newstate == 'T' and $object_id != 0)
+                       {
+                               // At this point we already have a record in RackSpace.
+                               $query =
+                                       "update RackSpace set object_id=${object_id} " .
+                                       "where rack_id=${rack_id} and unit_no=${unit_no} and atom='${atom}' limit 1";
+                               $r = $dbxlink->query ($query);
+                               if ($r->rowCount() == 1)
+                                       $rackData[$unit_no][$locidx]['object_id'] = $object_id;
+                               else
+                                       return array ('code' => 500, 'message' => "${rack_name}: Rack ID ${rack_id}, unit ${unit_no}, atom '${atom}' failed to set object_id to '${object_id}'");
+                       }
+               }
+       }
+       if ($rackchanged)
+               return array ('code' => 200, 'message' => "${rack_name}: All changes were successfully saved.");
+       else
+               return array ('code' => 300, 'message' => "${rack_name}: No changes.");
+}
+
+// This function builds a list of rack-unit-atom records, which are assigned to
+// the requested object.
+function getMoleculeForObject ($object_id = 0)
+{
+       if ($object_id == 0)
+       {
+               showError ("object_id == 0 in getMoleculeForObject()");
+               return NULL;
+       }
+       global $dbxlink;
+       $query =
+               "select rack_id, unit_no, atom from RackSpace " .
+               "where state = 'T' and object_id = ${object_id} order by rack_id, unit_no, atom";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ("SQL query failed in getMoleculeForObject()");
+               return NULL;
+       }
+       $ret = $result->fetchAll (PDO::FETCH_ASSOC);
+       $result->closeCursor();
+       return $ret;
+}
+
+// This function builds a list of rack-unit-atom records for requested molecule.
+function getMolecule ($mid = 0)
+{
+       if ($mid == 0)
+       {
+               showError ("mid == 0 in getMolecule()");
+               return NULL;
+       }
+       global $dbxlink;
+       $query =
+               "select rack_id, unit_no, atom from Atom " .
+               "where molecule_id=${mid}";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ("SQL query failed in getMolecule()");
+               return NULL;
+       }
+       $ret = $result->fetchAll (PDO::FETCH_ASSOC);
+       $result->closeCursor();
+       return $ret;
+}
+
+// This function creates a new record in Molecule and number of linked
+// R-U-A records in Atom.
+function createMolecule ($molData)
+{
+       global $dbxlink;
+       $query = "insert into Molecule values()";
+       $result1 = $dbxlink->query ($query);
+       if ($result1->rowCount() != 1)
+       {
+               showError ('Error inserting into Molecule in createMolecule()');
+               return NULL;
+       }
+       $query = 'select last_insert_id()';
+       $result2 = $dbxlink->query ($query);
+       if ($result2 == NULL)
+       {
+               showError ('Cannot get last ID in createMolecule().');
+               return NULL;
+       }
+       $row = $result2->fetch (PDO::FETCH_NUM);
+       $molecule_id = $row[0];
+       $result2->closeCursor();
+       foreach ($molData as $dummy => $rua)
+       {
+               $rack_id = $rua['rack_id'];
+               $unit_no = $rua['unit_no'];
+               $atom = $rua['atom'];
+               $query =
+                       "insert into Atom(molecule_id, rack_id, unit_no, atom) " .
+                       "values (${molecule_id}, ${rack_id}, ${unit_no}, '${atom}')";
+               $result3 = $dbxlink->query ($query);
+               if ($result3 == NULL or $result3->rowCount() != 1)
+               {
+                       showError ('Error inserting into Atom in createMolecule()');
+                       return NULL;
+               }
+       }
+       return $molecule_id;
+}
+
+// History logger. This function assumes certain table naming convention and
+// column design:
+// 1. History table name equals to dictionary table name plus 'History'.
+// 2. History table must have the same row set (w/o keys) plus one row named
+// 'ctime' of type 'timestamp'.
+function recordHistory ($tableName, $whereClause)
+{
+       global $dbxlink, $remote_username;
+       $query = "insert into ${tableName}History select *, current_timestamp(), '${remote_username}' from ${tableName} where ${whereClause}";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL or $result->rowCount() != 1)
+       {
+               showError ("SQL query failed in recordHistory() for table ${tableName}");
+               return FALSE;
+       }
+       return TRUE;
+}
+
+function getRackspaceHistory ()
+{
+       global $dbxlink;
+       $query =
+               "select mo.id as mo_id, ro.id as ro_id, ro.name, mo.ctime, mo.comment, dict_value as objtype_name, user_name from " .
+               "MountOperation as mo inner join RackObject as ro on mo.object_id = ro.id " .
+               "inner join Dictionary on objtype_id = dict_key natural join Chapter " .
+               "where chapter_name = 'RackObjectType' order by ctime desc";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failed in getRackspaceHistory()');
+               return;
+       }
+       $ret = $result->fetchAll(PDO::FETCH_ASSOC);
+       $result->closeCursor();
+       return $ret;
+}
+
+// This function is used in renderRackspaceHistory()
+function getOperationMolecules ($op_id = 0)
+{
+       if ($op_id <= 0)
+       {
+               showError ("Missing argument to getOperationMolecules()");
+               return;
+       }
+       global $dbxlink;
+       $query = "select old_molecule_id, new_molecule_id from MountOperation where id = ${op_id}";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ("SQL query failed in getOperationMolecules()");
+               return;
+       }
+       // We expect one row.
+       $row = $result->fetch (PDO::FETCH_ASSOC);
+       if ($row == NULL)
+       {
+               showError ("SQL query succeded, but returned no results in getOperationMolecules().");
+               return;
+       }
+       $omid = $row['old_molecule_id'];
+       $nmid = $row['new_molecule_id'];
+       $result->closeCursor();
+       return array ($omid, $nmid);
+}
+
+function getResidentRacksData ($object_id = 0)
+{
+       if ($object_id <= 0)
+       {
+               showError ('Invalid object_id in getResidentRacksData()');
+               return;
+       }
+       $query = "select distinct rack_id from RackSpace where object_id = ${object_id} order by rack_id";
+       global $dbxlink;
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ("SQL query failed in getResidentRacksData()");
+               return;
+       }
+       $rows = $result->fetchAll (PDO::FETCH_NUM);
+       $result->closeCursor();
+       $ret = array();
+       foreach ($rows as $row)
+       {
+               $rackData = getRackData ($row[0]);
+               if ($rackData == NULL)
+               {
+                       showError ('getRackData() failed in getResidentRacksData()');
+                       return NULL;
+               }
+               $ret[$row[0]] = $rackData;
+       }
+       $result->closeCursor();
+       return $ret;
+}
+
+function getObjectGroupInfo ($group_id = 0)
+{
+       $query =
+               'select dict_key as id, dict_value as name, count(id) as count from ' .
+               'Dictionary natural join Chapter left join RackObject on dict_key = objtype_id ' .
+               'where chapter_name = "RackObjectType" ' .
+               (($group_id > 0) ? "and dict_key = ${group_id} " : '') .
+               'group by dict_key';
+       global $dbxlink;
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failed in getObjectGroupSummary');
+               return NULL;
+       }
+       $ret = array();
+       $clist = array ('id', 'name', 'count');
+       while ($row = $result->fetch (PDO::FETCH_ASSOC))
+               foreach ($clist as $dummy => $cname)
+                       $ret[$row['id']][$cname] = $row[$cname];
+       $result->closeCursor();
+       if ($group_id > 0)
+               return current ($ret);
+       else
+               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 ()
+{
+       global $dbxlink;
+       $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 natural join Chapter ' .
+               'left join RackSpace on id = object_id '.
+               'where rack_id is null and chapter_name = "RackObjectType" order by dict_value, name';
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failure in getUnmountedObjects()');
+               return NULL;
+       }
+       $ret = array();
+       $clist = array ('id', 'name', 'label', 'barcode', 'objtype_name', 'objtype_id', 'asset_no');
+       while ($row = $result->fetch (PDO::FETCH_ASSOC))
+       {
+               foreach ($clist as $dummy => $cname)
+                       $ret[$row['id']][$cname] = $row[$cname];
+               $ret[$row['id']]['dname'] = displayedName ($ret[$row['id']]);
+       }
+       $result->closeCursor();
+       return $ret;
+}
+
+function getProblematicObjects ()
+{
+       global $dbxlink;
+       $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 natural join Chapter '.
+               'where has_problems = "yes" and chapter_name = "RackObjectType" order by objtype_name, name';
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failure in getProblematicObjects()');
+               return NULL;
+       }
+       $ret = array();
+       $clist = array ('id', 'name', 'objtype_name', 'objtype_id', 'asset_no');
+       while ($row = $result->fetch (PDO::FETCH_ASSOC))
+       {
+               foreach ($clist as $dummy => $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 in commitAddPort()');
+               return;
+       }
+       $port_l2address = l2addressForDatabase ($port_l2address);
+       $result = useInsertBlade
+       (
+               'Port',
+               array
+               (
+                       'name' => "'${port_name}'",
+                       'object_id' => "'${object_id}'",
+                       'label' => "'${port_label}'",
+                       'type' => "'${port_type_id}'",
+                       'l2address' => "${port_l2address}"
+               )
+       );
+       if ($result)
+               return '';
+       else
+               return 'SQL query failed';
+}
+
+function commitUpdatePort ($port_id, $port_name, $port_label, $port_l2address, $port_reservation_comment)
+{
+       global $dbxlink;
+       $port_l2address = l2addressForDatabase ($port_l2address);
+       $query =
+               "update Port set name='$port_name', label='$port_label', " .
+               "reservation_comment = ${port_reservation_comment}, l2address=${port_l2address} " .
+               "where id='$port_id'";
+       $result = $dbxlink->exec ($query);
+       if ($result == 1)
+               return '';
+       $errorInfo = $dbxlink->errorInfo();
+       // We could update nothing.
+       if ($errorInfo[0] == '00000')
+               return '';
+       return $errorInfo[2];
+}
+
+function delObjectPort ($port_id)
+{
+       if (unlinkPort ($port_id) != '')
+               return 'unlinkPort() failed in delObjectPort()';
+       if (useDeleteBlade ('Port', 'id', $port_id) != TRUE)
+               return 'useDeleteBlade() failed in delObjectPort()';
+       return '';
+}
+
+function getObjectAddressesAndNames ()
+{
+       global $dbxlink;
+       $query =
+               "select object_id as object_id, ".
+               "RackObject.name as object_name, ".
+               "IPBonds.name as name, ".
+               "INET_NTOA(ip) as ip ".
+               "from IPBonds join RackObject on id=object_id ";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ("SQL query failure in getObjectAddressesAndNames($type_id)");
+               return NULL;
+       }
+       $ret = array();
+       $count=0;
+       while ($row = $result->fetch (PDO::FETCH_ASSOC))
+       {
+               $ret[$count]['object_id']=$row['object_id'];
+               $ret[$count]['object_name']=$row['object_name'];
+               $ret[$count]['name']=$row['name'];
+               $ret[$count]['ip']=$row['ip'];
+               $count++;
+       }
+       $result->closeCursor();
+       return $ret;
+}
+
+
+function getEmptyPortsOfType ($type_id)
+{
+       global $dbxlink;
+       $query =
+               "select distinct Port.id as Port_id, ".
+               "Port.object_id as Port_object_id, ".
+               "RackObject.name as Object_name, ".
+               "Port.name as Port_name, ".
+               "Port.type as Port_type_id, ".
+               "dict_value as Port_type_name ".
+               "from ( ".
+               "       ( ".
+               "               Port inner join Dictionary on Port.type = dict_key natural join Chapter ".
+               "       ) ".
+               "       join RackObject on Port.object_id = RackObject.id ".
+               ") ".
+               "left join Link on Port.id=Link.porta or Port.id=Link.portb ".
+               "inner join PortCompat on Port.type = PortCompat.type2 ".
+               "where chapter_name = 'PortType' and PortCompat.type1 = '$type_id' and Link.porta is NULL ".
+               "and Port.reservation_comment is null order by Object_name, Port_name";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ("SQL query failure in getEmptyPortsOfType($type_id)");
+               return NULL;
+       }
+       $ret = array();
+       $count=0;
+       while ($row = $result->fetch (PDO::FETCH_ASSOC))
+       {
+               $ret[$count]['Port_id']=$row['Port_id'];
+               $ret[$count]['Port_object_id']=$row['Port_object_id'];
+               $ret[$count]['Object_name']=$row['Object_name'];
+               $ret[$count]['Port_name']=$row['Port_name'];
+               $ret[$count]['Port_type_id']=$row['Port_type_id'];
+               $ret[$count]['Port_type_name']=$row['Port_type_name'];
+               $count++;
+       }
+       $result->closeCursor();
+       return $ret;
+}
+
+function linkPorts ($porta, $portb)
+{
+       if ($porta == $portb)
+               return "Ports can't be the same";
+       if ($porta > $portb)
+       {
+               $tmp = $porta;
+               $porta = $portb;
+               $portb = $tmp;
+       }
+       global $dbxlink;
+       $query1 = "insert into Link set porta='${porta}', portb='{$portb}'";
+       $query2 = "update Port set reservation_comment = NULL where id = ${porta} or id = ${portb} limit 2";
+       // FIXME: who cares about the return value?
+       $result = $dbxlink->exec ($query1);
+       $result = $dbxlink->exec ($query2);
+       return '';
+       
+}
+
+function unlinkPort ($port)
+{
+       global $dbxlink;
+       $query =
+               "delete from Link where porta='$port' or portb='$port'";
+       $result = $dbxlink->exec ($query);
+       return '';
+       
+}
+
+function getObjectAddresses ($object_id = 0)
+{
+       if ($object_id == 0)
+       {
+               showError ('Invalid object_id in getObjectAddresses()');
+               return;
+       }
+       global $dbxlink;
+       $query =
+               "select ".
+               "IPAddress.name as IPAddress_name, ".
+               "IPAddress.reserved as IPAddress_reserved, ".
+               "IPBonds.name as IPBonds_name, ".
+               "INET_NTOA(IPBonds.ip) as IPBonds_ip, ".
+               "IPBonds.type as IPBonds_type, ".
+               "RemoteBonds.name as RemoteBonds_name, ".
+               "RemoteBonds.type as RemoteBonds_type, ".
+               "RemoteBonds.object_id as RemoteBonds_object_id, ".
+               "RemoteObject.name as RemoteObject_name from IPBonds " .
+               "left join IPBonds as RemoteBonds on IPBonds.ip=RemoteBonds.ip " .
+                       "and IPBonds.object_id!=RemoteBonds.object_id " .
+               "left join IPAddress on IPBonds.ip=IPAddress.ip " .
+               "left join RackObject as RemoteObject on RemoteBonds.object_id=RemoteObject.id ".
+               "where ".
+               "IPBonds.object_id = ${object_id} ".
+               "order by IPBonds.ip, RemoteObject.name";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ("SQL query failed in getObjectAddresses()");
+               return NULL;
+       }
+       else
+       {
+               $ret=array();
+               $count=0;
+               $refcount=0;
+               $prev_ip = 0;
+               while ($row = $result->fetch (PDO::FETCH_ASSOC))
+               {
+                       if ($prev_ip != $row['IPBonds_ip'])
+                       {
+                               $count++;
+                               $refcount=0;
+                               $prev_ip = $row['IPBonds_ip'];
+                               $ret[$count]['address_name'] = $row['IPAddress_name'];
+                               $ret[$count]['address_reserved'] = $row['IPAddress_reserved'];
+                               $ret[$count]['ip'] = $row['IPBonds_ip'];
+                               $ret[$count]['name'] = $row['IPBonds_name'];
+                               $ret[$count]['type'] = $row['IPBonds_type'];
+                               $ret[$count]['references'] = array();
+                       }
+
+                       if ($row['RemoteBonds_type'])
+                       {
+                               $ret[$count]['references'][$refcount]['type'] = $row['RemoteBonds_type'];
+                               $ret[$count]['references'][$refcount]['name'] = $row['RemoteBonds_name'];
+                               $ret[$count]['references'][$refcount]['object_id'] = $row['RemoteBonds_object_id'];
+                               $ret[$count]['references'][$refcount]['object_name'] = $row['RemoteObject_name'];
+                               $refcount++;
+                       }
+               }
+       }
+       $result->closeCursor();
+       return $ret;
+}
+
+function getAddressspaceList ()
+{
+       global $dbxlink;
+       $query =
+               "select ".
+               "id as IPRanges_id, ".
+               "INET_NTOA(ip) as IPRanges_ip, ".
+               "mask as IPRanges_mask, ".
+               "name as IPRanges_name ".
+               "from IPRanges ".
+               "order by ip";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               return NULL;
+       }
+       else
+       {
+               $ret=array();
+               $count=0;
+               while ($row = $result->fetch (PDO::FETCH_ASSOC))
+               {
+                       $ret[$count]['id'] = $row['IPRanges_id'];
+                       $ret[$count]['ip'] = $row['IPRanges_ip'];
+                       $ret[$count]['ip_bin'] = ip2long($row['IPRanges_ip']);
+                       $ret[$count]['name'] = $row['IPRanges_name'];
+                       $ret[$count]['mask'] = $row['IPRanges_mask'];
+                       $ret[$count]['mask_bin'] = binMaskFromDec($row['IPRanges_mask']);
+                       $ret[$count]['mask_bin_inv'] = binInvMaskFromDec($row['IPRanges_mask']);
+                       $count++;
+               }
+       }
+       $result->closeCursor();
+       return $ret;
+
+}
+
+function getRangeByIp ($ip = '', $id = 0)
+{
+       global $dbxlink;
+       if ($id == 0)
+               $query =
+                       "select ".
+                       "id, INET_NTOA(ip) as ip, mask, name ".
+                       "from IPRanges ";
+       else
+               $query =
+                       "select ".
+                       "id, INET_NTOA(ip) as ip, mask, name ".
+                       "from IPRanges where id='$id'";
+               
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               return NULL;
+       }
+       else
+       {
+               $ret=array();
+               while ($row = $result->fetch (PDO::FETCH_ASSOC))
+               {
+                       $binmask=binMaskFromDec($row['mask']);
+                       if ((ip2long($ip) & $binmask) == ip2long($row['ip']))
+                       {
+                               $ret['id'] = $row['id'];
+                               $ret['ip'] = $row['ip'];
+                               $ret['ip_bin'] = ip2long($row['ip']);
+                               $ret['name'] = $row['name'];
+                               $ret['mask'] = $row['mask'];
+                               $result->closeCursor();
+                               return $ret;
+                       }
+               }
+       }
+       $result->closeCursor();
+       return NULL;
+}
+
+function updateRange ($id=0, $name='')
+{
+       global $dbxlink;
+       $query =
+               "update IPRanges set name='$name' where id='$id'";
+       $result = $dbxlink->exec ($query);
+       return '';
+       
+}
+
+// This function is actually used not only to update, but also to create records,
+// that's why ON DUPLICATE KEY UPDATE was replaced by DELETE-INSERT pair
+// (MySQL 4.0 workaround).
+function updateAddress ($ip=0, $name='', $reserved='no')
+{
+       // DELETE may safely fail.
+       $r = useDeleteBlade ('IPAddress', 'ip', "INET_ATON('${ip}')", FALSE);
+       // INSERT may appear not necessary.
+       if ($name == '' and $reserved == 'no')
+               return '';
+       if (useInsertBlade ('IPAddress', array ('name' => "'${name}'", 'reserved' => "'${reserved}'", 'ip' => "INET_ATON('${ip}')")))
+               return '';
+       else
+               return 'useInsertBlade() failed in updateAddress()';
+}
+
+function commitDeleteRange ($id = 0)
+{
+       if ($id <= 0)
+               return 'Invalid range ID in commitDeleteRange()';
+       if (useDeleteBlade ('IPRanges', 'id', $id))
+               return '';
+       else
+               return 'SQL query failed in commitDeleteRange';
+}
+
+function updateBond ($ip='', $object_id=0, $name='', $type='')
+{
+       global $dbxlink;
+
+       $query =
+               "update IPBonds set name='$name', type='$type' where ip=INET_ATON('$ip') and object_id='$object_id'";
+       $result = $dbxlink->exec ($query);
+       return '';
+}
+
+function unbindIpFromObject ($ip='', $object_id=0)
+{
+       global $dbxlink;
+
+       $query =
+               "delete from IPBonds where ip=INET_ATON('$ip') and object_id='$object_id'";
+       $result = $dbxlink->exec ($query);
+       return '';
+}
+
+// This function returns either all or one user account. Array key is user name.
+function getUserAccounts ()
+{
+       global $dbxlink;
+       $query =
+               'select user_id, user_name, user_password_hash, user_realname, user_enabled ' .
+               'from UserAccount order by user_id';
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failed in getUserAccounts()');
+               return NULL;
+       }
+       $ret = array();
+       $clist = array ('user_id', 'user_name', 'user_realname', 'user_password_hash', 'user_enabled');
+       while ($row = $result->fetch (PDO::FETCH_ASSOC))
+               foreach ($clist as $dummy => $cname)
+                       $ret[$row['user_name']][$cname] = $row[$cname];
+       $result->closeCursor();
+       return $ret;
+}
+
+// This function returns permission array for all user accounts. Array key is user name.
+function getUserPermissions ()
+{
+       global $dbxlink;
+       $query =
+               "select UserPermission.user_id, user_name, page, tab, access from " .
+               "UserPermission natural left join UserAccount where (user_name is not null) or " .
+               "(user_name is null and UserPermission.user_id = 0) order by user_id, page, tab";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failed in getUserPermissions()');
+               return NULL;
+       }
+       $ret = array();
+       while ($row = $result->fetch (PDO::FETCH_ASSOC))
+       {
+               if ($row['user_id'] == 0)
+                       $row['user_name'] = '%';
+               $ret[$row['user_name']][$row['page']][$row['tab']] = $row['access'];
+       }
+       $result->closeCursor();
+       return $ret;
+}
+
+function searchByl2address ($l2addr)
+{
+       global $dbxlink;
+       $l2addr = l2addressForDatabase ($l2addr);
+       $query = "select object_id, Port.id as port_id from RackObject as ro inner join Port on ro.id = Port.object_id " .
+               "where l2address = ${l2addr}";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failed in objectIDbyl2address()');
+               return NULL;
+       }
+       $rows = $result->fetchAll (PDO::FETCH_ASSOC);
+       $result->closeCursor();
+       if (count ($rows) == 0) // No results.
+               return NULL;
+       if (count ($rows) == 1) // Target found.
+               return $rows[0];
+       showError ('More than one results found in objectIDbyl2address(). This is probably a broken unique key.');
+       return NULL;
+}
+
+// This function returns either port ID or NULL for specified arguments.
+function getPortID ($object_id, $port_name)
+{
+       global $dbxlink;
+       $query = "select id from Port where object_id=${object_id} and name='${port_name}' limit 2";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failed in getPortID()');
+               return NULL;
+       }
+       $rows = $result->fetchAll (PDO::FETCH_NUM);
+       if (count ($rows) != 1)
+               return NULL;
+       $ret = $rows[0][0];
+       $result->closeCursor();
+       return $ret;
+}
+
+function commitCreateUserAccount ($username, $realname, $password)
+{
+       return useInsertBlade
+       (
+               'UserAccount',
+               array
+               (
+                       'user_name' => "'${username}'",
+                       'user_realname' => "'${realname}'",
+                       'user_password_hash' => "'${password}'"
+               )
+       );
+}
+
+function commitUpdateUserAccount ($id, $new_username, $new_realname, $new_password)
+{
+       global $dbxlink;
+       $query =
+               "update UserAccount set user_name = '${new_username}', user_realname = '${new_realname}', " .
+               "user_password_hash = '${new_password}' where user_id = ${id} limit 1";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failed in commitUpdateUserAccount()');
+               die;
+       }
+       return TRUE;
+}
+
+function commitEnableUserAccount ($id, $new_enabled_value)
+{
+       global $dbxlink;
+       $query =
+               "update UserAccount set user_enabled = '${new_enabled_value}' " .
+               "where user_id = ${id} limit 1";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failed in commitEnableUserAccount()');
+               die;
+       }
+       return TRUE;
+}
+
+function commitGrantPermission ($userid, $page, $tab, $value)
+{
+       return useInsertBlade
+       (
+               'UserPermission',
+               array
+               (
+                       'user_id' => $userid,
+                       'page' => "'${page}'",
+                       'tab' => "'${tab}'",
+                       'access' => "'${value}'"
+               )
+       );
+}
+
+function commitRevokePermission ($userid, $page, $tab)
+{
+       global $dbxlink;
+       $query =
+               "delete from UserPermission where user_id = '${userid}' and page = '${page}' " .
+               "and tab = '$tab' limit 1";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failed in commitRevokePermission()');
+               die;
+       }
+       return TRUE;
+}
+
+// This function returns an array of all port type pairs from PortCompat table.
+function getPortCompat ()
+{
+       global $dbxlink;
+       $query =
+               "select type1, type2, d1.dict_value as type1name, d2.dict_value as type2name from " .
+               "PortCompat as pc inner join Dictionary as d1 on pc.type1 = d1.dict_key " .
+               "inner join Dictionary as d2 on pc.type2 = d2.dict_key " .
+               "inner join Chapter as c1 on d1.chapter_no = c1.chapter_no " .
+               "inner join Chapter as c2 on d2.chapter_no = c2.chapter_no " .
+               "where c1.chapter_name = 'PortType' and c2.chapter_name = 'PortType'";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failed in getPortCompat()');
+               return NULL;
+       }
+       $ret = $result->fetchAll (PDO::FETCH_ASSOC);
+       $result->closeCursor();
+       return $ret;
+}
+
+function removePortCompat ($type1 = 0, $type2 = 0)
+{
+       global $dbxlink;
+       if ($type1 == 0 or $type2 == 0)
+       {
+               showError ('Invalid arguments to removePortCompat');
+               die;
+       }
+       $query = "delete from PortCompat where type1 = ${type1} and type2 = ${type2} limit 1";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failed in removePortCompat()');
+               die;
+       }
+       return TRUE;
+}
+
+function addPortCompat ($type1 = 0, $type2 = 0)
+{
+       if ($type1 <= 0 or $type2 <= 0)
+       {
+               showError ('Invalid arguments to addPortCompat');
+               die;
+       }
+       return useInsertBlade
+       (
+               'PortCompat',
+               array ('type1' => $type1, 'type2' => $type2)
+       );
+}
+
+// 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.
+function getDict ()
+{
+       global $dbxlink;
+       $query =
+               "select chapter_name, Chapter.chapter_no, dict_key, dict_value, sticky from " .
+               "Chapter natural left join Dictionary order by chapter_name, dict_value";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failed in getDict()');
+               return NULL;
+       }
+       $dict = array();
+       while ($row = $result->fetch (PDO::FETCH_ASSOC))
+       {
+               $chapter_no = $row['chapter_no'];
+               if (!isset ($dict[$chapter_no]))
+               {
+                       $dict[$chapter_no]['no'] = $chapter_no;
+                       $dict[$chapter_no]['name'] = $row['chapter_name'];
+                       $dict[$chapter_no]['sticky'] = $row['sticky'] == 'yes' ? TRUE : FALSE;
+                       $dict[$chapter_no]['word'] = array();
+               }
+               if ($row['dict_key'] != NULL)
+                       $dict[$chapter_no]['word'][$row['dict_key']] = $row['dict_value'];
+       }
+       $result->closeCursor();
+       return $dict;
+}
+
+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 to commitUpdateDictionary()');
+               die;
+       }
+       global $dbxlink;
+       $query =
+               "update Dictionary set dict_value = '${dict_value}' where chapter_no=${chapter_no} " .
+               "and dict_key=${dict_key} limit 1";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failed in commitUpdateDictionary()');
+               die;
+       }
+       return TRUE;
+}
+
+function commitSupplementDictionary ($chapter_no = 0, $dict_value = '')
+{
+       if ($chapter_no <= 0 or empty ($dict_value))
+       {
+               showError ('Invalid args to commitSupplementDictionary()');
+               die;
+       }
+       return useInsertBlade
+       (
+               'Dictionary',
+               array ('chapter_no' => $chapter_no, 'dict_value' => "'${dict_value}'")
+       );
+}
+
+function commitReduceDictionary ($chapter_no = 0, $dict_key = 0)
+{
+       if ($chapter_no <= 0 or $dict_key <= 0)
+       {
+               showError ('Invalid args to commitReduceDictionary()');
+               die;
+       }
+       global $dbxlink;
+       $query =
+               "delete from Dictionary where chapter_no=${chapter_no} " .
+               "and dict_key=${dict_key} limit 1";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failed in commitReduceDictionary()');
+               die;
+       }
+       return TRUE;
+}
+
+function commitAddChapter ($chapter_name = '')
+{
+       if (empty ($chapter_name))
+       {
+               showError ('Invalid args to commitAddChapter()');
+               die;
+       }
+       return useInsertBlade
+       (
+               'Chapter',
+               array ('chapter_name' => "'${chapter_name}'")
+       );
+}
+
+function commitUpdateChapter ($chapter_no = 0, $chapter_name = '')
+{
+       if ($chapter_no <= 0 or empty ($chapter_name))
+       {
+               showError ('Invalid args to commitUpdateChapter()');
+               die;
+       }
+       global $dbxlink;
+       $query =
+               "update Chapter set chapter_name = '${chapter_name}' where chapter_no = ${chapter_no} " .
+               "and sticky = 'no' limit 1";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failed in commitUpdateChapter()');
+               die;
+       }
+       return TRUE;
+}
+
+function commitDeleteChapter ($chapter_no = 0)
+{
+       if ($chapter_no <= 0)
+       {
+               showError ('Invalid args to commitDeleteChapter()');
+               die;
+       }
+       global $dbxlink;
+       $query =
+               "delete from Chapter where chapter_no = ${chapter_no} and sticky = 'no' limit 1";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failed in commitDeleteChapter()');
+               die;
+       }
+       return TRUE;
+}
+
+// This is a dictionary accessor.
+function readChapter ($chapter_name = '')
+{
+       if (empty ($chapter_name))
+       {
+               showError ('invalid argument to readChapter()');
+               return NULL;
+       }
+       global $dbxlink;
+       $query =
+               "select dict_key, dict_value from Dictionary natural join Chapter " .
+               "where chapter_name = '${chapter_name}' order by dict_value";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               $errorInfo = $dbxlink->errorInfo();
+               showError ("SQL query '${query}'\nwith message '${errorInfo[2]}'\nfailed in readChapter('${chapter_name}')");
+               return NULL;
+       }
+       $chapter = array();
+       while ($row = $result->fetch (PDO::FETCH_ASSOC))
+               $chapter[] = $row;
+       $result->closeCursor();
+       return $chapter;
+}
+
+function getAttrMap ()
+{
+       global $dbxlink;
+       $query =
+               "select a.attr_id, a.attr_type, a.attr_name, am.objtype_id, " .
+               "d.dict_value as objtype_name, am.chapter_no, c2.chapter_name from " .
+               "Attribute as a natural left join AttributeMap as am " .
+               "left join Dictionary as d on am.objtype_id = d.dict_key " .
+               "left join Chapter as c1 on d.chapter_no = c1.chapter_no " .
+               "left join Chapter as c2 on am.chapter_no = c2.chapter_no " .
+               "where c1.chapter_name = 'RackObjectType' or c1.chapter_name is null " .
+               "order by attr_name";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               $errorInfo = $dbxlink->errorInfo();
+               showError ("SQL query '${query}'\nwith message '${errorInfo[2]}'\nfailed in getAttrMap()");
+               return NULL;
+       }
+       $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 ($row['objtype_id'] == '')
+                       continue;
+               $application['objtype_id'] = $row['objtype_id'];
+               $application['objtype_name'] = $row['objtype_name'];
+               if ($row['attr_type'] == 'dict')
+               {
+                       $application['chapter_no'] = $row['chapter_no'];
+                       $application['chapter_name'] = $row['chapter_name'];
+               }
+               $ret[$attr_id]['application'][] = $application;
+       }
+       $result->closeCursor();
+       return $ret;
+}
+
+function commitUpdateAttribute ($attr_id = 0, $attr_name = '')
+{
+       if ($attr_id <= 0 or empty ($attr_name))
+       {
+               showError ('Invalid args to commitUpdateAttribute()');
+               die;
+       }
+       global $dbxlink;
+       $query =
+               "update Attribute set attr_name = '${attr_name}' " .
+               "where attr_id = ${attr_id} limit 1";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ("SQL query '${query}' failed in commitUpdateAttribute()");
+               die;
+       }
+       return TRUE;
+}
+
+function commitAddAttribute ($attr_name = '', $attr_type = '')
+{
+       if (empty ($attr_name))
+       {
+               showError ('Invalid args to commitAddAttribute()');
+               die;
+       }
+       switch ($attr_type)
+       {
+               case 'uint':
+               case 'float':
+               case 'string':
+               case 'dict':
+                       break;
+               default:
+                       showError ('Invalid args to commitAddAttribute()');
+                       die;
+       }
+       return useInsertBlade
+       (
+               'Attribute',
+               array ('attr_name' => "'${attr_name}'", 'attr_type' => "'${attr_type}'")
+       );
+}
+
+function commitDeleteAttribute ($attr_id = 0)
+{
+       if ($attr_id <= 0)
+       {
+               showError ('Invalid args to commitDeleteAttribute()');
+               die;
+       }
+       return useDeleteBlade ('Attribute', 'attr_id', $attr_id);
+}
+
+// FIXME: don't store garbage in chapter_no for non-dictionary types.
+function commitSupplementAttrMap ($attr_id = 0, $objtype_id = 0, $chapter_no = 0)
+{
+       if ($attr_id <= 0 or $objtype_id <= 0 or $chapter_no <= 0)
+       {
+               showError ('Invalid args to commitSupplementAttrMap()');
+               die;
+       }
+       return useInsertBlade
+       (
+               'AttributeMap',
+               array
+               (
+                       'attr_id' => $attr_id,
+                       'objtype_id' => $objtype_id,
+                       'chapter_no' => $chapter_no
+               )
+       );
+}
+
+function commitReduceAttrMap ($attr_id = 0, $objtype_id)
+{
+       if ($attr_id <= 0 or $objtype_id <= 0)
+       {
+               showError ('Invalid args to commitReduceAttrMap()');
+               die;
+       }
+       global $dbxlink;
+       $query =
+               "delete from AttributeMap where attr_id=${attr_id} " .
+               "and objtype_id=${objtype_id} limit 1";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failed in commitReduceAttrMap()');
+               die;
+       }
+       return TRUE;
+}
+
+// This function returns all optional attributes for requested object
+// as an array of records. NULL is returned on error and empty array
+// is returned, if there are no attributes found.
+function getAttrValues ($object_id)
+{
+       if ($object_id <= 0)
+       {
+               showError ('Invalid argument to getAttrValues()');
+               return NULL;
+       }
+       global $dbxlink;
+       $ret = array();
+       $query =
+               "select A.attr_id, A.attr_name, A.attr_type, C.chapter_name, " .
+               "AV.uint_value, AV.float_value, AV.string_value, D.dict_value from " .
+               "RackObject as RO inner join AttributeMap as AM on RO.objtype_id = AM.objtype_id " .
+               "inner join Attribute as A using (attr_id) " .
+               "left join AttributeValue as AV on AV.attr_id = AM.attr_id and AV.object_id = RO.id " .
+               "left join Dictionary as D on D.dict_key = AV.uint_value and AM.chapter_no = D.chapter_no " .
+               "left join Chapter as C on AM.chapter_no = C.chapter_no " .
+               "where RO.id = ${object_id} order by A.attr_type";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               $errorInfo = $dbxlink->errorInfo();
+               showError ("SQL query '${query}'\nwith message '${errorInfo[2]}'\nfailed in getAttrValues()");
+               return NULL;
+       }
+       while ($row = $result->fetch (PDO::FETCH_ASSOC))
+       {
+               $record = array();
+               $record['id'] = $row['attr_id'];
+               $record['name'] = $row['attr_name'];
+               $record['type'] = $row['attr_type'];
+               switch ($row['attr_type'])
+               {
+                       case 'uint':
+                       case 'float':
+                       case 'string':
+                       case 'dict':
+                               $record['value'] = $row[$row['attr_type'] . '_value'];
+                               $record['chapter_name'] = $row['chapter_name'];
+                               $record['key'] = $row['uint_value'];
+                               break;
+                       default:
+                               $record['value'] = NULL;
+                               break;
+               }
+               $ret[$row['attr_id']] = $record;
+       }
+       $result->closeCursor();
+       return $ret;
+}
+
+function commitResetAttrValue ($object_id = 0, $attr_id = 0)
+{
+       if ($object_id <= 0 or $attr_id <= 0)
+       {
+               showError ('Invalid arguments to commitResetAttrValue()');
+               die;
+       }
+       global $dbxlink;
+       $query = "delete from AttributeValue where object_id = ${object_id} and attr_id = ${attr_id} limit 1";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failed in commitResetAttrValue()');
+               die;
+       }
+       return TRUE;
+}
+
+// FIXME: don't share common code with use commitResetAttrValue()
+function commitUpdateAttrValue ($object_id = 0, $attr_id = 0, $value = '')
+{
+       if ($object_id <= 0 or $attr_id <= 0)
+       {
+               showError ('Invalid arguments to commitUpdateAttrValue()');
+               die;
+       }
+       if (empty ($value))
+               return commitResetAttrValue ($object_id, $attr_id);
+       global $dbxlink;
+       $query1 = "select attr_type from Attribute where attr_id = ${attr_id}";
+       $result = $dbxlink->query ($query1);
+       if ($result == NULL)
+       {
+               showError ('SQL query #1 failed in commitUpdateAttrValue()');
+               die;
+       }
+       $row = $result->fetch (PDO::FETCH_NUM);
+       if ($row == NULL)
+       {
+               showError ('SQL query #1 returned no results in commitUpdateAttrValue()');
+               die;
+       }
+       $attr_type = $row[0];
+       $result->closeCursor();
+       switch ($attr_type)
+       {
+               case 'uint':
+               case 'float':
+               case 'string':
+                       $column = $attr_type . '_value';
+                       break;
+               case 'dict':
+                       $column = 'uint_value';
+                       break;
+               default:
+                       showError ("Unknown attribute type '${attr_type}' met in commitUpdateAttrValue()");
+                       die;
+       }
+       $query2 =
+               "delete from AttributeValue where " .
+               "object_id = ${object_id} and attr_id = ${attr_id} limit 1";
+       $result = $dbxlink->query ($query2);
+       if ($result == NULL)
+       {
+               showError ('SQL query #2 failed in commitUpdateAttrValue()');
+               die;
+       }
+       // We know $value isn't empty here.
+       $query3 =
+               "insert into AttributeValue set ${column} = '${value}', " .
+               "object_id = ${object_id}, attr_id = ${attr_id} ";
+       $result = $dbxlink->query ($query3);
+       if ($result == NULL)
+       {
+               showError ('SQL query #3 failed in commitUpdateAttrValue()');
+               die;
+       }
+       return TRUE;
+}
+
+function commitUseupPort ($port_id = 0)
+{
+       if ($port_id <= 0)
+       {
+               showError ("Invalid argument to commitUseupPort()");
+               die;
+       }
+       global $dbxlink;
+       $query = "update Port set reservation_comment = NULL where id = ${port_id} limit 1";
+       $result = $dbxlink->exec ($query);
+       if ($result == NULL)
+       {
+               showError ("SQL query failed in commitUseupPort()");
+               die;
+       }
+       return TRUE;
+       
+}
+
+// This is a swiss-knife blade to insert a record into a table.
+// The first argument is table name.
+// The second argument is an array of "name" => "value" pairs.
+// The function returns either TRUE or FALSE (we expect one row
+// to be inserted).
+function useInsertBlade ($tablename, $values)
+{
+       global $dbxlink;
+       $namelist = $valuelist = '';
+       foreach ($values as $name => $value)
+       {
+               $namelist = $namelist . ($namelist == '' ? "(${name}" : ", ${name}");
+               $valuelist = $valuelist . ($valuelist == '' ? "(${value}" : ", ${value}");
+       }
+       $query = "insert into ${tablename} ${namelist}) values ${valuelist})";
+       $result = $dbxlink->exec ($query);
+       if ($result != 1)
+               return FALSE;
+       return TRUE;
+}
+
+// This swiss-knife blade deletes one record from the specified table
+// using the specified key name and value.
+function useDeleteBlade ($tablename, $keyname, $keyvalue, $quotekey = TRUE)
+{
+       global $dbxlink;
+       if ($quotekey == TRUE)
+               $query = "delete from ${tablename} where ${keyname}='$keyvalue' limit 1";
+       else
+               $query = "delete from ${tablename} where ${keyname}=$keyvalue limit 1";
+       $result = $dbxlink->exec ($query);
+       if ($result === NULL)
+               return FALSE;
+       elseif ($result != 1)
+               return FALSE;
+       else
+               return TRUE;
+}
+
+?>
diff --git a/inc/functions.php b/inc/functions.php
new file mode 100644 (file)
index 0000000..9a56f24
--- /dev/null
@@ -0,0 +1,1005 @@
+<?
+/*
+*
+*  This file is a library of computational functions for RackTables.
+*
+*/
+
+$loclist[0] = 'front';
+$loclist[1] = 'interior';
+$loclist[2] = 'rear';
+$loclist['front'] = 0;
+$loclist['interior'] = 1;
+$loclist['rear'] = 2;
+$template[0] = array (TRUE, TRUE, TRUE);
+$template[1] = array (TRUE, TRUE, FALSE);
+$template[2] = array (FALSE, TRUE, TRUE);
+$template[3] = array (TRUE, FALSE, FALSE);
+$template[4] = array (FALSE, TRUE, FALSE);
+$template[5] = array (FALSE, FALSE, TRUE);
+$templateWidth[0] = 3;
+$templateWidth[1] = 2;
+$templateWidth[2] = 2;
+$templateWidth[3] = 1;
+$templateWidth[4] = 1;
+$templateWidth[5] = 1;
+
+// Objects of some types should be explicitly shown as
+// anonymous (labelless). This function is a single place where the
+// decision about displayed name is made.
+function displayedName ($objectData)
+{
+       if ($objectData['name'] != '')
+               return $objectData['name'];
+       else
+               switch ($objectData['objtype_id'])
+               {
+                       case TYPE_SERVER:
+                       case TYPE_SWITCH:
+                       case TYPE_ROUTER:
+                               return "ANONYMOUS " . $objectData['objtype_name'];
+                       default:
+                               return "[${objectData['objtype_name']}]";
+               }
+}
+
+// This function finds height of solid rectangle of atoms, which are all
+// assigned to the same object. Rectangle base is defined by specified
+// template.
+function rectHeight ($rackData, $startRow, $template_idx)
+{
+       $height = 0;
+       // The first met object_id is used to match all the folowing IDs.
+       $object_id = 0;
+       global $template;
+       do
+       {
+               for ($locidx = 0; $locidx < 3; $locidx++)
+               {
+                       // At least one value in template is TRUE, but the following block
+                       // can meet 'skipped' atoms. Let's ensure we have something after processing
+                       // the first row.
+                       if ($template[$template_idx][$locidx])
+                       {
+                               if (isset ($rackData[$startRow - $height][$locidx]['skipped']))
+                                       break 2;
+                               if ($rackData[$startRow - $height][$locidx]['state'] != 'T')
+                                       break 2;
+                               if ($object_id == 0)
+                                       $object_id = $rackData[$startRow - $height][$locidx]['object_id'];
+                               if ($object_id != $rackData[$startRow - $height][$locidx]['object_id'])
+                                       break 2;
+                       }
+               }
+               // If the first row can't offer anything, bail out.
+               if ($height == 0 and $object_id == 0)
+                       break;
+               $height++;
+       }
+       while ($startRow - $height > 0);
+//     echo "for startRow==${startRow} and template==(${template[0]}, ${template[1]}, ${template[2]}) height==${height}<br>\n";
+       return $height;
+}
+
+// This function marks atoms to be avoided by rectHeight() and assigns rowspan/colspan
+// attributes.
+function markSpan (&$rackData, $startRow, $maxheight, $template_idx)
+{
+       global $template, $templateWidth;
+       $colspan = 0;
+       for ($height = 0; $height < $maxheight; $height++)
+       {
+               for ($locidx = 0; $locidx < 3; $locidx++)
+               {
+                       if ($template[$template_idx][$locidx])
+                       {
+                               // Add colspan/rowspan to the first row met and mark the following ones to skip.
+                               if ($colspan != 0)
+                                       $rackData[$startRow - $height][$locidx]['skipped'] = TRUE;
+                               else
+                               {
+                                       $colspan = $templateWidth[$template_idx];
+                                       if ($colspan > 1)
+                                               $rackData[$startRow - $height][$locidx]['colspan'] = $colspan;
+                                       if ($maxheight > 1)
+                                               $rackData[$startRow - $height][$locidx]['rowspan'] = $maxheight;
+                               }
+                       }
+               }
+       }
+       return;
+}
+
+// This function finds rowspan/solspan/skipped atom attributes for renderRack()
+// What we actually have to do is to find all possible rectangles for each objects
+// and then find the widest of those with the maximal square.
+function markAllSpans (&$rackData = NULL)
+{
+       if ($rackData == NULL)
+       {
+               showError ('Invalid rackData in markupAllSpans()');
+               return;
+       }
+       for ($i = $rackData['height']; $i > 0; $i--)
+       {
+               // calculate height of 6 possible span templates (array is presorted by width descending)
+               global $template;
+               for ($j = 0; $j < 6; $j++)
+                       $height[$j] = rectHeight ($rackData, $i, $j);
+               // find the widest rectangle of those with maximal height
+               $maxheight = max ($height);
+               if ($maxheight > 0)
+               {
+                       $best_template_index = 0;
+                       for ($j = 0; $j < 6; $j++)
+                               if ($height[$j] == $maxheight)
+                               {
+                                       $best_template_index = $j;
+                                       break;
+                               }
+                       // distribute span marks
+                       markSpan ($rackData, $i, $maxheight, $best_template_index);
+               }
+       }
+}
+
+function delRow ($row_id = 0)
+{
+       if ($row_id == 0)
+       {
+               showError ('Not all required args to delRow() are present.');
+               return;
+       }
+       if (!isset ($_REQUEST['confirmed']) || $_REQUEST['confirmed'] != 'true')
+       {
+               echo "Press <a href='?op=del_row&row_id=${row_id}&confirmed=true'>here</a> to confirm rack row deletion.";
+               return;
+       }
+       global $dbxlink;
+       echo 'Deleting rack row information: ';
+       $result = $dbxlink->query ("update RackRow set deleted = 'yes' where id=${row_id} limit 1");
+       if ($result->rowCount() != 1)
+       {
+               showError ('Marked ' . $result.rowCount() . ' rows as deleted, but expected 1');
+               return;
+       }
+       echo 'OK<br>';
+       recordHistory ('RackRow', "id = ${row_id}");
+       echo "Information was deleted. You may return to <a href='?op=list_rows&editmode=on'>rack row list</a>.";
+}
+
+function delRack ($rack_id = 0)
+{
+       if ($rack_id == 0)
+       {
+               showError ('Not all required args to delRack() are present.');
+               return;
+       }
+       if (!isset ($_REQUEST['confirmed']) || $_REQUEST['confirmed'] != 'true')
+       {
+               echo "Press <a href='?op=del_rack&rack_id=${rack_id}&confirmed=true'>here</a> to confirm rack deletion.";
+               return;
+       }
+       global $dbxlink;
+       echo 'Deleting rack information: ';
+       $result = $dbxlink->query ("update Rack set deleted = 'yes' where id=${rack_id} limit 1");
+       if ($result->rowCount() != 1)
+       {
+               showError ('Marked ' . $result.rowCount() . ' rows as deleted, but expected 1');
+               return;
+       }
+       echo 'OK<br>';
+       recordHistory ('Rack', "id = ${rack_id}");
+       echo "Information was deleted. You may return to <a href='?op=list_racks&editmode=on'>rack list</a>.";
+}
+
+function delObject ($object_id = 0)
+{
+       if ($object_id == 0)
+       {
+               showError ('Not all required args to delObject() are present.');
+               return;
+       }
+       if (!isset ($_REQUEST['confirmed']) || $_REQUEST['confirmed'] != 'true')
+       {
+               echo "Press <a href='?op=del_object&object_id=${object_id}&confirmed=true'>here</a> to confirm object deletion.";
+               return;
+       }
+       global $dbxlink;
+       echo 'Deleting object information: ';
+       $result = $dbxlink->query ("update RackObject set deleted = 'yes' where id=${object_id} limit 1");
+       if ($result->rowCount() != 1)
+       {
+               showError ('Marked ' . $result.rowCount() . ' rows as deleted, but expected 1');
+               return;
+       }
+       echo 'OK<br>';
+       recordHistory ('RackObject', "id = ${object_id}");
+       echo "Information was deleted. You may return to <a href='?op=list_objects&editmode=on'>object list</a>.";
+}
+
+// We can mount 'F' atoms and unmount our own 'T' atoms.
+function applyObjectMountMask (&$rackData, $object_id)
+{
+       for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
+               for ($locidx = 0; $locidx < 3; $locidx++)
+                       switch ($rackData[$unit_no][$locidx]['state'])
+                       {
+                               case 'F':
+                                       $rackData[$unit_no][$locidx]['enabled'] = TRUE;
+                                       break;
+                               case 'T':
+                                       $rackData[$unit_no][$locidx]['enabled'] = ($rackData[$unit_no][$locidx]['object_id'] == $object_id);
+                                       break;
+                               default:
+                                       $rackData[$unit_no][$locidx]['enabled'] = FALSE;
+                       }
+}
+
+// Design change means transition between 'F' and 'A' and back.
+function applyRackDesignMask (&$rackData)
+{
+       for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
+               for ($locidx = 0; $locidx < 3; $locidx++)
+                       switch ($rackData[$unit_no][$locidx]['state'])
+                       {
+                               case 'F':
+                               case 'A':
+                                       $rackData[$unit_no][$locidx]['enabled'] = TRUE;
+                                       break;
+                               default:
+                                       $rackData[$unit_no][$locidx]['enabled'] = FALSE;
+                       }
+}
+
+// The same for 'F' and 'U'.
+function applyRackProblemMask (&$rackData)
+{
+       for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
+               for ($locidx = 0; $locidx < 3; $locidx++)
+                       switch ($rackData[$unit_no][$locidx]['state'])
+                       {
+                               case 'F':
+                               case 'U':
+                                       $rackData[$unit_no][$locidx]['enabled'] = TRUE;
+                                       break;
+                               default:
+                                       $rackData[$unit_no][$locidx]['enabled'] = FALSE;
+                       }
+}
+
+// This mask should allow toggling 'T' and 'W' on object's rackspace.
+function applyObjectProblemMask (&$rackData)
+{
+       for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
+               for ($locidx = 0; $locidx < 3; $locidx++)
+                       switch ($rackData[$unit_no][$locidx]['state'])
+                       {
+                               case 'T':
+                               case 'W':
+                                       $rackData[$unit_no][$locidx]['enabled'] = ($rackData[$unit_no][$locidx]['object_id'] == $object_id);
+                                       break;
+                               default:
+                                       $rackData[$unit_no][$locidx]['enabled'] = FALSE;
+                       }
+}
+
+// This function highlights specified object (and removes previous highlight).
+function highlightObject (&$rackData, $object_id)
+{
+       for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
+               for ($locidx = 0; $locidx < 3; $locidx++)
+                       if
+                       (
+                               $rackData[$unit_no][$locidx]['state'] == 'T' and
+                               $rackData[$unit_no][$locidx]['object_id'] == $object_id
+                       )
+                               $rackData[$unit_no][$locidx]['hl'] = 'h';
+                       else
+                               unset ($rackData[$unit_no][$locidx]['hl']);
+}
+
+// This function marks atoms to selected or not depending on their current state.
+function markupAtomGrid (&$data, $checked_state)
+{
+       for ($unit_no = $data['height']; $unit_no > 0; $unit_no--)
+               for ($locidx = 0; $locidx < 3; $locidx++)
+               {
+                       if (!($data[$unit_no][$locidx]['enabled'] === TRUE))
+                               continue;
+                       if ($data[$unit_no][$locidx]['state'] == $checked_state)
+                               $data[$unit_no][$locidx]['checked'] = ' checked';
+                       else
+                               $data[$unit_no][$locidx]['checked'] = '';
+               }
+}
+
+// This function is almost a clone of processGridForm(), but doesn't save anything to database
+// Return value is the changed rack data.
+// Here we assume that correct filter has already been applied, so we just
+// set or unset checkbox inputs w/o changing atom state.
+function mergeGridFormToRack (&$rackData)
+{
+       $rack_id = $rackData['id'];
+       for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
+               for ($locidx = 0; $locidx < 3; $locidx++)
+               {
+                       if ($rackData[$unit_no][$locidx]['enabled'] != TRUE)
+                               continue;
+                       $inputname = "atom_${rack_id}_${unit_no}_${locidx}";
+                       if (isset ($_REQUEST[$inputname]) and $_REQUEST[$inputname] == 'on')
+                               $rackData[$unit_no][$locidx]['checked'] = ' checked';
+                       else
+                               $rackData[$unit_no][$locidx]['checked'] = '';
+               }
+}
+
+function binMaskFromDec ($maskL)
+{
+       $binmask=0;
+       for ($i=0; $i<$maskL; $i++)
+       {
+               $binmask*=2;
+               $binmask+=1;
+       }
+       for ($i=$maskL; $i<32; $i++)
+       {
+               $binmask*=2;
+       }
+       return $binmask;
+}
+
+function binInvMaskFromDec ($maskL)
+{
+       $binmask=0;
+       for ($i=0; $i<$maskL; $i++)
+       {
+               $binmask*=2;
+       }
+       for ($i=$maskL; $i<32; $i++)
+       {
+               $binmask*=2;
+               $binmask+=1;
+       }
+       return $binmask;
+}
+
+function addRange ($ip='', $mask='', $name='')
+{
+       $ipL = ip2long($ip);
+       $maskL = ip2long($mask);
+       if ($ipL == -1 || $ipL === FALSE)
+               return 'Bad ip address';
+       if ($mask < 32 && $mask > 0)
+               $maskL = $mask;
+       else
+       {
+               $maskB = decbin($maskL);
+               if (strlen($maskB)!=32)
+                       return 'Bad mask';
+               $ones=0;
+               $zeroes=FALSE;
+               foreach( str_split ($maskB) as $digit)
+               {
+                       if ($digit == '0')
+                               $zeroes = TRUE;
+                       if ($digit == '1')
+                       {
+                               $ones++;
+                               if ($zeroes == TRUE)
+                                       return 'Bad mask';
+                       }
+               }
+               $maskL = $ones;
+       }
+       $binmask = binMaskFromDec($maskL);
+       $ipL = $ipL & $binmask;
+
+       $query =
+               "select ".
+               "id, ip, mask, name ".
+               "from IPRanges ";
+       
+
+       global $dbxlink;
+
+       $result = $dbxlink->query ($query);
+
+       while ($row = $result->fetch (PDO::FETCH_ASSOC))
+       {
+               $otherip = $row['ip'];
+               $othermask = binMaskFromDec($row['mask']);
+//             echo "checking $otherip & $othermask ".($otherip & $othermask)." == $ipL & $othermask ".($ipL & $othermask)." ".decbin($otherip)." ".decbin($othermask)." ".decbin($otherip & $othermask)." ".decbin($ipL)." ".decbin($othermask)." ".decbin($ipL & $othermask)."\n";
+//             echo "checking $otherip & $binmask ".($otherip & $binmask)." == $ipL & $binmask ".($ipL & $binmask)." ".decbin($otherip)." ".decbin($binmask)." ".decbin($otherip & $binmask)." ".decbin($ipL)." ".decbin($binmask)." ".decbin($ipL & $binmask)."\n";
+//             echo "\n";
+//             flush();
+               if (($otherip & $othermask) == ($ipL & $othermask))
+                       return "This subnet intersects with ".long2ip($row['ip'])."/${row['mask']}";
+               if (($otherip & $binmask) == ($ipL & $binmask))
+                       return "This subnet intersects with ".long2ip($row['ip'])."/${row['mask']}";
+       }
+       $result->closeCursor();
+       $query =
+               "insert into IPRanges set ip=".sprintf('%u', $ipL).", mask='$maskL', name='$name'";
+       $result = $dbxlink->exec ($query);
+       return '';
+}
+
+function getIPRange ($id = 0)
+{
+       global $dbxlink;
+       $query =
+               "select ".
+               "id as IPRanges_id, ".
+               "INET_NTOA(ip) as IPRanges_ip, ".
+               "ip as IPRanges_ip_bin, ".
+               "mask as IPRanges_mask, ".
+               "name as IPRanges_name ".
+               "from IPRanges ".
+               "where id = '$id'";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               return NULL;
+       }
+       else
+       {
+               $ret=array();
+               if ($row = $result->fetch (PDO::FETCH_ASSOC))
+               {
+                       $ret['id'] = $row['IPRanges_id'];
+                       $ret['ip'] = $row['IPRanges_ip'];
+                       $ret['ip_bin'] = ip2long($ret['ip']);
+                       $ret['mask_bin'] = binMaskFromDec($row['IPRanges_mask']);
+                       $ret['mask_bin_inv'] = binInvMaskFromDec($row['IPRanges_mask']);
+                       $ret['name'] = $row['IPRanges_name'];
+                       $ret['mask'] = $row['IPRanges_mask'];
+                       $ret['addrlist'] = array();
+
+                       $result->closeCursor();
+                       $query =
+                               "select ".
+                               "IPAddress.ip as ip_bin, ".
+                               "INET_NTOA(IPAddress.ip) as ip, ".
+                               "IPAddress.name as name, ".
+                               "IPAddress.reserved as reserved, ".
+                               "IPBonds.object_id as object_id, ".
+                               "RackObject.name as object_name, ".
+                               "IPBonds.name as bond_name, ".
+                               "IPBonds.type as bond_type ".
+                               "from IPAddress left join ".
+                                       "(IPBonds join RackObject on IPBonds.object_id = RackObject.id) ".
+                               "on IPAddress.ip = IPBonds.ip ".
+                               "where IPAddress.ip >= '".sprintf('%u', ($ret['ip_bin'] & $ret['mask_bin']))."' and ".
+                                       "IPAddress.ip <= '".sprintf('%u', ($ret['ip_bin'] | ($ret['mask_bin_inv']) ))."' ".
+                               "having (reserved='yes' or name != '' or IPAddress.name != '' or object_id is not NULL) ". 
+                               "order by IPAddress.ip, IPBonds.type, RackObject.name";
+                       $res_list=$dbxlink->query ($query);
+                       $prev_ip=0;
+                       if ($res_list != NULL)
+                       {
+                               while ($row1 = $res_list->fetch (PDO::FETCH_ASSOC))
+                               {
+                                       if ($prev_ip != $row1['ip'])
+                                       {
+                                               $refcount=0;
+                                               $count=ip2long($row1['ip']);
+                                               $ret['addrlist'][$count]['name'] = $row1['name'];
+                                               $ret['addrlist'][$count]['reserved'] = $row1['reserved'];
+                                               $ret['addrlist'][$count]['ip'] = $row1['ip'];
+                                               $ret['addrlist'][$count]['ip_bin'] = ip2long($row1['ip']);
+                                               $prev_ip = $ret['addrlist'][$count]['ip'];
+                                               $ret['addrlist'][$count]['references'] = array();
+                                       }
+
+                                       if ($row1['bond_type'])
+                                       {
+                                               $ret['addrlist'][$count]['references'][$refcount]['type'] = $row1['bond_type'];
+                                               $ret['addrlist'][$count]['references'][$refcount]['name'] = $row1['bond_name'];
+                                               $ret['addrlist'][$count]['references'][$refcount]['object_id'] = $row1['object_id'];
+                                               $ret['addrlist'][$count]['references'][$refcount]['object_name'] = $row1['object_name'];
+                                               $refcount++;
+                                       }
+                               }
+                               $res_list->closeCursor();
+                       }
+               }
+       }
+       return $ret;
+}
+
+function getIPAddress ($ip=0)
+{
+       $ret = array();
+       $ret['range'] = getIPRange($ip);
+       $ret['bonds'] = array();
+       $ret['outpf'] = array();
+       $ret['inpf'] = array();
+       $ret['exists'] = 0;
+       $ret['reserved'] = 'no';
+       global $dbxlink;
+       $query =
+               "select ".
+               "ip, name, reserved ".
+               "from IPAddress ".
+               "where ip = INET_ATON('$ip')";
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               return NULL;
+       }
+       else
+       {
+               if ($row = $result->fetch (PDO::FETCH_ASSOC))
+               {
+                       $ret['exists'] = 1;
+                       $ret['ip_bin'] = $row['ip'];
+                       $ret['ip'] = $ip;
+                       $ret['name'] = $row['name'];
+                       $ret['reserved'] = $row['reserved'];
+               }
+               $result->fetch (PDO::FETCH_ASSOC);
+       }
+       $result->closeCursor();
+
+       if ($ret['exists'] == 1)
+       {
+               $query =
+                       "select ".
+                       "IPBonds.object_id as object_id, ".
+                       "IPBonds.name as name, ".
+                       "IPBonds.type as type, ".
+                       "RackObject.name as object_name ".
+                       "from IPBonds join RackObject on IPBonds.object_id=RackObject.id ".
+                       "where IPBonds.ip=INET_ATON('$ip') ".
+                       "order by RackObject.id, IPBonds.name";
+               $result1 = $dbxlink->query ($query);
+               $count=0;
+               while ($row = $result1->fetch (PDO::FETCH_ASSOC))
+               {
+                       $ret['bonds'][$count]['object_id'] = $row['object_id'];
+                       $ret['bonds'][$count]['name'] = $row['name'];
+                       $ret['bonds'][$count]['type'] = $row['type'];
+                       $ret['bonds'][$count]['object_name'] = $row['object_name'];
+                       $count++;
+               }
+               $result1->closeCursor();
+
+
+       }
+
+       return $ret;
+}
+       
+function bindIpToObject ($ip='', $object_id=0, $name='', $type='')
+{
+       global $dbxlink;
+
+       $range = getRangeByIp($ip);
+
+       if (!$range)
+               return 'Non-existant ip address. Try adding IP range first';
+
+       $address = getIPAddress($ip);
+
+       if ($address['exists'] == 0)
+       {
+               $query =
+                       "insert into IPAddress set ip=INET_ATON('$ip')";
+               $dbxlink->exec ($query);
+       }
+
+       $query =
+               "insert into IPBonds set ip=INET_ATON('$ip'), object_id='$object_id', name='$name', type='$type'";
+       $result = $dbxlink->exec ($query);
+       return '';
+}
+
+// This function looks up 'has_problems' flag for 'T' atoms
+// and modifies 'hl' key. May be, this should be better done
+// in getRackData(). We don't honour 'skipped' key, because
+// the function is also used for thumb creation.
+function markupObjectProblems (&$rackData)
+{
+       for ($i = $rackData['height']; $i > 0; $i--)
+               for ($locidx = 0; $locidx < 3; $locidx++)
+                       if ($rackData[$i][$locidx]['state'] == 'T')
+                       {
+                               $object = getObjectInfo ($rackData[$i][$locidx]['object_id']);
+                               if ($object['has_problems'] == 'yes')
+                               {
+                                       // Object can be already highlighted.
+                                       if (isset ($rackData[$i][$locidx]['hl']))
+                                               $rackData[$i][$locidx]['hl'] = $rackData[$i][$locidx]['hl'] . 'w';
+                                       else
+                                               $rackData[$i][$locidx]['hl'] = 'w';
+                               }
+                       }
+}
+
+function search_cmpObj ($a, $b)
+{
+       return ($a['score'] > $b['score'] ? -1 : 1);
+}
+
+// This function performs search and then calculates score for each result.
+// Given previous search results in $objects argument, it adds new results
+// to the array and updates score for existing results, if it is greater than
+// existing score.
+function mergeSearchResults (&$objects, $terms, $fieldname)
+{
+       global $dbxlink;
+       $query =
+               "select name, label, asset_no, barcode, ro.id, dict_key as objtype_id, " .
+               "dict_value as objtype_name, asset_no from RackObject as ro inner join Dictionary " .
+               "on objtype_id = dict_key natural join Chapter where chapter_name = 'RackObjectType' and ";
+       $count = 0;
+       foreach (explode (' ', $terms) as $term)
+       {
+               if ($count) $query .= ' or ';
+               $query .= "${fieldname} like '%$term%'";
+               $count++;
+       }
+       $query .= "";
+       $result = $dbxlink->query($query);
+       if ($result == NULL)
+       {
+               showError ("SQL query failed in mergeSearchResults()");
+               return NULL;
+       }
+// 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))
+       {
+               foreach ($clist as $cname)
+                       $object[$cname] = $row[$cname];
+               $object['score'] = 0;
+               $object['dname'] = displayedName ($object);
+               unset ($object['objtype_id']);
+               foreach (explode (' ', $terms) as $term)
+                       if (strstr ($object['name'], $term))
+                               $object['score'] += 1;
+               unset ($object['name']);
+               if (!isset ($objects[$row['id']]))
+                       $objects[$row['id']] = $object;
+               elseif ($objects[$row['id']]['score'] < $object['score'])
+                       $objects[$row['id']]['score'] = $object['score'];
+       }
+       return $objects;
+}
+
+function getSearchResults ($terms)
+{
+       $objects = array();
+       mergeSearchResults ($objects, $terms, 'name');
+       mergeSearchResults ($objects, $terms, 'label');
+       mergeSearchResults ($objects, $terms, 'asset_no');
+       mergeSearchResults ($objects, $terms, 'barcode');
+       if (count ($objects) == 1)
+               usort ($objects, 'search_cmpObj');
+       return $objects;
+}
+
+// This function removes all colons and dots from a string.
+function l2addressForDatabase ($string)
+{
+       if (empty ($string))
+               return 'NULL';
+       $pieces = explode (':', $string);
+       // This workaround is for SunOS ifconfig.
+       foreach ($pieces as &$byte)
+               if (strlen ($byte) == 1)
+                       $byte = '0' . $byte;
+       // And this workaround is for PHP.
+       unset ($byte);
+       $string = implode ('', $pieces);
+       $pieces = explode ('.', $string);
+       $string = implode ('', $pieces);
+       $string = strtoupper ($string);
+       return "'$string'";
+}
+
+function l2addressFromDatabase ($string)
+{
+       switch (strlen ($string))
+       {
+               case 12: // Ethernet
+               case 16: // FireWire
+                       $ret = implode (':', str_split ($string, 2));
+                       break;
+               default:
+                       $ret = $string;
+                       break;
+       }
+       return $ret;
+}
+
+// The following 2 functions return previous and next rack IDs for
+// a given rack ID. The order of racks is the same as in renderRackspace()
+// or renderRow().
+function getPrevIDforRack ($row_id = 0, $rack_id = 0)
+{
+       if ($row_id <= 0 or $rack_id <= 0)
+       {
+               showError ('Invalid arguments passed to getPrevIDforRack()');
+               return NULL;
+       }
+       $rackList = getRacksForRow ($row_id);
+       doubleLink ($rackList);
+       if (isset ($rackList[$rack_id]['prev_key']))
+               return $rackList[$rack_id]['prev_key'];
+       return NULL;
+}
+
+function getNextIDforRack ($row_id = 0, $rack_id = 0)
+{
+       if ($row_id <= 0 or $rack_id <= 0)
+       {
+               showError ('Invalid arguments passed to getNextIDforRack()');
+               return NULL;
+       }
+       $rackList = getRacksForRow ($row_id);
+       doubleLink ($rackList);
+       if (isset ($rackList[$rack_id]['next_key']))
+               return $rackList[$rack_id]['next_key'];
+       return NULL;
+}
+
+// This function finds previous and next array keys for each array key and
+// modifies its argument accordingly.
+function doubleLink (&$array)
+{
+       $prev_key = NULL;
+       foreach (array_keys ($array) as $key)
+       {
+               if ($prev_key)
+               {
+                       $array[$key]['prev_key'] = $prev_key;
+                       $array[$prev_key]['next_key'] = $key;
+               }
+               $prev_key = $key;
+       }
+}
+
+// After applying usort() to a rack list we will lose original array keys.
+// This function restores the keys so they are equal to rack IDs.
+function restoreRackIDs ($racks)
+{
+       $ret = array();
+       foreach ($racks as $rack)
+               $ret[$rack['id']] = $rack;
+       return $ret;
+}
+
+function sortTokenize ($a, $b)
+{
+       $aold='';
+       while ($a != $aold)
+       {
+               $aold=$a;
+               $a = ereg_replace('[^a-zA-Z0-9]',' ',$a);
+               $a = ereg_replace('([0-9])([a-zA-Z])','\\1 \\2',$a);
+               $a = ereg_replace('([a-zA-Z])([0-9])','\\1 \\2',$a);
+       }
+
+       $bold='';
+       while ($b != $bold)
+       {
+               $bold=$b;
+               $b = ereg_replace('[^a-zA-Z0-9]',' ',$b);
+               $b = ereg_replace('([0-9])([a-zA-Z])','\\1 \\2',$b);
+               $b = ereg_replace('([a-zA-Z])([0-9])','\\1 \\2',$b);
+       }
+
+
+
+       $ar = explode(' ', $a);
+       $br = explode(' ', $b);
+       for ($i=0; $i<count($ar) && $i<count($br); $i++)
+       {
+               $ret = 0;
+               if (is_numeric($ar[$i]) and is_numeric($br[$i]))
+                       $ret = ($ar[$i]==$br[$i])?0:($ar[$i]<$br[$i]?-1:1);
+               else
+                       $ret = strcasecmp($ar[$i], $br[$i]);
+               if ($ret != 0)
+                       return $ret;
+       }
+       if ($i<count($ar))
+               return 1;
+       if ($i<count($br))
+               return -1;
+       return 0;
+}
+
+function sortByName ($a, $b)
+{
+       return sortTokenize($a['name'], $b['name']);
+}
+
+function eq ($a, $b)
+{
+       return $a==$b;
+}
+
+function neq ($a, $b)
+{
+       return $a!=$b;
+}
+
+function countRefsOfType ($refs, $type, $eq)
+{
+       $count=0;
+       foreach ($refs as $ref)
+       {
+               if ($eq($ref['type'], $type))
+                       $count++;
+       }
+       return $count;
+}
+
+function sortEmptyPorts ($a, $b)
+{
+       $objname_cmp = sortTokenize($a['Object_name'], $b['Object_name']);
+       if ($objname_cmp == 0)
+       {
+               return sortTokenize($a['Port_name'], $b['Port_name']);
+       }
+       return $objname_cmp;
+}
+
+function sortObjectAddressesAndNames ($a, $b)
+{
+       $objname_cmp = sortTokenize($a['object_name'], $b['object_name']);
+       if ($objname_cmp == 0)
+       {
+               $objname_cmp = sortTokenize($a['port_name'], $b['port_name']);
+               if ($objname_cmp == 0)
+               {
+                       sortTokenize($a['ip'], $b['ip']);
+               }
+               return $objname_cmp;
+       }
+       return $objname_cmp;
+}
+
+
+
+function sortAddresses ($a, $b)
+{
+       $name_cmp = sortTokenize($a['name'], $b['name']);
+       if ($name_cmp == 0)
+       {
+               return sortTokenize($a['ip'], $b['ip']);
+       }
+       return $name_cmp;
+}
+
+// This function expands port compat list into a matrix.
+function buildPortCompatMatrixFromList ($portTypeList, $portCompatList)
+{
+       $matrix = array();
+       // Create type matrix and markup compatible types.
+       foreach (array_keys ($portTypeList) as $type1)
+               foreach (array_keys ($portTypeList) as $type2)
+                       $matrix[$type1][$type2] = FALSE;
+       foreach ($portCompatList as $pair)
+               $matrix[$pair['type1']][$pair['type2']] = TRUE;
+       return $matrix;
+}
+
+function newPortForwarding($object_id, $localip, $localport, $remoteip, $remoteport, $proto, $description)
+{
+       global $dbxlink;
+
+       $range = getRangeByIp($localip);
+       if (!$range)
+               return "$localip: Non existant ip";
+       
+       $range = getRangeByIp($remoteip);
+       if (!$range)
+               return "$remoteip: Non existant ip";
+       
+       if ( ($localport <= 0) or ($localport >= 65536) )
+               return "$localport: invaild port";
+
+       if ( ($remoteport <= 0) or ($remoteport >= 65536) )
+               return "$remoteport: invaild port";
+
+        $query =
+                "insert into PortForwarding set object_id='$object_id', localip=INET_ATON('$localip'), remoteip=INET_ATON('$remoteip'), localport='$localport', remoteport='$remoteport', proto='$proto', description='$description'";
+        $result = $dbxlink->exec ($query);
+
+       return '';
+}
+
+function deletePortForwarding($object_id, $localip, $localport, $remoteip, $remoteport, $proto)
+{
+       global $dbxlink;
+
+       $query =
+               "delete from PortForwarding where object_id='$object_id' and localip=INET_ATON('$localip') and remoteip=INET_ATON('$remoteip') and localport='$localport' and remoteport='$remoteport' and proto='$proto'";
+       $result = $dbxlink->exec ($query);
+       return '';
+}
+
+function updatePortForwarding($object_id, $localip, $localport, $remoteip, $remoteport, $proto, $description)
+{
+       global $dbxlink;
+
+       $query =
+               "update PortForwarding set description='$description' where object_id='$object_id' and localip=INET_ATON('$localip') and remoteip=INET_ATON('$remoteip') and localport='$localport' and remoteport='$remoteport' and proto='$proto'";
+       $result = $dbxlink->exec ($query);
+       return '';
+}
+
+function getObjectForwards($object_id)
+{
+       global $dbxlink;
+
+       $ret = array();
+       $ret['out'] = array();
+       $ret['in'] = array();
+       $query =
+               "select ".
+               "dict_value as proto, ".
+               "proto as proto_bin, ".
+               "INET_NTOA(localip) as localip, ".
+               "localport, ".
+               "INET_NTOA(remoteip) as remoteip, ".
+               "remoteport, ".
+               "description ".
+               "from PortForwarding inner join Dictionary on proto = dict_key natural join Chapter ".
+               "where object_id='$object_id' and chapter_name = 'Protocols' ".
+               "order by localip, localport, proto, remoteip, remoteport";
+       $result2 = $dbxlink->query ($query);
+       $count=0;
+       while ($row = $result2->fetch (PDO::FETCH_ASSOC))
+       {
+               $ret['out'][$count]['proto'] = $row['proto'];
+               $ret['out'][$count]['proto_bin'] = $row['proto_bin'];
+               $ret['out'][$count]['localip'] = $row['localip'];
+               $ret['out'][$count]['localport'] = $row['localport'];
+               $ret['out'][$count]['remoteip'] = $row['remoteip'];
+               $ret['out'][$count]['remoteport'] = $row['remoteport'];
+               $ret['out'][$count]['description'] = $row['description'];
+               $count++;
+       }
+       $result2->closeCursor();
+
+       $query =
+               "select ".
+               "dict_value as proto, ".
+               "proto as proto_bin, ".
+               "INET_NTOA(localip) as localip, ".
+               "localport, ".
+               "INET_NTOA(remoteip) as remoteip, ".
+               "remoteport, ".
+               "PortForwarding.object_id as object_id, ".
+               "RackObject.name as object_name, ".
+               "description ".
+               "from ((PortForwarding join IPBonds on remoteip=ip) join RackObject on PortForwarding.object_id=RackObject.id) inner join Dictionary on proto = dict_key natural join Chapter ".
+               "where IPBonds.object_id='$object_id' and chapter_name = 'Protocols' ".
+               "order by remoteip, remoteport, proto, localip, localport";
+       $result3 = $dbxlink->query ($query);
+       $count=0;
+       while ($row = $result3->fetch (PDO::FETCH_ASSOC))
+       {
+               $ret['in'][$count]['proto'] = $row['proto'];
+               $ret['in'][$count]['proto_bin'] = $row['proto_bin'];
+               $ret['in'][$count]['localport'] = $row['localport'];
+               $ret['in'][$count]['localip'] = $row['localip'];
+               $ret['in'][$count]['remoteport'] = $row['remoteport'];
+               $ret['in'][$count]['remoteip'] = $row['remoteip'];
+               $ret['in'][$count]['object_id'] = $row['object_id'];
+               $ret['in'][$count]['object_name'] = $row['object_name'];
+               $ret['in'][$count]['description'] = $row['description'];
+               $count++;
+       }
+       $result3->closeCursor();
+
+       return $ret;
+}
+
+?>
diff --git a/inc/init.php b/inc/init.php
new file mode 100644 (file)
index 0000000..f1a037b
--- /dev/null
@@ -0,0 +1,83 @@
+<?
+/*
+*
+* This file performs RackTables initialisation. After you include it
+* from 1st-level page, don't forget to call authorize(). This is done
+* to allow reloading of pageno and tabno variables. pageno and tabno
+* together form security context.
+*
+*/
+
+$root = (empty($_SERVER['HTTPS'])?'http':'https').
+       '://'.
+       (isset($_SERVER['HTTP_HOST'])?$_SERVER['HTTP_HOST']:($_SERVER['SERVER_NAME'].($_SERVER['SERVER_PORT']=='80'?'':$_SERVER['SERVER_PORT']))).
+       dirname($_SERVER['PHP_SELF']).'/';
+
+// This is the first thing we need to do.
+require_once 'inc/config.php';
+
+// What we need first is database and interface functions.
+require_once 'inc/interface.php';
+require_once 'inc/functions.php';
+require_once 'inc/database.php';
+if (file_exists ('inc/secret.php'))
+       require_once 'inc/secret.php';
+else
+{
+       showError
+       (
+               "Database connection parameters are read from inc/secret.php file, " .
+               "which cannot be found.\nCopy provided inc/secret-sample.php to " .
+               "inc/secret.php and modify to your setup.\n\nThen reload the page."
+       );
+       die;
+}
+
+// Now try to connect...
+try
+{
+       $dbxlink = new PDO ($pdo_dsn, $db_username, $db_password);
+}
+catch (PDOException $e)
+{
+       showError ("Database connection failed:\n\n" . $e->getMessage());
+       die();
+}
+
+// Escape any globals before we ever try to use them.
+foreach ($_REQUEST as $key => $value)
+       if (gettype ($value) == 'string')
+               $_REQUEST[$key] = escapeString ($value);
+if (isset ($_SERVER['PHP_AUTH_USER']))
+       $_SERVER['PHP_AUTH_USER'] = escapeString ($_SERVER['PHP_AUTH_USER']);
+if (isset ($_SERVER['PHP_AUTH_PW']))
+       $_SERVER['PHP_AUTH_PW'] = escapeString ($_SERVER['PHP_AUTH_PW']);
+
+// Now init authentication.
+
+require_once 'inc/auth.php';
+// Load access database once.
+$accounts = getUserAccounts();
+$perms = getUserPermissions();
+if ($accounts === NULL or $perms === NULL)
+{
+       showError ('Failed to initialize access database.');
+       die();
+}
+
+authenticate();
+
+// Authentication passed.
+// Note that we don't perform autorization here, so each 1st level page
+// has to do it in its way, e.g. to call authorize().
+
+$remote_username = $_SERVER['PHP_AUTH_USER'];
+$pageno = (isset ($_REQUEST['page'])) ? $_REQUEST['page'] : 'index';
+$tabno = (isset ($_REQUEST['tab'])) ? $_REQUEST['tab'] : 'default';
+
+require_once 'inc/navigation.php';
+require_once 'inc/pagetitles.php';
+require_once 'inc/pagehandlers.php';
+require_once 'inc/ophandlers.php';
+
+?>
diff --git a/inc/interface.php b/inc/interface.php
new file mode 100644 (file)
index 0000000..5cf80e1
--- /dev/null
@@ -0,0 +1,2799 @@
+<?
+/*
+*
+*  This file contains frontend functions for RackTables.
+*
+*/
+
+// Main menu.
+function renderIndex ()
+{
+       global $root;
+?>
+<table border=0 cellpadding=0 cellspacing=0 width='100%'>
+       <tr>
+               <td>
+                       <div style='text-align: center; margin: 10px; '>
+                       <table width='100%' cellspacing=0 cellpadding=30 class=mainmenu border=0>
+                               <tr>
+                                       <td>
+                                               <h1><a href='<? echo $root; ?>?page=rackspace'>Rackspace<br>
+                                               <? printImageHREF ('rackspace'); ?></a></h1>
+                                       </td>
+                                       <td>
+                                               <h1><a href='<? echo $root; ?>?page=objects'>Objects<br>
+                                               <? printImageHREF ('objects'); ?></a></h1>
+                                       </td>
+                                       <td>
+                                                       <h1><a href='<? echo $root; ?>?page=ipv4space'>IPv4 space<br>
+                                                       <? printImageHREF ('ipv4space'); ?></a></h1>
+                                       </td>
+                               </tr>
+                       </table>
+                       <table width='100%' cellspacing=0 cellpadding=30 class=mainmenu border=0>
+                               <tr>
+                                       <td>
+                                                       <h1><a href='<? echo $root; ?>?page=config'>Configuration<br>
+                                                       <? printImageHREF ('config'); ?></a></h1>
+                                       </td>
+                                       <td>
+                                               <h1><a href='<? echo $root; ?>?page=reports'>Reports<br>
+                                               <? printImageHREF ('reports'); ?></a></h1>
+                                       </td>
+                               </tr>
+                       </table>
+                       </div>
+               </td>
+       </tr>
+</table>
+<?
+}
+
+function renderRackspace ()
+{
+?>
+       <table border=0 cellpadding=10 cellpadding=1>
+<?
+       // generate thumb gallery
+       $rackrowList = getRackRowInfo();
+       global $rtwidth, $root, $nextorder;
+       $rackwidth = $rtwidth[0] + $rtwidth[1] + $rtwidth[2];
+       $order = 'odd';
+       foreach ($rackrowList as $rackrow)
+       {
+               echo "<tr class=row_${order}><th><a href='${root}?page=row&row_id=${rackrow['dict_key']}'>${rackrow['dict_value']}</a></th>";
+               $rackList = getRacksForRow ($rackrow['dict_key']);
+               echo "<td><table border=0 cellspacing=5><tr>";
+               foreach ($rackList as $dummy => $rack)
+               {
+                       echo "<td align=center><a href='${root}?page=rack&rack_id=${rack['id']}'>";
+                       echo "<img border=0 width=${rackwidth} height=";
+                       echo 3 + 3 + $rack['height'] * 2;
+                       echo " title='${rack['height']} units'";
+                       echo "src='render_rack_thumb.php?rack_id=${rack['id']}'>";
+                       echo "<br>${rack['name']}</a></td>";
+               }
+               echo "</tr></table></tr>\n";
+               $order = $nextorder[$order];
+       }
+       echo "</table>\n";
+}
+
+function renderRow ($row_id)
+{
+       if ($row_id == 0)
+       {
+               showError ('Invalid row_id in renderRow()');
+               return;
+       }
+       if (($rowInfo = getRackRowInfo ($row_id)) == NULL)
+       {
+               showError ('getRackRowInfo() failed in renderRow()');
+               return;
+       }
+       // Main layout starts.
+       echo "<table border=0 class=objectview cellspacing=0 cellpadding=0>";
+
+       // Left portlet with row information.
+       echo "<tr><td class=pcleft>";
+       startPortlet ('Row information');
+       echo "<table border=0 cellspacing=0 cellpadding=3 width='100%'>\n";
+       echo "<tr><th width='50%' class=tdright>Row name:</th><td class=tdleft>${rowInfo['dict_value']}</td></tr>\n";
+       echo "<tr><th width='50%' class=tdright>Racks:</th><td class=tdleft>${rowInfo['count']}</td></tr>\n";
+       echo "<tr><th width='50%' class=tdright>Units:</th><td class=tdleft>${rowInfo['sum']}</td></tr>\n";
+       echo "</table><br>\n";
+       finishPortlet();
+
+       echo "</td><td class=pcright>";
+
+       global $rtwidth, $root, $nextorder;
+       $rackwidth = $rtwidth[0] + $rtwidth[1] + $rtwidth[2];
+       $rackList = getRacksForRow ($row_id);
+       $order = 'odd';
+       startPortlet ('Racks');
+       echo "<table border=0 cellspacing=5 align='center'><tr>";
+       foreach ($rackList as $dummy => $rack)
+       {
+               echo "<td align=center class=row_${order}><a href='${root}?page=rack&rack_id=${rack['id']}'>";
+               echo "<img border=0 width=" . $rackwidth * ROW_SCALE . " height=";
+               echo (3 + 3 + $rack['height'] * 2) * ROW_SCALE;
+               echo " title='${rack['height']} units'";
+               echo "src='render_rack_thumb.php?rack_id=${rack['id']}'>";
+               echo "<br>${rack['name']}</a></td>";
+               $order = $nextorder[$order];
+       }
+       echo "</tr></table>\n";
+       finishPortlet();
+
+       echo "</td></tr></table>";
+}
+
+function showError ($info = '')
+{
+       global $root;
+       echo '<div class=msg_error>An error has occured. ';
+       if (empty ($info))
+               echo 'No additional information is available.';
+       else
+               echo "Additional information:<br><p>\n<pre>\n${info}\n</pre></p>";
+       echo "Go back or try starting from <a href='${root}'>index page</a>.<br></div>\n";
+}
+
+// This function renders rack as HTML table.
+function renderRack ($rack_id = 0, $hl_obj_id = 0)
+{
+       if ($rack_id == 0)
+       {
+               showError ('Invalid rack_id in renderRack()');
+               return;
+       }
+       if (($rackData = getRackData ($rack_id)) == NULL)
+       {
+               showError ('getRackData() failed in renderRack()');
+               return;
+       }
+       global $root, $pageno, $tabno;
+       markAllSpans ($rackData);
+       if ($hl_obj_id > 0)
+               highlightObject ($rackData, $hl_obj_id);
+       markupObjectProblems ($rackData);
+       $prev_id = getPrevIDforRack ($rackData['row_id'], $rack_id);
+       $next_id = getNextIDforRack ($rackData['row_id'], $rack_id);
+       echo "<center>\n<h2><a href='${root}?page=row&row_id=${rackData['row_id']}'>${rackData['row_name']}</a> :";
+       // FIXME: use 'bypass'?
+       if ($prev_id != NULL)
+               echo " <a href='${root}?page=rack&rack_id=${prev_id}'>&lt; &lt; &lt;</a>";
+       echo " <a href='${root}?page=rack&rack_id=${rackData['id']}'>${rackData['name']}</a>";
+       if ($next_id != NULL)
+               echo " <a href='${root}?page=rack&rack_id=${next_id}'>&gt; &gt; &gt;</a>";
+       echo "</h2>\n";
+       echo "<table class=rack border=0 cellspacing=0 cellpadding=1>\n";
+       echo "<tr><th width='10%'>&nbsp;</th><th width='20%'>Front</th>";
+       echo "<th width='50%'>Interior</th><th width='20%'>Back</th></tr>\n";
+       for ($i = $rackData['height']; $i > 0; $i--)
+       {
+               echo "<tr><th>$i</th>";
+               for ($locidx = 0; $locidx < 3; $locidx++)
+               {
+                       if (isset ($rackData[$i][$locidx]['skipped']))
+                               continue;
+                       $state = $rackData[$i][$locidx]['state'];
+                       echo "<td class=state_${state}";
+                       if (isset ($rackData[$i][$locidx]['hl']))
+                               echo $rackData[$i][$locidx]['hl'];
+                       if (isset ($rackData[$i][$locidx]['colspan']))
+                               echo ' colspan=' . $rackData[$i][$locidx]['colspan'];
+                       if (isset ($rackData[$i][$locidx]['rowspan']))
+                               echo ' rowspan=' . $rackData[$i][$locidx]['rowspan'];
+                       echo ">";
+                       switch ($state)
+                       {
+                               case 'T':
+                                       $objectData = getObjectInfo ($rackData[$i][$locidx]['object_id']);
+                                       if (!empty ($objectData['asset_no']))
+                                               $prefix = "<div title='${objectData['asset_no']}";
+                                       else
+                                               $prefix = "<div title='no asset tag";
+                                       // Don't tell about label, if it matches common name.
+                                       if ($objectData['name'] != $objectData['label'] and !empty ($objectData['label']))
+                                               $suffix = ", visible label is \"${objectData['label']}\"'>";
+                                       else
+                                               $suffix = "'>";
+                                       echo $prefix . $suffix;
+                                       echo "<a href='${root}?page=object&object_id=${objectData['id']}'>${objectData['dname']}</a></div>";
+                                       break;
+                               case 'A':
+                                       echo '<div title="This rackspace does not exist">&nbsp;</div>';
+                                       break;
+                               case 'F':
+                                       echo '<div title="Free rackspace">&nbsp;</div>';
+                                       break;
+                               case 'U':
+                                       echo '<div title="Problematic rackspace, you CAN\'T mount here">&nbsp;</div>';
+                                       break;
+                               default:
+                                       echo '<div title="No data">&nbsp;</div>';
+                                       break;
+                       }
+                       echo '</td>';
+               }
+               echo "</tr>\n";
+       }
+       echo "</table></center>\n";
+}
+
+function renderNewObjectForm ()
+{
+       global $pageno, $tabno;
+
+       // Look for current submit.
+       if (isset ($_REQUEST['got_data']))
+       {
+               $log = array();
+               assertUIntArg ('object_type_id');
+               assertStringArg ('object_name', TRUE);
+               assertStringArg ('object_label', TRUE);
+               assertStringArg ('object_barcode', TRUE);
+               assertStringArg ('object_asset_no', TRUE);
+               $type_id = $_REQUEST['object_type_id'];
+               $name = $_REQUEST['object_name'];
+               $label = $_REQUEST['object_label'];
+               $asset_no = $_REQUEST['object_asset_no'];
+               $barcode = $_REQUEST['object_barcode'];
+
+               if (commitAddObject ($name, $label, $barcode, $type_id, $asset_no) === TRUE)
+                       $log[] = array ('code' => 'success', 'message' => "Added new object '${name}'");
+               else
+                       $log[] = array ('code' => 'error', 'message' => 'commitAddObject() failed in renderNewObjectForm()');
+               printLog ($log);
+       }
+
+       // Render a form for the next.
+       startPortlet ('Object attributes');
+       echo '<form>';
+       echo "<input type=hidden name=page value=${pageno}>";
+       echo "<input type=hidden name=tab value=${tabno}>";
+       echo '<table border=0 align=center>';
+       echo "<tr><th class=tdright>Type:</th><td class=tdleft>";
+       printSelect (getObjectTypeList(), 'object_type_id');
+       echo "</td></tr>\n";
+       echo "<tr><th class=tdright>Common name:</th><td class=tdleft><input type=text name=object_name></td></tr>\n";
+       echo "<tr><th class=tdright>Visible label:</th><td class=tdleft><input type=text name=object_label></td></tr>\n";
+       echo "<tr><th class=tdright>Asset tag:</th><td class=tdleft><input type=text name=object_asset_no></td></tr>\n";
+       echo "<tr><th class=tdright>Barcode:</th><td class=tdleft><input type=text name=object_barcode></td></tr>\n";
+       echo "<tr><td class=submit colspan=2><input type=submit name=got_data value='Create'></td></tr>\n";
+       echo '</form></table>';
+       finishPortlet();
+}
+
+function renderNewRackForm ($row_id)
+{
+       global $pageno, $tabno;
+
+       // Look for current submit.
+       if (isset ($_REQUEST['got_data']))
+       {
+               $log = array();
+               assertStringArg ('rack_name');
+               assertUIntArg ('rack_height');
+               assertStringArg ('rack_comment', TRUE);
+               $name = $_REQUEST['rack_name'];
+               $height = $_REQUEST['rack_height'];
+               $comment = $_REQUEST['rack_comment'];
+
+               if (commitAddRack ($name, $height, $row_id, $comment) === TRUE)
+                       $log[] = array ('code' => 'success', 'message' => "Added new rack '${name}'");
+               else
+                       $log[] = array ('code' => 'error', 'message' => 'commitAddRack() failed in renderNewRackForm()');
+               printLog ($log);
+       }
+
+       // Render a form for the next.
+       startPortlet ('Rack attributes');
+       echo '<form>';
+       echo "<input type=hidden name=page value=${pageno}>";
+       echo "<input type=hidden name=tab value=${tabno}>";
+       echo "<input type=hidden name=row_id value=${row_id}>";
+       echo '<table border=0 align=center>';
+       echo "<tr><th class=tdright>Name (required):</th><td class=tdleft><input type=text name=rack_name></td></tr>\n";
+       echo "<tr><th class=tdright>Height in units (required):</th><td class=tdleft><input type=text name=rack_height></td></tr>\n";
+       echo "<tr><th class=tdright>Comment:</th><td class=tdleft><input type=text name=rack_comment></td></tr>\n";
+       echo "<tr><td class=submit colspan=2><input type=submit name=got_data value='Create'></td></tr>\n";
+       echo '</form></table>';
+       finishPortlet();
+}
+
+function renderEditObjectForm ($object_id)
+{
+       showMessageOrError();
+       // Handle submit.
+       if (isset ($_REQUEST['got_data']))
+       {
+               $log = array();
+               // object_id is already verified by page handler
+               assertUIntArg ('object_type_id');
+               assertStringArg ('object_name', TRUE);
+               assertStringArg ('object_label', TRUE);
+               assertStringArg ('object_barcode', TRUE);
+               assertStringArg ('object_asset_no', TRUE);
+               $type_id = $_REQUEST['object_type_id'];
+               if (isset ($_REQUEST['object_has_problems']) and $_REQUEST['object_has_problems'] == 'on')
+                       $has_problems = 'yes';
+               else
+                       $has_problems = 'no';
+               $name = $_REQUEST['object_name'];
+               $label = $_REQUEST['object_label'];
+               $barcode = $_REQUEST['object_barcode'];
+               $asset_no = $_REQUEST['object_asset_no'];
+               $comment = $_REQUEST['object_comment'];
+
+               if (commitUpdateObject ($object_id, $name, $label, $barcode, $type_id, $has_problems, $asset_no, $comment) === TRUE)
+                       $log[] = array ('code' => 'success', 'message' => "Updated object '${name}'");
+               else
+                       $log[] = array ('code' => 'error', 'message' => 'commitUpdateObject() failed in renderEditObjectForm()');
+               printLog ($log);
+       }
+
+       global $pageno, $tabno;
+       $object = getObjectInfo ($object_id);
+       if ($object == NULL)
+       {
+               showError ('getObjectInfo() failed in renderEditObjectForm()');
+               return;
+       }
+
+       // Render a form for the next submit;
+       echo '<table border=0 width=100%><tr>';
+
+       echo '<td class=pcleft>';
+       startPortlet ('Static attributes');
+       echo '<form>';
+       echo "<input type=hidden name=page value=${pageno}>";
+       echo "<input type=hidden name=tab value=${tabno}>";
+       echo "<input type=hidden name=object_id value=${object_id}>";
+       echo '<table border=0 align=center>';
+       echo "<tr><th class=tdright>Type:</th><td class=tdleft>";
+       printSelect (getObjectTypeList(), 'object_type_id', $object['objtype_id']);
+       echo "</td></tr>\n";
+       // Common attributes.
+       echo "<tr><th class=tdright>Common name:</th><td class=tdleft><input type=text name=object_name value='${object['name']}'></td></tr>\n";
+       echo "<tr><th class=tdright>Visible label:</th><td class=tdleft><input type=text name=object_label value='${object['label']}'></td></tr>\n";
+       echo "<tr><th class=tdright>Asset tag:</th><td class=tdleft><input type=text name=object_asset_no value='${object['asset_no']}'></td></tr>\n";
+       echo "<tr><th class=tdright>Barcode:</th><td class=tdleft><input type=text name=object_barcode value='${object['barcode']}'></td></tr>\n";
+       echo "<tr><th class=tdright>Has problems:</th><td class=tdleft><input type=checkbox name=object_has_problems";
+       if ($object['has_problems'] == 'yes')
+               echo ' checked';
+       echo "></td></tr>\n";
+       echo "<tr><td colspan=2><b>Comment:</b><br><textarea name=object_comment rows=10 cols=80>${object['comment']}</textarea></td></tr>";
+       echo "<tr><th class=submit colspan=2><input type=submit name=got_data value='Update'></td></tr>\n";
+       echo '</form></table><br>';
+       finishPortlet();
+       echo '</td>';
+       
+       // Optional attributes.
+       echo '<td class=pcright>';
+       startPortlet ('Optional attributes');
+       $values = getAttrValues ($object_id);
+       global $root;
+       echo "<table cellspacing=0 cellpadding=5 align=center class=widetable>\n";
+       echo "<tr><th>&nbsp;</th><th>Attribute</th><th>Value</th><th>&nbsp;</th></tr>\n";
+       foreach ($values as $record)
+       {
+               echo "<form method=post action='${root}process.php'>";
+               echo "<input type=hidden name=page value=${pageno}>";
+               echo "<input type=hidden name=tab value=${tabno}>";
+               echo "<input type=hidden name=op value=upd>";
+               echo "<input type=hidden name=object_id value=${object_id}>";
+               echo "<input type=hidden name=attr_id value=${record['id']}>";
+               echo "<tr><td><a href=${root}process.php?page=${pageno}&tab=${tabno}&op=del&object_id=${object_id}&attr_id=${record['id']}>";
+               printImageHREF ('delete', 'Delete value');
+               echo "</a></td>";
+               echo "<td class=tdright>${record['name']}:</td><td class=tdleft>";
+               switch ($record['type'])
+               {
+                       case 'uint':
+                       case 'float':
+                       case 'string':
+                               echo "<input type=text name=value value='${record['value']}'>";
+                               break;
+                       case 'dict':
+                               $chapter = readChapter ($record['chapter_name']);
+                               $chapter[] = array ('dict_key' => 0, 'dict_value' => '-- NOT SET --');
+                               printSelect ($chapter, 'value', $record['key']);
+                               break;
+               }
+               echo "</td>";
+               echo "<td><input type=submit value='OK'></td></tr>\n";
+               echo "</form>";
+       }
+       echo "</table>\n";
+       finishPortlet();
+       echo '</td>';
+
+       echo '</tr><tr>';
+
+       echo '<td colspan=2>';
+       startPortlet ('history');
+       renderHistory ($pageno, $object_id);
+       finishPortlet();
+       echo '</td>';
+
+       echo '</tr></table>';
+}
+
+// This is a clone of renderEditObjectForm().
+function renderEditRackForm ($rack_id)
+{
+       // Handle submit.
+       if (isset ($_REQUEST['got_data']))
+       {
+               $log = array();
+               assertUIntArg ('rack_row_id');
+               assertUIntArg ('rack_height');
+               assertStringArg ('rack_name');
+               assertStringArg ('rack_comment', TRUE);
+               $row_id = $_REQUEST['rack_row_id'];
+               $height = $_REQUEST['rack_height'];
+               $name = $_REQUEST['rack_name'];
+               $comment = $_REQUEST['rack_comment'];
+
+               if (commitUpdateRack ($rack_id, $name, $height, $row_id, $comment) === TRUE)
+                       $log[] = array ('code' => 'success', 'message' => "Updated rack '${name}'");
+               else
+                       $log[] = array ('code' => 'error', 'message' => 'commitUpdateRack() failed in renderEditRackForm()');
+               printLog ($log);
+       }
+
+       global $pageno, $tabno;
+       $rack = getRackData ($rack_id);
+       if ($rack == NULL)
+       {
+               showError ('getRackData() failed in renderEditRackForm()');
+               return;
+       }
+
+       // Render a form for the next.
+       startPortlet ('Rack attributes');
+       echo '<form>';
+       echo "<input type=hidden name=page value=${pageno}>";
+       echo "<input type=hidden name=tab value=${tabno}>";
+       echo "<input type=hidden name=rack_id value=${rack_id}>";
+       echo '<table border=0 align=center>';
+       echo "<tr><th class=tdright>Rack row:</th><td class=tdleft>";
+       printSelect (getRackRowInfo(), 'rack_row_id', $rack['row_id']);
+       echo "</td></tr>\n";
+       echo "<tr><th class=tdright>Name (required):</th><td class=tdleft><input type=text name=rack_name value='${rack['name']}'></td></tr>\n";
+       echo "<tr><th class=tdright>Height (required):</th><td class=tdleft><input type=text name=rack_height value='${rack['height']}'></td></tr>\n";
+       echo "<tr><th class=tdright>Comment:</th><td class=tdleft><input type=text name=rack_comment value='${rack['comment']}'></td></tr>\n";
+       echo "<tr><td class=submit colspan=2><input type=submit name=got_data value='Update'></td></tr>\n";
+       echo '</form></table><br>';
+       finishPortlet();
+       
+       startPortlet ('History');
+       renderHistory ($pageno, $rack_id);
+       finishPortlet();
+}
+
+// This is a helper for creators and editors.
+function printSelect ($rowList, $select_name, $selected_id = 1)
+{
+       echo "<select name=${select_name}>";
+       foreach ($rowList as $dummy => $data)
+       {
+               echo "<option value=${data['dict_key']}";
+               if ($data['dict_key'] == $selected_id)
+                       echo ' selected';
+               echo ">${data['dict_value']}</option>";
+       }
+       echo "</select>";
+}
+
+// This is a universal editor of rack design/waste.
+function renderGridForm ($rack_id = 0, $filter, $header, $submit, $help, $state1, $state2)
+{
+       if ($rack_id == 0)
+       {
+               showError ('Invalid rack_id in renderGridFrom()');
+               return;
+       }
+       if (($rackData = getRackData ($rack_id)) == NULL)
+       {
+               showError ('getRackData() failed in renderGridFrom()');
+               return;
+       }
+
+       global $root, $pageno, $tabno;
+       $filter($rackData);
+       markupObjectProblems ($rackData);
+
+       // Process form submit.
+       if (isset ($_REQUEST['do_update']))
+       {
+               $log[] = processGridForm ($rackData, $state1, $state2);
+               printLog($log);
+       }
+
+       // Render the result whatever it is.
+       // Main layout.
+       echo "<table border=0 class=objectview cellspacing=0 cellpadding=0>";
+       echo "<tr><td colspan=2 align=center><h1>${rackData['name']}</h1></td></tr>\n";
+
+       // Left column with information portlet.
+       echo "<tr><td class=pcleft height='1%' width='50%'>";
+       startPortlet ('Rack information');
+       echo "<table border=0 cellspacing=0 cellpadding=3 width='100%'>\n";
+       echo "<tr><th width='50%' class=tdright>Rack name:</th><td class=tdleft>${rackData['name']}</td></tr>\n";
+       echo "<tr><th width='50%' class=tdright>Height:</th><td class=tdleft>${rackData['height']}</td></tr>\n";
+       echo "<tr><th width='50%' class=tdright>Rack row:</th><td class=tdleft>${rackData['row_name']}</td></tr>\n";
+       echo "<tr><th width='50%' class=tdright>Comment:</th><td class=tdleft>${rackData['comment']}</td></tr>\n";
+       echo "</table>\n";
+       finishPortlet();
+
+       echo "</td>\n";
+       echo "<td class=pcright rowspan=2>";
+
+       // Grid form.
+       startPortlet ($header);
+       echo "<center>\n";
+       echo "<table class=rack border=0 cellspacing=0 cellpadding=1>\n";
+       echo "<tr><th width='10%'>&nbsp;</th><th width='20%'>Front</th>";
+       echo "<th width='50%'>Interior</th><th width='20%'>Back</th></tr>\n";
+       echo "<form action='${root}?'>\n";
+       echo "<input type=hidden name=page value=${pageno}>\n";
+       echo "<input type=hidden name=tab value=${tabno}>\n";
+       echo "<input type=hidden name=rack_id value=${rack_id}>\n";
+       markupAtomGrid ($rackData, $state2);
+       renderAtomGrid ($rackData);
+       echo "</table></center>\n";
+       echo "<br><input type=submit name=do_update value='${submit}'></form><br><br>\n";
+       finishPortlet();
+       echo "</td></tr>\n";
+
+       // Help portlet.
+       echo '<tr><td class=pcleft>';
+       startPortlet ('Help');
+       echo "<p align=justify style='margin-left: 30px; margin-right: 30px;'>$help</p>";
+       finishPortlet();
+       echo "</td></td></table>\n";
+}
+
+function renderRackDesign ($rack_id)
+{
+       $help =
+               "Rack design defines the physical layout of a rack cabinet. " .
+               "Most common reason to use this tab is absence of back rails, although " .
+               "any other design can be defined. " .
+               "In this tab you can change atoms' state between 'free' and 'absent'.<br>" .
+               "A selected checkbox means atom presence.";
+       renderGridForm ($rack_id, 'applyRackDesignMask', 'Rack design', 'Set rack design', $help, 'A', 'F');
+}
+
+function renderRackProblems ($rack_id = 0)
+{
+       $help =
+               "Rack problems prevent free rackspace from being used for mounting. Such rackspace is considered " .
+               "unusable. After the problem is gone, the atom can become free again. " .
+               "In this tab you can change atoms' state from free to unusable and back.<br>" .
+               "A selected checkbox means a problem.";
+       renderGridForm ($rack_id, 'applyRackProblemMask', 'Rack problems', 'Mark unusable atoms', $help, 'F', 'U');
+}
+
+function startPortlet ($title = '')
+{
+       echo "<div class=portlet><h2>${title}</h2>";
+}
+
+function finishPortlet ()
+{
+       echo "</div>\n";
+}
+
+function printRefsOfType ($refs, $type, $eq)
+{
+       global $root;
+       $gotone=0;
+       foreach ($refs as $ref)
+       {
+               if ($eq($ref['type'], $type))
+               {
+                       if ($gotone) echo ', ';
+                       echo "<a href='${root}?page=object&object_id=${ref['object_id']}'>${ref['object_name']}</a>";
+                       $gotone=1;
+               }
+       }
+}
+
+function renderRackObject ($object_id = 0)
+{
+       global $root;
+       if ($object_id <= 0)
+       {
+               showError ('Invalid object_id in renderRackObject()');
+               return;
+       }
+       $info = getObjectInfo ($object_id);
+       if ($info == NULL)
+       {
+               showError ('getObjectInfo() failed in renderRackObject()');
+               return;
+       }
+       // Main layout starts.
+       echo "<table border=0 class=objectview cellspacing=0 cellpadding=0>";
+       echo "<tr><td colspan=2 align=center><h1>${info['dname']}</h1></td></tr>\n";
+       // left column with uknown number of portlets
+       echo "<tr><td class=pcleft>";
+       startPortlet ('Object information');
+       echo "<table border=0 cellspacing=0 cellpadding=3 width='100%'>\n";
+       if (!empty ($info['name']))
+               echo "<tr><th width='50%' class=tdright>Common name:</th><td class=tdleft>${info['name']}</td></tr>\n";
+       else
+               switch ($info['objtype_id'])
+               {
+                       case TYPE_SERVER:
+                       case TYPE_SWITCH:
+                       case TYPE_ROUTER:
+                               echo "<tr><td colspan=2 class=msg_error>Common name is missing.</td></tr>\n";
+               }
+       echo "<tr><th width='50%' class=tdright>Object type:</th><td class=tdleft>${info['objtype_name']}</td></tr>\n";
+       if (!empty ($info['asset_no']))
+               echo "<tr><th width='50%' class=tdright>Asset tag:</th><td class=tdleft>${info['asset_no']}</td></tr>\n";
+       else
+               switch ($info['objtype_id'])
+               {
+                       case TYPE_SERVER:
+                       case TYPE_SWITCH:
+                       case TYPE_ROUTER:
+                               echo "<tr><td colspan=2 class=msg_error>Asset tag is missing.</td></tr>\n";
+               }
+       if (!empty ($info['label']))
+               echo "<tr><th width='50%' class=tdright>Visible label:</th><td class=tdleft>${info['label']}</td></tr>\n";
+       if (!empty ($info['barcode']))
+               echo "<tr><th width='50%' class=tdright>Barcode:</th><td class=tdleft>${info['barcode']}</td></tr>\n";
+       if ($info['has_problems'] == 'yes')
+               echo "<tr><td colspan=2 class=msg_error>Has problems</td></tr>\n";
+       foreach (getAttrValues ($object_id) as $record)
+               if (!empty ($record['value']))
+                       echo "<tr><th width='50%' class=opt_attr_th>${record['name']}:</th><td class=tdleft>${record['value']}</td></tr>\n";
+       echo "</table><br>\n";
+       finishPortlet();
+
+       if (!empty ($info['comment']))
+       {
+               startPortlet ('Comment');
+               echo '<div class=commentblock>' . $info['comment'] . '</div>';
+               finishPortlet ();
+       }
+
+       $ports = getObjectPortsAndLinks ($object_id);
+       if (count ($ports))
+       {
+               startPortlet ('Ports and links');
+               usort($ports, 'sortByName');
+               if ($ports)
+               {
+                       $hl_port_id = 0;
+                       if (isset ($_REQUEST['hl_port_id']))
+                       {
+                               assertUIntArg ('hl_port_id');
+                               $hl_port_id = $_REQUEST['hl_port_id'];
+                       }
+                       echo "<table cellspacing=0 cellpadding='5' align='center' class='widetable'>\n";
+                       echo "<tr><th>Local name</th><th>Visible label</th><th>Port type</th><th>L2 address</th>";
+                       echo "<th>Rem. Object</th><th>Rem. port</th></tr>\n";
+                       foreach ($ports as $port)
+                       {
+                               echo '<tr';
+                               if ($hl_port_id == $port['id'])
+                                       echo ' class=port_highlight';
+                               echo "><td>${port['name']}</td><td>${port['label']}</td><td>${port['type']}</td>";
+                               echo "<td>${port['l2address']}</td>";
+                               if ($port['remote_object_id'])
+                               {
+                                       echo "<td><a href='${root}?page=object&object_id=${port['remote_object_id']}'>${port['remote_object_name']}</a></td>";
+                                       echo "<td>${port['remote_name']}</td>";
+                               }
+                               elseif (!empty ($port['reservation_comment']))
+                               {
+                                       echo "<td><b>Reserved;</b></td>";
+                                       echo "<td>${port['reservation_comment']}</td>";
+                               }
+                               else
+                                       echo '<td>&nbsp;</td><td>&nbsp;</td>';
+                               echo "</tr>\n";
+                       }
+                       echo "</table><br>\n";
+               }
+               finishPortlet();
+       }
+       $addresses = getObjectAddresses ($object_id);
+       usort($addresses, 'sortAddresses');
+       if (count ($addresses))
+       {
+               startPortlet ('Network addresses');
+               echo "<table cellspacing=0 cellpadding='5' align='center' class='widetable'>\n";
+               echo "<tr><th>Interface name</th><th>IP Address</th><th>Description</th><th>Misc</th></tr>\n";
+               foreach ($addresses as $addr)
+               {
+                       if (strlen($addr['address_name'])>40)
+                               $address_name = substr($addr['address_name'],0,38).'...';
+                       else
+                               $address_name = $addr['address_name'];
+
+                       $virtnum = countRefsOfType($addr['references'], 'virtual', 'eq');
+                       $sharednum = countRefsOfType($addr['references'], 'shared', 'eq');
+                       $regnum = countRefsOfType($addr['references'], 'regular', 'eq');
+                       $notvirtnum = countRefsOfType($addr['references'], 'virtual', 'neq');
+
+                       if ($addr['address_reserved']=='yes')
+                               $class='trwarning';
+                       elseif ($addr['type']!='virtual' && $regnum>0)
+                               $class='trwarning';
+                       elseif ($addr['type']=='regular' && $sharednum>0)
+                               $class='trwarning';
+                       else 
+                               $class='';
+
+                       echo "<tr class='$class'><td>${addr['name']}</td><td><a href='${root}?page=ipaddress&ip=${addr['ip']}'>${addr['ip']}</a></td><td class='description'>$address_name</td><td>\n";
+
+                       if ($addr['address_reserved']=='yes')
+                               echo "<b>Reserved;</b> ";
+
+                       if ($addr['type'] == 'virtual')
+                       {
+                               echo "<b>V</b>";
+                               if ($notvirtnum > 0)
+                               {
+                                       echo " Owners: ";
+                                       printRefsOfType($addr['references'], 'virtual', 'neq');
+                               }
+                       }
+                       elseif ($addr['type'] == 'shared')
+                       {
+                               echo "<b>S</b>";
+                               if ($sharednum > 0)
+                               {
+                                       echo " Peers: ";
+                                       printRefsOfType($addr['references'], 'shared', 'eq');
+                                       echo ";";
+                               }
+                               if ($virtnum > 0)
+                               {
+                                       echo " Virtuals: ";
+                                       printRefsOfType($addr['references'], 'virtual', 'eq');
+                                       echo ";";
+                               }
+                               if ($regnum > 0)
+                               {
+                                       echo " Collisions: ";
+                                       printRefsOfType($addr['references'], 'regular', 'eq');
+                               }
+                               
+                       }
+                       else
+                       {
+                               if ($virtnum > 0)
+                               {
+                                       echo " Virtuals: ";
+                                       printRefsOfType($addr['references'], 'virtual', 'eq');
+                                       echo ";";
+                               }
+                               if ($notvirtnum > 0)
+                               {
+                                       echo " Collisions: ";
+                                       printRefsOfType($addr['references'], 'virtual', 'neq');
+                               }
+                       }
+
+                       echo "</td></tr>\n";
+               }
+               echo "</table><br>\n";
+               finishPortlet();
+       }
+
+       $forwards = getObjectForwards ($object_id);
+       if (count($forwards['in']) or count($forwards['out']))
+       {
+               startPortlet('Port Forwarding');
+
+               if (count($forwards['out']))
+               {
+
+                       echo "<h3>Forwarding out:</h3>";
+
+                       echo "<table class='widetable' cesspadding=5 cellspacing=0 border=0 align='center'>\n";
+                       echo "<tr><th>Source</th><th>Target</th><th>Target Objects</th><th>Description</th></tr>\n";
+
+                       foreach ($forwards['out'] as $pf)
+                       {
+                               $class='trwarning';
+                               $name='';
+                               foreach ($addresses as $addr)
+                                       if ($addr['ip'] == $pf['localip'])
+                                       {
+                                               $class='';
+                                               $name=$addr['name'];
+                                               break;
+                                       }
+
+                               echo "<tr class='$class'>";
+
+                               echo "<td>${pf['proto']}/$name:<a href='${root}?page=ipaddress&tab=default&ip=${pf['localip']}'>${pf['localip']}</a>:${pf['localport']}</td>";
+
+                               echo "<td><a href='${root}?page=ipaddress&tab=default&ip=${pf['remoteip']}'>${pf['remoteip']}</a>:${pf['remoteport']}</td>";
+
+                               $address=getIPAddress($pf['remoteip']);
+
+                               echo "<td class='description'>";
+                               foreach($address['bonds'] as $bond)
+                                       echo "<a href='${root}?page=object&tab=default&object_id=${bond['object_id']}'>${bond['object_name']}(${bond['name']})</a> ";
+
+                               echo "</td><td class='description'>${pf['description']}</td>";
+
+                               echo "</tr>";
+                       }
+                       echo "</table><br><br>";
+               }
+               if (count($forwards['in']))
+               {
+                       echo "<h3>Forwarded from:</h3>";
+
+                       echo "<table class='widetable' cesspadding=5 cellspacing=0 border=0 align='center'>\n";
+                       echo "<tr><th>Source</th><th>Source objects</th><th>Target</th><th>Description</th></tr>\n";
+
+                       foreach ($forwards['in'] as $pf)
+                       {
+                               echo "<tr>";
+                               echo "<td>${pf['proto']}/<a href='${root}?page=ipaddress&tab=default&ip=${pf['localip']}'>${pf['localip']}</a>:${pf['localport']}</td>";
+
+                               echo "<td class='description'><a href='${root}?page=object&tab=default&object_id=${pf['object_id']}'>${pf['object_name']}</a>";
+
+                               echo "</td><td><a href='${root}?page=ipaddress&tab=default&ip=${pf['remoteip']}'>${pf['remoteip']}</a>:${pf['remoteport']}</td>";
+                               echo "<td class='description'>${pf['description']}</td></tr>";
+                       }
+
+                       echo "</table><br><br>";
+               }
+               finishPortlet();
+       }
+
+       echo "</td>\n";
+
+       // After left column we have (surprise!) right column with rackspace portled only.
+       echo "<td class=pcright>";
+       // rackspace portlet
+       startPortlet ('Rackspace allocation');
+       // FIXME: now we call getRackData() twice
+       $racks = getResidentRacksData ($object_id);
+       foreach ($racks as $rackData)
+               renderRack ($rackData['id'], $object_id);
+       echo '<br>';
+       finishPortlet();
+       echo "</td></tr>";
+       echo "</table>\n";
+}
+
+function renderRackMultiSelect ($sname, $racks, $selected)
+{
+       echo "<select name=${sname} multiple size=" . MAXSELSIZE . " onchange='getElementById(\"racks\").submit()'>\n";
+       foreach ($racks as $rack)
+       {
+               echo "<option value=${rack['id']}";
+               if (!(array_search ($rack['id'], $selected) === FALSE))
+                       echo ' selected';
+               echo">${rack['name']}</option>\n";
+       }
+       echo "</select>\n";
+}
+
+function showMessageOrError ()
+{
+       if (isset($_REQUEST['message']))
+               echo "<div class=msg_success>${_REQUEST['message']}</div>";
+       if (isset($_REQUEST['error']))
+               echo "<div class=msg_error>${_REQUEST['error']}</div>";
+}
+
+// This function renders a form for port edition.
+function renderPortsForObject ($object_id = 0)
+{
+       global $root, $pageno, $tabno;
+       if ($object_id <= 0)
+       {
+               showError ('Invalid object_id in renderPortsForObject()');
+               return;
+       }
+       showMessageOrError();
+       startPortlet ('Ports and interfaces');
+       $ports = getObjectPortsAndLinks ($object_id);
+       usort($ports, 'sortByName');
+       echo "<table cellspacing=0 cellpadding='5' align='center' class='widetable'>\n";
+       echo "<tr><th>&nbsp;</th><th>Local name</th><th>Visible label</th><th>Port type</th><th>L2 address</th>";
+       echo "<th>Rem. object</th><th>Rem. port</th><th>(Un)link or (un)reserve</th><th>&nbsp;</th></tr>\n";
+       foreach ($ports as $port)
+       {
+               echo "<form action='${root}process.php'>";
+               echo "<input type=hidden name=op value=editPort>";
+               echo "<input type=hidden name=page value='${pageno}'>\n";
+               echo "<input type=hidden name=tab value='${tabno}'>\n";
+               echo "<input type=hidden name=port_id value='${port['id']}'>";
+               echo "<input type=hidden name=object_id value='$object_id'>\n";
+               echo "<tr><td><a href='${root}process.php?op=delPort&page=${pageno}&tab=${tabno}&port_id=${port['id']}&object_id=$object_id&port_name=${port['name']}'>";
+               printImageHREF ('delete', 'Unlink and Delete this port');
+               echo "</a></td>\n";
+               echo "<td><input type=text name=name value='${port['name']}' size=8></td>";
+               echo "<td><input type=text name=label value='${port['label']}' size=24></td>";
+               echo "<td>${port['type']}</td>\n";
+               echo "<td><input type=text name=l2address value='${port['l2address']}'></td>\n";
+               if ($port['remote_object_id'])
+               {
+                       echo "<td><a href='${root}?page=object&object_id=${port['remote_object_id']}'>${port['remote_object_name']}</a></td>";
+                       echo "<td>${port['remote_name']}</td>";
+                       echo "<td><a href='${root}process.php?op=unlinkPort&page=${pageno}&tab=${tabno}&port_id=${port['id']}&object_id=$object_id&port_name=";
+                       echo urlencode ($port['name']);
+                       echo "&remote_port_name=${port['remote_name']}&remote_object_name=${port['remote_object_name']}'>";
+                       printImageHREF ('unlink', 'Unlink this port');
+                       echo "</a></td>";
+               }
+               elseif (!empty ($port['reservation_comment']))
+               {
+                       echo "<td><b>Reserved;</b></td>";
+                       echo "<td><input type=text name=reservation_comment value='${port['reservation_comment']}'></td>";
+                       echo "<td><a href='${root}process.php?op=useup&page=${pageno}&tab=${tabno}&port_id=${port['id']}&object_id=${object_id}'>";
+                       printImageHREF ('useup', 'Use up this port');
+                       echo "</a></td>";
+               }
+               else
+               {
+                       echo "<td>&nbsp;</td><td>&nbsp;</td>";
+                       echo "<td>";
+                       echo "<a href='javascript:;' onclick='window.open(\"${root}link_helper.php?port=${port['id']}&type=${port['type_id']}&object_id=$object_id&port_name=";
+                       echo urlencode ($port['name']);
+                       echo "\",\"findlink\",\"height=700, width=400, location=no, menubar=no, resizable=yes, scrollbars=no, status=no, titlebar=no, toolbar=no\");'>";
+                       printImageHREF ('link', 'Link this port');
+                       echo "</a> <input type=text name=reservation_comment>";
+                       echo "</td>\n";
+               }
+               echo "<td><input type='submit' value='OK'></td>";
+               echo "</form></tr>\n";
+       }
+       echo "<form action='${root}process.php'><tr>";
+       echo "<td colspan=2><input type=text size=10 name=port_name tabindex=100></td>\n";
+       echo "<td><input type=text size=24 name=port_label tabindex=101></td>";
+       echo "<input type=hidden name=op value=addPort>\n";
+       echo "<input type=hidden name=object_id value='${object_id}'>\n";
+       echo "<input type=hidden name=page value='${pageno}'>\n";
+       echo "<input type=hidden name=tab value='${tabno}'>\n";
+       echo "<td><select name='port_type_id' tabindex=102>\n";
+       $types = getPortTypes();
+       global $default_port_type;
+       foreach ($types as $typeid => $typename)
+       {
+               echo "<option value='${typeid}'";
+               if ($typeid == $default_port_type)
+                       echo " selected";
+               echo ">${typename}</option>\n";
+       }
+       echo "</select></td>";
+       echo "<td><input type=text name=port_l2address tabindex=103></td>\n";
+       echo "<td colspan=4><input type='submit' value='Add a new port' tabindex=104></td></tr></form>";
+       echo "</table><br>\n";
+       finishPortlet();
+
+       startPortlet ('Add/update multiple ports');
+       echo "<form action=${root}process.php method=post>";
+       echo "<input type=hidden name=page value='${pageno}'>\n";
+       echo "<input type=hidden name=tab value='${tabno}'>\n";
+       echo "<input type=hidden name=object_id value='${object_id}'>\n";
+       echo "<input type=hidden name=op value=addMultiPorts>";
+       echo 'Format: <select name=format>';
+       echo '<option value=c2900 disabled>Cisco 2900 series: sh int eth</option>';
+       echo '<option value=c3600eth disabled>Cisco 3600 ethernet: sh arp | inc -</option>';
+       echo '<option value=c3600asy>Cisco 3600 async: sh line | inc TTY</option>';
+       echo '<option value=fiwg selected>Foundry ServerIron/FastIron WorkGroup/Edge: sh int br</option>';
+       echo '<option value=fiedge disabled>Foundry FastIron Edge: sh int br</option>';
+       echo '<option value=fisxii>Foundry FastIron SuperX/II4000: sh int br</option>';
+       echo "</select>";
+       echo 'Default port type: ';
+       echo "<select name=port_type>\n";
+       foreach ($types as $typeid => $typename)
+       {
+               echo "<option value='${typeid}'";
+               if ($typeid == $default_port_type)
+                       echo " selected";
+               echo ">${typename}</option>\n";
+       }
+       echo "</select>";
+       echo "<input type=submit value='Parse output'><br>\n";
+       echo "<textarea name=input cols=100 rows=50></textarea><br>\n";
+       echo '</form>';
+       finishPortlet();
+}
+
+function renderNetworkForObject ($object_id=0)
+{
+       global $root, $pageno, $tabno;
+       if ($object_id <= 0)
+       {
+               showError ('Invalid object_id in renderNetworkForObject()');
+               return;
+       }
+       showMessageOrError();
+       startPortlet ('Network Addresses');
+       $addresses = getObjectAddresses ($object_id);
+       usort($addresses, 'sortAddresses');
+       echo "<table cellspacing=0 cellpadding='5' align='center' class='widetable'>\n";
+       echo "<tr><th>&nbsp;</th><th>Interface name</th><th>IP Address</th><th>Description</th><th>Type</th><th>Misc</th><th>&nbsp</th></tr>\n";
+       foreach ($addresses as $addr)
+       {
+               if (strlen($addr['address_name'])>40)
+                       $address_name = substr($addr['address_name'],0,38).'...';
+               else
+                       $address_name = $addr['address_name'];
+
+               $virtnum = countRefsOfType($addr['references'], 'virtual', 'eq');
+               $sharednum = countRefsOfType($addr['references'], 'shared', 'eq');
+               $regnum = countRefsOfType($addr['references'], 'regular', 'eq');
+               $notvirtnum = countRefsOfType($addr['references'], 'virtual', 'neq');
+
+               if ($addr['address_reserved']=='yes')
+                       $class='trwarning';
+               elseif ($addr['type']!='virtual' && $regnum>0)
+                       $class='trwarning';
+               elseif ($addr['type']=='regular' && $sharednum>0)
+                       $class='trwarning';
+               else 
+                       $class='';
+
+               echo "<form action='process.php'>";
+               echo "<input type=hidden name=page value='${pageno}'>\n";
+               echo "<input type=hidden name=tab value='${tabno}'>\n";
+               echo "<input type=hidden name=op value=editAddressFromObject>";
+               echo "<input type=hidden name=object_id value='$object_id'>";
+               echo "<input type=hidden name=ip value='${addr['ip']}'>";
+               echo "<tr class='$class'><td><a href='process.php?op=delAddrFObj&page=${pageno}&tab=${tabno}&ip=${addr['ip']}&object_id=$object_id'>";
+               printImageHREF ('delete', 'Delete this IPv4 address');
+               echo "</a></td>";
+               echo "<td><input type='text' name='bond_name' value='${addr['name']}' size=10></td>";
+               echo "<td><a href='${root}?page=ipaddress&ip=${addr['ip']}'>${addr['ip']}</a></td>";
+               echo "<td class='description'>$address_name</td>\n";
+               echo "<td><select name='bond_type'>";
+               foreach (array('regular'=>'Regular', 'virtual'=>'Virtual', 'shared'=>'Shared') as $n => $v)
+               {
+                       echo "<option value='$n'";
+                       if ($addr['type'] == $n)
+                               echo " selected";
+                       echo ">$v</option>";
+               }
+               echo "</td><td>";
+               if ($addr['address_reserved']=='yes')
+                       echo "<b>Reserved</b>; ";
+
+               if ($addr['type'] == 'virtual')
+               {
+                       if ($notvirtnum > 0)
+                       {
+                               echo " Owners: ";
+                               printRefsOfType($addr['references'], 'virtual', 'neq');
+                       }
+               }
+               elseif ($addr['type'] == 'shared')
+               {
+                       if ($sharednum > 0)
+                       {
+                               echo " Peers: ";
+                               printRefsOfType($addr['references'], 'shared', 'eq');
+                               echo ";";
+                       }
+                       if ($virtnum > 0)
+                       {
+                               echo " Virtuals: ";
+                               printRefsOfType($addr['references'], 'virtual', 'eq');
+                               echo ";";
+                       }
+                       if ($regnum > 0)
+                       {
+                               echo " Collisions: ";
+                               printRefsOfType($addr['references'], 'regular', 'eq');
+                       }
+                       
+               }
+               else
+               {
+                       if ($virtnum > 0)
+                       {
+                               echo " Virtuals: ";
+                               printRefsOfType($addr['references'], 'virtual', 'eq');
+                               echo ";";
+                       }
+                       if ($notvirtnum > 0)
+                       {
+                               echo " Collisions: ";
+                               printRefsOfType($addr['references'], 'virtual', 'neq');
+                       }
+               }
+
+               echo "</td><td><input type=submit value='OK'></td></form></tr>\n";
+       }
+
+
+       echo "<form action='${root}process.php'><tr><td colspan=2><input type='text' size='10' name='name' tabindex=100></td>\n";
+       echo "<input type=hidden name=page value='${pageno}'>\n";
+       echo "<input type=hidden name=tab value='${tabno}'>\n";
+       echo "<input type=hidden name=op value=addAddrFObj>\n";
+       echo "<input type=hidden name=object_id value='$object_id'>\n";
+
+       echo "<td><input type=text name='ip' tabindex=101>\n";
+       echo "</td><td><select name='type' tabindex=102>";
+       echo "<option value='regular'>Regular</option>";
+       echo "<option value='virtual'>Virtual</option>";
+       echo "<option value='shared'>Shared</option>";
+       echo "</select>";
+       echo "</td><td colspan=3><input type='submit' value='Add a new interface' tabindex=103></td></tr></form>";
+       echo "</table><br>\n";
+       finishPortlet();
+
+}
+
+function printLog ($log)
+{
+       foreach ($log as $record)
+               echo "<div class=msg_${record['code']}>${record['message']}</div>";
+}
+
+/*
+The following conditions must be followed:
+1. We can mount onto free atoms only. This means: if any record for an atom
+already exists in RackSpace, it can't be used for mounting.
+2. We can't unmount from 'W' atoms. Operator should review appropriate comments
+and either delete them before unmounting or refuse to unmount the object.
+*/
+
+// We extensively use $_REQUEST in the function.
+function renderRackSpaceForObject ($object_id = 0)
+{
+       if ($object_id <= 0)
+       {
+               showError ('Invalid object_id in renderRackSpaceForObject()');
+               return;
+       }
+       $is_submit = isset ($_REQUEST['got_atoms']);
+       $is_update = isset ($_REQUEST['rackmulti'][0]);
+       $info = getObjectInfo ($object_id);
+       if ($info == NULL)
+       {
+               showError ('getObjectInfo() failed in renderRackSpaceForObject()');
+               return;
+       }
+       // Always process occupied racks plus racks chosen by user. First get racks with
+       // already allocated rackspace...
+       $workingRacksData = getResidentRacksData ($object_id);
+       if ($workingRacksData === NULL)
+       {
+               print_r ($workingRacksData);
+               showError ('getResidentRacksData() failed in renderRackSpaceForObject()');
+               return;
+       }
+
+       // ...and then add those chosen by user (if any).
+       if ($is_update)
+               foreach ($_REQUEST['rackmulti'] as $cand_id)
+               {
+                       if (!isset ($workingRacksData[$cand_id]))
+                       {
+                               $rackData = getRackData ($cand_id);
+                               if ($rackData == NULL)
+                               {
+                                       showError ('getRackData() failed in renderRackSpaceForObject()');
+                                       return NULL;
+                               }
+                               $workingRacksData[$cand_id] = $rackData;
+                       }
+               }
+
+       // Do it only once...
+       foreach ($workingRacksData as &$rackData)
+               applyObjectMountMask ($rackData, $object_id);
+       // Now we workaround an old caveat: http://bugs.php.net/bug.php?id=37410
+       unset ($rackData);
+
+       // Here we process form submit by trying to save all submitted info to database.
+       if ($is_submit)
+       {
+               $oldMolecule = getMoleculeForObject ($object_id);
+               $worldchanged = FALSE;
+               $log = array();
+               foreach ($workingRacksData as $rack_id => $rackData)
+               {
+                       $logrecord = processGridForm ($rackData, 'F', 'T', $object_id);
+                       $log[] = $logrecord;
+                       if ($logrecord['code'] != 300)
+                       {
+                               $worldchanged = TRUE;
+                               // Reload our working copy after form processing.
+                               $rackData = getRackData ($rack_id);
+                               if ($rackData == NULL)
+                                       $log[] = array ('code' => 500, 'message' => 'Working copy update failed in renderRackSpaceForObject()');
+                               applyObjectMountMask ($rackData, $object_id);
+                               $workingRacksData[$rack_id] = $rackData;
+                       }
+               }
+               if ($worldchanged)
+               {
+                       // Log a record.
+                       $newMolecule = getMoleculeForObject ($object_id);
+                       $oc = count ($oldMolecule);
+                       $nc = count ($newMolecule);
+                       $omid = $oc ? createMolecule ($oldMolecule) : 'NULL';
+                       $nmid = $nc ? createMolecule ($newMolecule) : 'NULL';
+                       global $remote_username;
+                       $comment = empty ($_REQUEST['comment']) ? 'NULL' : "'${_REQUEST['comment']}'";
+                       $query =
+                               "insert into MountOperation(object_id, old_molecule_id, new_molecule_id, user_name, comment) " .
+                               "values (${object_id}, ${omid}, ${nmid}, '${remote_username}', ${comment})";
+                       global $dbxlink;
+                       $result = $dbxlink->query ($query);
+                       if ($result == NULL)
+                               $log[] = array ('code' => 'error', 'message' => 'SQL query failed during history logging.');
+                       else
+                               $log[] = array ('code' => 'success', 'message' => 'History logged.');
+               }
+               printLog ($log);
+       }
+
+       // This is the time for rendering.
+       global $root, $pageno, $tabno;
+       echo "<form id='racks' action='${root}'>";
+       echo "<input type=hidden name=page value='${pageno}'>\n";
+       echo "<input type=hidden name=tab value='${tabno}'>\n";
+       echo "<input type=hidden name=object_id value='${object_id}'>\n";
+       // Main layout starts.
+       echo "<table border=0 class=objectview cellspacing=0 cellpadding=0><tr>";
+
+       // Left portlet with rack list.
+       echo "<td class=pcleft height='1%'>";
+       startPortlet ('Racks');
+       $allRacksData = getRacksForRow();
+       renderRackMultiSelect ('rackmulti[]', $allRacksData, array_keys ($workingRacksData));
+       echo "<br>";
+       echo "<br>";
+       finishPortlet();
+       echo "</td>";
+
+       // Middle portlet with comment and submit.
+       echo "<td class=pcleft>";
+       startPortlet ('Comment');
+       echo "<textarea name=comment rows=10 cols=40></textarea><br>\n";
+       echo "<input type=submit value='Save' name=got_atoms>\n";
+       echo "<br>";
+       echo "<br>";
+       finishPortlet();
+       echo "</td>";
+
+       // Right portlet with rendered racks. If this form submit is not final, we have to
+       // reflect the former state of the grid in current form.
+       echo "<td class=pcright rowspan=2 height='1%'>";
+       startPortlet ('Working copy');
+       echo '<table border=0 cellspacing=10 align=center><tr>';
+       foreach ($workingRacksData as $rack_id => $rackData)
+       {
+               // Order is important here: only original allocation is highlighted.
+               highlightObject ($rackData, $object_id);
+               markupAtomGrid ($rackData, 'T');
+               // If we have a form processed, discard user input and show new database
+               // contents.
+               if (!$is_submit and $is_update)
+                       mergeGridFormToRack ($rackData);
+               echo "<td valign=top>";
+               echo "<center>\n<h2>${rackData['name']}</h2>\n";
+               echo "<table class=rack border=0 cellspacing=0 cellpadding=1>\n";
+               echo "<tr><th width='10%'>&nbsp;</th><th width='20%'>Front</th>";
+               echo "<th width='50%'>Interior</th><th width='20%'>Back</th></tr>\n";
+               renderAtomGrid ($rackData);
+               echo "</table></center>\n";
+               echo '</td>';
+       }
+       echo "</tr></table>";
+       finishPortlet();
+       echo "</td>\n";
+
+       echo "</form>\n";
+       echo "</tr></table>\n";
+}
+
+function renderMolecule ($mdata, $object_id)
+{
+       // sort data out
+       $rackpack = array();
+       global $loclist;
+       foreach ($mdata as $dummy => $rua)
+       {
+               $rack_id = $rua['rack_id'];
+               $unit_no = $rua['unit_no'];
+               $atom = $rua['atom'];
+               if (!isset ($rackpack[$rack_id]))
+               {
+                       $rackData = getRackData ($rack_id);
+                       for ($i = $rackData['height']; $i > 0; $i--)
+                               for ($locidx = 0; $locidx < 3; $locidx++)
+                                       $rackData[$i][$locidx]['state'] = 'F';
+                       $rackpack[$rack_id] = $rackData;
+               }
+               $rackpack[$rack_id][$unit_no][$loclist[$atom]]['state'] = 'T';
+               $rackpack[$rack_id][$unit_no][$loclist[$atom]]['object_id'] = $object_id;
+       }
+       // now we have some racks to render
+       foreach ($rackpack as $dummy => $rackData)
+       {
+               markAllSpans ($rackData);
+               echo "<table class=molecule cellspacing=0>\n";
+               echo "<caption>${rackData['name']}</caption>\n";
+               echo "<tr><th width='10%'>&nbsp;</th><th width='20%'>Front</th><th width='50%'>Interior</th><th width='20%'>Back</th></tr>\n";
+               for ($i = $rackData['height']; $i > 0; $i--)
+               {
+                       echo "<tr><th>$i</th>";
+                       for ($locidx = 0; $locidx < 3; $locidx++)
+                       {
+                               $state = $rackData[$i][$locidx]['state'];
+                               echo "<td class=state_${state}>&nbsp;</td>\n";
+                       }
+                       echo "</tr>\n";
+               }
+               echo "</table>\n";
+       }
+}
+
+function renderUnmountedObjectsPortlet ()
+{
+       startPortlet ('Unmounted objects');
+       $objs = getUnmountedObjects();
+       if ($objs === NULL)
+       {
+               showError ('getUnmountedObjects() failed in renderUnmountedObjectsPortlet()');
+               return;
+       }
+       global $root, $nextorder;
+       $order = 'odd';
+       echo '<br><br><table border=0 cellpadding=5 cellspacing=0 align=center class=cooltable>';
+       echo '<tr><th>Common name</th><th>Visible label</th><th>Asset number</th></tr>';
+       foreach ($objs as $obj)
+       {
+               echo "<tr class=row_${order}><td><a href='${root}?page=object&object_id=${obj['id']}'>${obj['dname']}</a></td>";
+               echo "<td>${obj['label']}</td>";
+               echo "<td>${obj['asset_no']}</td></tr>";
+               $order = $nextorder[$order];
+       }
+       echo "</table><br>\n";
+       finishPortlet();
+}
+
+function renderProblematicObjectsPortlet ()
+{
+       startPortlet ('Problematic objects');
+       $objs = getProblematicObjects();
+       if ($objs === NULL)
+       {
+               showError ('getProblematicObjects() failed in renderProblematicObjectsPortlet()');
+               return;
+       }
+       global $root, $nextorder;
+       $order = 'odd';
+       echo '<br><br><table border=0 cellpadding=5 cellspacing=0 align=center class=cooltable>';
+       echo '<tr><th>Type</th><th>Common name</th></tr>';
+       foreach ($objs as $obj)
+       {
+               echo "<tr class=row_${order}><td>${obj['objtype_name']}</td>";
+               echo "<td><a href='${root}?page=object&object_id=${obj['id']}'>${obj['dname']}</a></tr>";
+               $order = $nextorder[$order];
+       }
+       echo "</table><br>\n";
+       finishPortlet();
+}
+
+function renderObjectGroupSummary ()
+{
+       global $root;
+       $summary = getObjectGroupInfo();
+       if ($summary === NULL)
+       {
+               showError ('getObjectGroupInfo() failed in renderObjectGroupSummary()');
+               return;
+       }
+       echo "<table border=0 class=objectview>\n";
+       echo "<tr><td class=pcleft width='25%'>";
+
+       startPortlet ('Summary');
+       foreach ($summary as $gi)
+       {
+               echo "<a href='${root}?page=objgroup&group_id=${gi['id']}'><b>${gi['name']}</b></a> <i>(${gi['count']})</i><br>";
+       }
+       finishPortlet();
+
+       echo '</td><td class=pcright>';
+       renderUnmountedObjectsPortlet();
+       echo '</td><td class=pcright>';
+       renderProblematicObjectsPortlet();
+       echo "</td></tr></table>\n";
+}
+
+function renderObjectGroup ($group_id = 0)
+{
+       global $root;
+       $summary = getObjectGroupInfo();
+       if ($summary == NULL)
+       {
+               showError ('getObjectGroupInfo() failed in renderObjectGroup()');
+               return;
+       }
+       $objects = getObjectList ($group_id);
+       if ($objects === NULL)
+       {
+               showError ('getObjectList() failed in renderObjectGroup()');
+               return;
+       }
+       echo "<table border=0 class=objectview>\n";
+       echo "<tr><td class=pcleft width='25%'>";
+
+       startPortlet ('All objects');
+       foreach ($summary as $gi)
+       {
+               echo "<a href='${root}?page=objgroup&group_id=${gi['id']}'><b>${gi['name']}</b></a> <i>(${gi['count']})</i><br>";
+       }
+       finishPortlet();
+
+       echo '</td><td class=pcright>';
+
+       startPortlet ('Object group');
+       echo '<br><br><table border=0 cellpadding=5 cellspacing=0 align=center class=cooltable>';
+       echo '<tr><th>Common name</th><th>Visible label</th><th>Asset tag</th><th>Rack</th></tr>';
+       $order = 'odd';
+       global $nextorder;
+       foreach ($objects as $obj)
+       {
+               echo "<tr class=row_${order}><td><a href='${root}?page=object&object_id=${obj['id']}'>${obj['dname']}</a></td>";
+               echo "<td>${obj['label']}</td>";
+               echo "<td>${obj['asset_no']}</td>";
+               if ($obj['rack_id'])
+                       echo "<td><a href='${root}?page=rack&rack_id=${obj['rack_id']}'>${obj['Rack_name']}</a></td>";
+               else
+                       echo '<td>Unmounted</td>';
+               echo '</tr>';
+               $order = $nextorder[$order];
+       }
+       echo '</table>';
+       finishPortlet();
+
+       echo "</td></tr></table>";
+}
+
+function renderEmptyPortsSelect ($port_id, $type_id)
+{
+       $ports = getEmptyPortsOfType($type_id);
+       usort($ports, 'sortEmptyPorts');
+       foreach ($ports as $port)
+       {
+               if ($port_id == $port['Port_id'])
+                       continue;
+               echo "<option value='${port['Port_id']}' onclick='getElementById(\"remote_port_name\").value=\"${port['Port_name']}\"; getElementById(\"remote_object_name\").value=\"${port['Object_name']}\";'>${port['Object_name']} ${port['Port_name']}</option>\n";
+       }
+}
+
+function renderObjectAddressesAndNames ()
+{
+       $addresses = getObjectAddressesAndNames();
+       usort($addresses, 'sortObjectAddressesAndNames');
+       foreach ($addresses as $address)
+       {
+               echo "<option value='${address['ip']}' onclick='getElementById(\"ip\").value=\"${address['ip']}\";'>${address['object_name']} ${address['name']} ${address['ip']}</option>\n";
+       }
+}
+
+// History viewer for history-enabled simple dictionaries.
+function renderHistory ($object_type, $object_id)
+{
+       switch ($object_type)
+       {
+               case 'row':
+                       $query = "select ctime, user_name, name, deleted, comment from RackRowHistory where id = ${object_id} order by ctime";
+                       $header = '<tr><th>change time</th><th>author</th><th>rack row name</th><th>is deleted?</th><th>rack row comment</th></tr>';
+                       $extra = 4;
+                       break;
+               case 'rack':
+                       $query =
+                               "select ctime, user_name, rh.name, rh.deleted, d.dict_value as name, rh.height, rh.comment " .
+                               "from RackHistory as rh left join Dictionary as d on rh.row_id = d.dict_key " .
+                               "natural join Chapter " .
+                               "where chapter_name = 'RackRow' and rh.id = ${object_id} order by ctime";
+                       $header = '<tr><th>change time</th><th>author</th><th>rack name</th><th>is deleted?</th><th>rack row name</th><th>rack height</th><th>rack comment</th></tr>';
+                       $extra = 6;
+                       break;
+               case 'object':
+                       $query =
+                               "select ctime, user_name, name, label, barcode, asset_no, deleted, has_problems, dict_value, comment " .
+                               "from RackObjectHistory inner join Dictionary on objtype_id = dict_key natural join Chapter " .
+                               "where chapter_name = 'RackObjectType' and id=${object_id} order by ctime";
+                       $header = '<tr><th>change time</th><th>author</th><th>common name</th><th>visible label</th><th>barcode</th><th>asset no</th><th>is deleted?</th><th>has problems?</th><th>object type</th><th>comment</th></tr>';
+                       $extra = 9;
+                       break;
+               default:
+                       showError ("Uknown object type '${object_type}' in renderHistory()");
+                       return;
+       }
+       global $dbxlink;
+       $result = $dbxlink->query ($query);
+       if ($result == NULL)
+       {
+               showError ('SQL query failed in renderHistory()');
+               return;
+       }
+       echo '<table border=0 cellpadding=5 cellspacing=0 align=center class=cooltable>';
+       $order = 'odd';
+       global $nextorder;
+       echo $header;
+       while ($row = $result->fetch (PDO::FETCH_NUM))
+       {
+               echo "<tr class=row_${order}><td>${row[0]}</td>";
+               for ($i = 1; $i <= $extra; $i++)
+                       echo "<td>" . $row[$i] . "</td>";
+               echo "</tr>\n";
+               $order = $nextorder[$order];
+       }
+       echo "</table><br>\n";
+}
+
+function renderRackspaceHistory ()
+{
+       global $root, $nextorder, $pageno, $tabno;
+       $order = 'odd';
+       $history = getRackspaceHistory();
+       // Show the last operation by default.
+       if (isset ($_REQUEST['op_id']))
+               $op_id = $_REQUEST['op_id'];
+       elseif (isset ($history[0]['mo_id']))
+               $op_id = $history[0]['mo_id'];
+       else $op_id = NULL;
+       
+       $omid = NULL;
+       $nmid = NULL;
+       $object_id = 1;
+       if ($op_id)
+               list ($omid, $nmid) = getOperationMolecules ($op_id);
+
+       // Main layout starts.
+       echo "<table border=0 class=objectview cellspacing=0 cellpadding=0>";
+
+       // Left top portlet with old allocation.
+       echo "<tr><td class=pcleft>";
+       startPortlet ('Old allocation');
+       if ($omid)
+       {
+               $oldMolecule = getMolecule ($omid);
+               renderMolecule ($oldMolecule, $object_id);
+       }
+       else
+               echo "nothing";
+       finishPortlet();
+
+       echo '</td><td class=pcright>';
+
+       // Right top portlet with new allocation
+       startPortlet ('New allocation');
+       if ($nmid)
+       {
+               $newMolecule = getMolecule ($nmid);
+               renderMolecule ($newMolecule, $object_id);
+       }
+       else
+               echo "nothing";
+       finishPortlet();
+       
+       echo '</td></tr><tr><td colspan=2>';
+       
+       // Bottom portlet with list
+
+       startPortlet ('Rackspace allocation history');
+       echo "<table border=0 cellpadding=5 cellspacing=0 align=center class=cooltable>\n";
+       echo "<tr><th>timestamp</th><th>author</th><th>rack object ID</th><th>rack object type</th><th>rack object name</th><th>comment</th></tr>\n";
+       foreach ($history as $row)
+       {
+               if ($row['mo_id'] == $op_id)
+                       $class = 'hl';
+               else
+                       $class = "row_${order}";
+               echo "<tr class=${class}><td><a href='${root}?page=${pageno}&tab=${tabno}&op_id=${row['mo_id']}'>${row['ctime']}</a></td>";
+               echo "<td>${row['user_name']}</td>";
+               echo "<td>${row['ro_id']}</td><td>${row['objtype_name']}</td><td>${row['name']}</td><td>${row['comment']}</td>\n";
+               echo "</tr>\n";
+               $order = $nextorder[$order];
+       }
+       echo "</table>\n";
+       finishPortlet();
+       
+       echo '</td></tr></table>';
+       
+}
+
+function renderAddressspace ()
+{
+       global $root;
+       echo "<table class='widetable' border=0 cellpadding=10 cellspacing=0 align='center'>\n";
+       $addrspaceList = getAddressspaceList();
+       echo "<tr><th>Address range</th><th>Name</th><th>Total/used addresses</th></tr>";
+       foreach ($addrspaceList as $iprange)
+       {
+               echo "<tr><td><a href='${root}?page=iprange&id=${iprange['id']}'>${iprange['ip']}/${iprange['mask']}</a></td><td>${iprange['name']}</td><td>";
+               echo ($iprange['ip_bin'] | $iprange['mask_bin_inv']) - ($iprange['ip_bin'] & $iprange['mask_bin'])+1;
+               $range = getIPRange($iprange['id']);
+               echo "/";
+               echo count($range['addrlist']);
+               echo "</td></tr>";
+       }
+       echo "</table>\n";
+}
+
+function renderAddNewRange ()
+{
+       global $root, $pageno, $tabno;
+       showMessageOrError();
+       echo "<table class='widetable' border=0 cellpadding=10 cellpadding=0 align='center'>\n";
+       $addrspaceList = getAddressspaceList();
+       echo "<tr><th>&nbsp;</th><th>Address range</th><th>Name</th><th>Total/used addresses</th></tr>";
+       foreach ($addrspaceList as $iprange)
+       {
+               $range = getIPRange($iprange['id']);
+               $usedips = 0;
+               foreach ($range['addrlist'] as $addr)
+                       if (count($addr['references'])>0 || $addr['reserved']=='yes')
+                               $usedips++;
+               echo "<tr>";
+               if ($usedips == 0)
+               {
+                       echo "<td><a href='process.php?op=delRange&page=${pageno}&tab=${tabno}&id=${iprange['id']}'>";
+                       printImageHREF ('delete', 'Delete this IP range');
+                       echo "</a></td>\n";
+               }
+               else
+                       echo "<td>&nbsp</td>";
+               echo "<td><a href='${root}?page=iprange&id=${iprange['id']}'>${iprange['ip']}/${iprange['mask']}</a></td><td>${iprange['name']}</td><td>";
+               echo ($iprange['ip_bin'] | $iprange['mask_bin_inv']) - ($iprange['ip_bin'] & $iprange['mask_bin'])+1;
+               echo "/";
+               echo $usedips;
+               echo "</td></tr>";
+       }
+       echo "<form action='process.php'>";
+       echo "<input type=hidden name=op value=addRange>";
+       echo "<input type=hidden name=page value='${pageno}'>\n";
+       echo "<input type=hidden name=tab value='${tabno}'>\n";
+       echo "<tr><td colspan=4 class='tdcenter'><input type=text name=ip size=10>/<input type=text name=mask size=10>&nbsp;<input type=text name='name' size='20'>&nbsp;<input type=submit value='Add a new range'></td></tr>";
+       echo "</form></table>\n";
+}
+
+function renderIPRange ()
+{
+       global $root;
+       $maxperpage=256;
+       $id = $_REQUEST['id'];
+       if (isset($_REQUEST['pg']))
+               $page = $_REQUEST['pg'];
+       else
+               $page=0;
+
+       $paging=0;
+
+       $range = getIPRange($id);
+       echo "<center><h1>${range['ip']}/${range['mask']}</h1><h2>${range['name']}</h2></center>\n";
+
+
+       $startip = $range['ip_bin'] & $range['mask_bin'];
+       $endip = $range['ip_bin'] | $range['mask_bin_inv'];
+       $realstartip = $startip;
+       $realendip = $endip;
+       $numpages = 0;
+       if($endip - $startip > $maxperpage)
+       {
+               $paging=1;
+               $numpages = ($endip - $startip)/$maxperpage;
+               $startip = $startip + $page * $maxperpage;
+               $endip = $startip + $maxperpage-1;
+       }
+       echo "<center>";
+       for ($i=0; $i<$numpages; $i++)
+       {
+               if ($i == $page)
+                       echo "<b>$i</b> ";
+               else
+                       echo "<a href='${root}?page=iprange&id=$id&pg=$i'>$i</a> ";
+       }
+       echo "</center>";
+
+       echo "<table class='widetable' border=0 cellspacing=0 cellpadding=5 align='center'>\n";
+       echo "<tr><th>Address</th><th>Name</th><th>Allocation</th></tr>\n";
+
+
+       for($ip = $startip; $ip<=$endip; $ip++)
+       {
+               if ( (isset($range['addrlist'][$ip])) && ($range['addrlist'][$ip]['ip_bin'] == $ip) )
+               {
+                       $numshared = countRefsOfType($range['addrlist'][$ip]['references'], 'shared', 'eq');
+                       $numreg = countRefsOfType($range['addrlist'][$ip]['references'], 'regular', 'eq');
+                       $numvirt = countRefsOfType($range['addrlist'][$ip]['references'], 'virtual', 'eq');
+                       
+                       $addr = $range['addrlist'][$ip];
+                       if ( ($numshared > 0 && $numreg > 0) || $numreg > 1 )
+                               echo "<tr class='trwarning'>";
+                       elseif ( $addr['reserved'] == 'yes' and $numshared+$numreg+$numvirt > 0)
+                               echo "<tr class='trwarning'>";
+                       elseif ( $addr['reserved'] == 'yes')
+                               echo "<tr class='trbusy'>";
+                       elseif ( $numshared > 0 || $numreg > 0)
+                               echo "<tr class='trbusy'>";
+                       else
+                               echo "<tr>";
+
+                       echo "<td><a href='${root}?page=ipaddress&ip=${addr['ip']}'>${addr['ip']}</a></td><td>${addr['name']}</td><td>";
+                       if ( $addr['reserved'] == 'yes')
+                               echo "<b>Reserved;</b> ";
+                       foreach ($range['addrlist'][$ip]['references'] as $ref)
+                       {
+                               echo "<a href='${root}?page=object&object_id=${ref['object_id']}'>${ref['object_name']}[${ref['name']}]</a>; ";
+                       }
+                       echo "</td></tr>\n";
+               }
+               else
+               {
+                       echo "<tr><td><a href='${root}?page=ipaddress&ip=".long2ip($ip)."'>".long2ip($ip)."</a></td><td>&nbsp;</td><td>&nbsp;</td></tr>\n";
+               }
+       }
+
+       echo "</table>";
+       
+}
+
+function renderIPRangeProperties ()
+{
+       global $pageno, $tabno;
+       $id = $_REQUEST['id'];
+       showMessageOrError();
+       $range = getIPRange($id);
+       echo "<center><h1>${range['ip']}/${range['mask']}</h1></center>\n";
+       echo "<table border=0 cellpadding=10 cellpadding=1 align='center'>\n";
+       echo "<form action='process.php'><input type=hidden name=op value=editRange>";
+       echo "<input type=hidden name=page value='${pageno}'>\n";
+       echo "<input type=hidden name=tab value='${tabno}'>\n";
+       echo "<input type=hidden name=id value='${id}'>";
+       echo "<tr><td class='tdright'>Name:</td><td class='tdleft'><input type=text name=name size=20 value='${range['name']}'></tr><tr><td colspan=2 class='tdcenter'><input type=submit value='Update range'></td></form></tr>";
+       echo "</table>\n";
+
+}
+
+function renderIPAddress ()
+{
+       global $root;
+       $ip = $_REQUEST['ip'];
+       $address = getIPAddress($ip);
+       echo "<center><h1>$ip</h1>";
+       if ($address['exists'] == 1)
+               echo "<h2>${address['name']}</h2>";
+       echo "</center>\n";
+
+//     echo "<table width='100%' cesspadding=5 cellspacing=0 border=0 align='center'>";
+//     echo "<tr valign='top'><td>";
+
+       startPortlet ('Address assignment');
+       echo "<table class='widetable' cesspadding=5 cellspacing=0 border=0 align='center'>\n";
+       echo "<tr><th>Object name</th><th>Interface name</th><th>Interface type</th></tr>\n";
+
+       $numshared = countRefsOfType($address['bonds'], 'shared', 'eq');
+       $numreg = countRefsOfType($address['bonds'], 'regular', 'eq');
+       $numvirt = countRefsOfType($address['bonds'], 'virtual', 'eq');
+
+       
+       if ( ($numshared > 0 && $numreg > 0) || $numreg > 1 )
+               $class='trwarning';
+       elseif ( $address['reserved'] == 'yes' and $numshared+$numreg+$numvirt > 0)
+               $class='trwarning';
+       else
+               $class='';
+
+
+
+       if ($address['reserved'] == 'yes')
+               echo "<tr class='$class'><td colspan='3'><b>RESERVED</b></td></tr>";
+       foreach ($address['bonds'] as $bond)
+       {
+               echo "<tr class='$class'><td><a href='${root}?page=object&object_id=${bond['object_id']}'>${bond['object_name']}</td><td>${bond['name']}</td><td><b>";
+               switch ($bond['type'])
+               {
+                       case 'virtual':
+                               echo "Virtual";
+                               break;
+                       case 'shared':
+                               echo "Shared";
+                               break;
+                       case 'regular':
+                               echo "Regular";
+                               break;
+               }
+               echo "</b></td></tr>\n";
+       }
+       echo "</table><br><br>";
+       finishPortlet();
+
+//     echo "</td><td>";
+//     echo "</td></tr></table>";
+}
+
+function renderIPAddressProperties ()
+{
+       global $pageno, $tabno;
+       $ip = $_REQUEST['ip'];
+       showMessageOrError();
+       $address = getIPAddress($ip);
+       echo "<center><h1>$ip</h1></center>\n";
+       echo "<table border=0 cellpadding=10 cellpadding=1 align='center'>\n";
+       echo "<form action='process.php'><input type=hidden name=op value=editAddress>";
+       echo "<input type=hidden name=page value='${pageno}'>\n";
+       echo "<input type=hidden name=tab value='${tabno}'>\n";
+       echo "<input type=hidden name=ip value='${ip}'>";
+       echo "<tr><td class='tdright'>Name:</td><td class='tdleft'><input type=text name=name size=20 value='".($address['exists']==1?$address['name']:'')."'></tr>";
+       echo "<td class='tdright'>Reserved:</td><td class='tdleft'><input type=checkbox name=reserved size=20 ".($address['exists']==1?(($address['reserved']=='yes')?'checked':''):'')."></tr>";
+       echo "<tr><td colspan=2 class='tdcenter'><input type=submit value='Update address'></td></form></tr>";
+       echo "</table>\n";
+
+}
+
+function renderIPAddressAssignment ()
+{
+       global $pageno, $tabno, $root;
+       $ip = $_REQUEST['ip'];
+       $address = getIPAddress($ip);
+
+       showMessageOrError();
+       echo "<center><h1>$ip</h1></center>\n";
+
+
+       echo "<table class='widetable' cesspadding=5 cellspacing=0 border=0 align='center'>\n";
+       echo "<tr><th>&nbsp;</th><th>Object name</th><th>Interface name</th><th>Interface type</th><th>&nbsp;</th></tr>\n";
+
+       $numshared = countRefsOfType($address['bonds'], 'shared', 'eq');
+       $numreg = countRefsOfType($address['bonds'], 'regular', 'eq');
+       $numvirt = countRefsOfType($address['bonds'], 'virtual', 'eq');
+
+       
+       if ( ($numshared > 0 && $numreg > 0) || $numreg > 1 )
+               $class='trwarning';
+       elseif ( $address['reserved'] == 'yes' and $numshared+$numreg+$numvirt > 0)
+               $class='trwarning';
+       else
+               $class='';
+
+
+
+       if ($address['reserved'] == 'yes')
+               echo "<tr class='$class'><td colspan='5'><b>RESERVED</b></td></tr>";
+       foreach ($address['bonds'] as $bond)
+       {
+               echo "<tr class='$class'><form action='process.php'>";
+               echo "<input type=hidden name=op value='editBondForAddress'>";
+               echo "<input type=hidden name=page value='${pageno}'>";
+               echo "<input type=hidden name=tab value='${tabno}'>";
+               echo "<input type=hidden name=ip value='$ip'>";
+               echo "<input type=hidden name=object_id value='${bond['object_id']}'>";
+               echo "<td><a href='process.php?op=delIpAssignment&page=${pageno}&tab=${tabno}&ip=$ip&object_id=${bond['object_id']}'>";
+               printImageHREF ('delete', 'Unallocate address');
+               echo "</a></td>";
+               echo "<td><a href='${root}?page=object&object_id=${bond['object_id']}'>${bond['object_name']}</td>";
+               echo "<td><input type='text' name='bond_name' value='${bond['name']}' size=10></td>";
+               echo "<td><select name='bond_type'>";
+               switch ($bond['type'])
+               {
+                       case 'virtual':
+                               echo "<option value='regular'>Regular</option>";
+                               echo "<option value='virtual' selected>Virtual</option>";
+                               echo "<option value='shared'>Shared</option>";
+                               break;
+                       case 'shared':
+                               echo "<option value='regular'>Regular</option>";
+                               echo "<option value='virtual'>Virtual</option>";
+                               echo "<option value='shared' selected>Shared</option>";
+                               break;
+                       case 'regular':
+                               echo "<option value='regular' selected>Regular</option>";
+                               echo "<option value='virtual'>Virtual</option>";
+                               echo "<option value='shared'>Shared</option>";
+                               break;
+               }
+               echo "</select></td><td><input type='submit' value='OK'></td></form></tr>\n";
+       }
+       echo "<form action='process.php'><input type='hidden' name='op' value='bindObjectToIp'>";
+       echo "<input type=hidden name=page value='${pageno}'>\n";
+       echo "<input type=hidden name=tab value='${tabno}'>\n";
+       echo "<input type='hidden' name='ip' value='$ip'>";
+       echo "<td colspan=2><select name='object_id'>";
+
+       foreach (array(1, 4, 7, 8, 12, 14) as $type) 
+       {
+               //get all Balck Boxes, Servers, Routers, Switches, UPS, Modems
+               $objects = getObjectList($type);
+               foreach ($objects as $object)
+                       echo "<option value='${object['id']}'>${object['dname']}</option>";
+       }
+
+       echo "</select></td><td><input type='text' name='bond_name' value='' size=10></td>";
+       echo "<td><select name='bond_type'><option value='regular'>Regular</option><option value='virtual'>Virtual</option><option value='shared'>Shared</option></select></td>";
+       echo "<td><input type='submit' value='Assign address'></td></form></tr>";
+       echo "</table><br><br>";
+
+}
+
+function renderIPAddressPortForwarding ($object_id=0)
+{
+       global $pageno, $tabno, $root;
+       
+       $info = getObjectInfo ($object_id);
+       $forwards = getObjectForwards ($object_id);
+       $addresses = getObjectAddresses ($object_id);
+       showMessageOrError();
+       echo "<center><h1>Port Forwardings</h1></center>\n";
+       echo "<center><h3>Forwarding out:</h3></center>";
+
+       echo "<table class='widetable' cesspadding=5 cellspacing=0 border=0 align='center'>\n";
+       echo "<tr><th></th><th>Source</th><th>Target</th><th>Target Objects</th><th>Description</th></tr>\n";
+
+       foreach ($forwards['out'] as $pf)
+       {
+               $class='trwarning';
+               $name='';
+               foreach ($addresses as $addr)
+                       if ($addr['ip'] == $pf['localip'])
+                       {
+                               $class='';
+                               $name = $addr['name'];
+                               break;
+                       }
+
+               echo "<tr class='$class'>";
+               echo "<td><a href='process.php?op=delPortForwarding&localip=${pf['localip']}&localport=${pf['localport']}&remoteip=${pf['remoteip']}&remoteport=${pf['remoteport']}&proto=${pf['proto_bin']}&object_id=$object_id&page=${pageno}&tab=${tabno}'><img src='${root}/pix/delete_s.gif' title='Delete port forwarding' border=0 width=16 height=16></a></td>";
+               echo "<td>${pf['proto']}/$name:<a href='${root}?page=ipaddress&tab=default&ip=${pf['localip']}'>${pf['localip']}</a>:${pf['localport']}</td>";
+               echo "<td><a href='${root}?page=ipaddress&tab=default&ip=${pf['remoteip']}'>${pf['remoteip']}</a>:${pf['remoteport']}</td>";
+
+               $address=getIPAddress($pf['remoteip']);
+
+               echo "<td class='description'>";
+               foreach($address['bonds'] as $bond)
+                       echo "<a href='${root}?page=object&tab=default&object_id=${bond['object_id']}'>${bond['object_name']}(${bond['name']})</a> ";
+               echo "</td><form action='process.php'><input type='hidden' name='op' value='updPortForwarding'><input type=hidden name=page value='${pageno}'><input type=hidden name=tab value='${tabno}'><input type='hidden' name='object_id' value='$object_id'><input type='hidden' name='localip' value='${pf['localip']}'><input type='hidden' name='localport' value='${pf['localport']}'><input type='hidden' name='remoteip' value='${pf['remoteip']}'><input type='hidden' name='remoteport' value='${pf['remoteport']}'><input type='hidden' name='proto' value='${pf['proto_bin']}'><td class='description'><input type='text' name='description' value='${pf['description']}'> <input type='submit' value='OK'></td></form>";
+               echo "</tr>";
+       }
+       echo "<form action='process.php'><input type='hidden' name='op' value='forwardPorts'>";
+       echo "<input type='hidden' name='object_id' value='$object_id'>";
+       echo "<input type=hidden name=page value='${pageno}'>\n";
+       echo "<input type=hidden name=tab value='${tabno}'>\n";
+       echo "<tr align='center'><td colspan=2><select name='proto'><option value='1'>TCP</option><option value='2'>UDP</option></select><select name='localip'>";
+
+       foreach ($addresses as $addr)
+               echo "<option value='${addr['ip']}'>" . (empty ($addr['name']) ? '' : "${addr['name']}:") . "${addr['ip']}</option>";
+
+       echo "</select>:<input type='text' name='localport' size='4'></td><td><input type='text' name='remoteip' id='remoteip' size='10'>";
+       echo "<a href='javascript:;' onclick='window.open(\"${root}/find_object_ip_helper.php\", \"findobjectip\", \"height=700, width=400, location=no, menubar=no, resizable=yes, scrollbars=no, status=no, titlebar=no, toolbar=no\");'><img src='${root}/pix/find.png' title='Find object' border=0 height=16 width=16></a>";
+       echo ":<input type='text' name='remoteport' size='4'></td><td></td><td colspan=1><input type='text' name='description' size='20'> <input type='submit' value='Create Forwarding'></td></tr>";
+       echo "</form>";
+
+       echo "</table><br><br>";
+
+
+       echo "<center><h3>Forwarded from:</h3></center>";
+       echo "<table class='widetable' cesspadding=5 cellspacing=0 border=0 align='center'>\n";
+       echo "<tr><th></th><th>Source</th><th>Source objects</th><th>Target</th><th>Description</th></tr>\n";
+
+       foreach ($forwards['in'] as $pf)
+       {
+               echo "<tr>";
+
+               echo "<td><a href='process.php?op=delPortForwarding&localip=${pf['localip']}&localport=${pf['localport']}&remoteip=${pf['remoteip']}&remoteport=${pf['remoteport']}&proto=${pf['proto_bin']}&object_id=${pf['object_id']}&page=${pageno}&tab=${tabno}'><img src='${root}/pix/delete_s.gif' title='Delete port forwarding' border=0 width=16 height=16></a></td>";
+               echo "<td>${pf['proto']}/<a href='${root}?page=ipaddress&tab=default&ip=${pf['localip']}'>${pf['localip']}</a>:${pf['localport']}</td>";
+               echo "<td class='description'><a href='${root}?page=object&tab=default&object_id=${pf['object_id']}'>${pf['object_name']}</a>";
+               echo "</td><td><a href='${root}?page=ipaddress&tab=default&ip=${pf['remoteip']}'>${pf['remoteip']}</a>:${pf['remoteport']}</td>";
+               echo "<td class='description'>${pf['description']}</td></tr>";
+       }
+
+//     echo "<form action='process.php'><input type='hidden' name='op' value='forwardPorts'>";
+//     echo "<input type='hidden' name='object_id' value='$object_id'>";
+//     echo "<input type=hidden name=page value='${pageno}'>\n";
+//     echo "<input type=hidden name=tab value='${tabno}'>\n";
+//     echo "<tr align='center'><td colspan=2><select name='proto'><option value='1'>TCP</option><option value='2'>UDP</option><input type='text' name='localip' size='10'>:<input type='text' name='localport' size='4'></td><td><select name='localip'>";
+//     foreach ($addresses as $addr)
+//             echo "<option value='${addr['ip']}'>${addr['ip']}</option>";
+//
+//     echo "</select>:<input type='text' name='remoteport' size='4'></td><td><input type='text' name='description' size='20'></td><td><input type='submit' value='Create Forwarding'></td></tr>";
+//     echo "</form>";
+       echo "</table><br><br>";
+
+
+}
+
+
+function renderAddMultipleObjectsForm ()
+{
+       global $pageno, $tabno, $nextorder;
+
+       $type_id = array();
+       $name = array();
+       $asset_no = array();
+       $keepvalues = FALSE;
+       // Look for current submit.
+       if (isset ($_REQUEST['got_data']))
+       {
+               $keepvalues = TRUE;
+               $log = array();
+               for ($i=0; $i < MASSCOUNT; $i++)
+               {
+                       if (!isset ($_REQUEST["${i}_object_type_id"]))
+                       {
+                               $log[] = array ('code' => 'error', 'message' => "Submitted form is invalid at line " . $i + 1);
+                               break;
+                       }
+                       assertUIntArg ("${i}_object_type_id", TRUE);
+                       assertStringArg ("${i}_object_name", TRUE);
+                       assertStringArg ("${i}_object_label", TRUE);
+                       assertStringArg ("${i}_object_asset_no", TRUE);
+                       assertStringArg ("${i}_object_barcode", TRUE);
+                       $type_id[$i] = $_REQUEST["${i}_object_type_id"];
+                       // Save user input for possible rendering.
+                       $name[$i] = $_REQUEST["${i}_object_name"];
+                       $label[$i] = $_REQUEST["${i}_object_label"];
+                       $asset_no[$i] = $_REQUEST["${i}_object_asset_no"];
+                       $barcode[$i] = $_REQUEST["${i}_object_barcode"];
+
+                       // It's better to skip silently than printing a notice.
+                       if ($type_id[$i] == 0)
+                               continue;
+                       if (commitAddObject ($name[$i], $label[$i], $barcode[$i], $type_id[$i], $asset_no[$i]) === TRUE)
+                               $log[] = array ('code' => 'success', 'message' => "Added new object '${name[$i]}'");
+                       else
+                               $log[] = array ('code' => 'error', 'message' => 'commitAddObject() failed in renderAddMultipleObjectsForm()');
+               }
+               printLog ($log);
+       }
+
+       // Render a form for the next.
+       $typelist = getObjectTypeList();
+       $typelist[0]['dict_key'] = 0;
+       $typelist[0]['dict_value'] = 'SKIP';
+       $order = 'odd';
+       startPortlet ('Add multiple objects');
+       echo '<form>';
+       echo "<input type=hidden name=page value=${pageno}>";
+       echo "<input type=hidden name=tab value=${tabno}>";
+       echo '<table border=0 align=center>';
+       echo "<tr><th>Object type</th><th>Common name</th><th>Visible label</th><th>Asset tag</th><th>Barcode</th></tr>\n";
+       // If a user forgot to select object type on input (it is 'SKIP'), we keep his
+       // previous input in the form.
+       $order = 'odd';
+       for ($i = 0; $i < MASSCOUNT; $i++)
+       {
+               echo '<tr class=row_{order}>';
+               echo '<td>';
+               printSelect ($typelist, "${i}_object_type_id", 0);
+               echo '</td>';
+               echo "<td><input type=text size=30 name=${i}_object_name";
+               if ($keepvalues and $type_id[$i] == 0)
+                       echo " value='${name[$i]}'";
+               echo "></td>";
+               echo "<td><input type=text size=30 name=${i}_object_label";
+               if ($keepvalues and $type_id[$i] == 0)
+                       echo " value='${label[$i]}'";
+               echo "></td>";
+               echo "<td><input type=text size=20 name=${i}_object_asset_no";
+               if ($keepvalues and $type_id[$i] == 0)
+                       echo " value='${asset_no[$i]}'";
+               echo "></td>";
+               echo "<td><input type=text size=10 name=${i}_object_barcode";
+               if ($keepvalues and $type_id[$i] == 0)
+                       echo " value='${barcode[$i]}'";
+               echo "></td>";
+               echo "</tr>\n";
+               $order = $nextorder[$order];
+       }
+       echo "<tr><td class=submit colspan=5><input type=submit name=got_data value='Create'></td></tr>\n";
+       echo "</form></table>\n";
+       finishPortlet();
+}
+
+function printGreeting ()
+{
+       global $remote_username, $accounts;
+       $account = $accounts[$remote_username];
+       echo "Hello, ${account['user_realname']}. This is RackTables " . VERSION;
+}
+
+function renderSearchResults ()
+{
+       global $remote_username, $root;
+       $terms = trim ($_REQUEST['q']);
+       if (empty ($terms))
+       {
+               showError ('Search string cannot be empty.');
+               return;
+       }
+       if (!authorized ($remote_username, 'object', 'default'))
+       {
+               showError ('You are not authorized for viewing information about objects.');
+               return;
+       }
+       // If we search for L2 address, we can either find one or find none.
+       if
+       (
+               preg_match ('/^[0-9a-f][0-9a-f]?:[0-9a-f][0-9a-f]?:[0-9a-f][0-9a-f]?:[0-9a-f][0-9a-f]?:[0-9a-f][0-9a-f]?:[0-9a-f][0-9a-f]?$/i', $terms) or
+               preg_match ('/^[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]$/i', $terms) or
+               preg_match ('/^[0-9a-f][0-9a-f][0-9a-f][0-9a-f].[0-9a-f][0-9a-f][0-9a-f][0-9a-f].[0-9a-f][0-9a-f][0-9a-f][0-9a-f]$/i', $terms)
+       )
+       // Search for L2 address.
+       {
+               $result = searchByl2address ($terms);
+               if ($result !== NULL)
+               {
+                       echo "<script language='Javascript'>document.location='${root}?page=object";
+                       echo "&hl_port_id=${result['port_id']}";
+                       echo "&object_id=${result['object_id']}';//</script>";
+               }
+               else
+                       echo "L2 address '${terms}' not found!";
+       }
+       elseif (preg_match ('/^[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]$/i', $terms))
+       // STP bridge ID: bridge priotity + port MAC address. Cut off first 4 chars and look for MAC address.
+       {
+               $terms = substr ($terms, 4);
+               $result = searchByl2address ($terms);
+               if ($result !== NULL)
+               {
+                       echo "<script language='Javascript'>document.location='${root}?page=object";
+                       echo "&hl_port_id=${result['port_id']}";
+                       echo "&object_id=${result['object_id']}';//</script>";
+               }
+               else
+                       echo "L2 address '${terms}' not found!";
+       }
+       elseif (preg_match ('/^[0-9][0-9]?[0-9]?\.[0-9]?[0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9]?[0-9]?[0-9]?$/i', $terms))
+       // Search for IP address.
+       {
+               $result = getRangeByIp ($terms);
+               if ($result !== NULL)
+               {
+                       echo "<script language='Javascript'>document.location='${root}?page=ipaddress";
+                       echo "&ip=${terms}";
+                       echo "';//</script>";
+               }
+               else
+                       echo "IP address '${terms}' not found!";
+               return;
+       }
+       else
+       // Search for objects.
+       {
+               $objects = getSearchResults ($terms);
+               if (count ($objects) == 1)
+               {
+                       $obj = current ($objects);
+                       echo "<script language='Javascript'>document.location='${root}?page=object&object_id=${obj['id']}';//</script>";
+               }
+               elseif (count ($objects) > 1)
+               {
+                       echo '<br><br><table border=0 cellpadding=5 cellspacing=0 align=center class=cooltable>';
+                       echo '<tr><th>Common name</th><th>Visible label</th><th>Asset tag</th><th>barcode</th></tr>';
+                       $order = 'odd';
+                       global $nextorder;
+                       foreach ($objects as $obj)
+                       {
+                               echo "<tr class=row_${order}><td><a href=\"${root}?page=object&object_id=${obj['id']}\">${obj['dname']}</a></td>";
+                               echo "<td>${obj['label']}</td>";
+                               echo "<td>${obj['asset_no']}</td>";
+                               echo "<td>${obj['barcode']}</td></tr>";
+                               $order = $nextorder[$order];
+                       }
+                       echo '</table>';
+               }
+               else
+                       echo "Object '${terms}' not found!";
+       }
+}
+
+// This function prints a table of checkboxes to aid the user in toggling mount atoms
+// from one state to another. The first argument is rack data as
+// produced by getRackData(), the second is the value used for the 'unckecked' state
+// and the third is the value used for 'checked' state.
+// Usage contexts:
+// for mounting an object:             printAtomGrid ($data, 'F', 'T')
+// for changing rack design:           printAtomGrid ($data, 'A', 'F')
+// for adding rack problem:            printAtomGrid ($data, 'F', 'U')
+// for adding object problem:          printAtomGrid ($data, 'T', 'W')
+
+function renderAtomGrid ($data)
+{
+       $rack_id = $data['id'];
+       for ($unit_no = $data['height']; $unit_no > 0; $unit_no--)
+       {
+               echo "<tr><th>${unit_no}</th>";
+               for ($locidx = 0; $locidx < 3; $locidx++)
+               {
+                       $state = $data[$unit_no][$locidx]['state'];
+                       echo "<td class=state_${state}";
+                       if (isset ($data[$unit_no][$locidx]['hl']))
+                               echo $data[$unit_no][$locidx]['hl'];
+                       echo ">";
+                       if (!($data[$unit_no][$locidx]['enabled'] === TRUE))
+                               echo '<input type=checkbox disabled>';
+                       else
+                               echo "<input type=checkbox" . $data[$unit_no][$locidx]['checked'] . " name=atom_${rack_id}_${unit_no}_${locidx}>";
+                       echo '</td>';
+               }
+               echo "</tr>\n";
+       }
+}
+
+function renderPermissions ()
+{
+       startPortlet ('User permissions');
+       echo "<table class=cooltable border=0 cellpadding=5 cellspacing=0 align=center>\n";
+       echo "<tr><th>Username</th><th>Page</th><th>Tab</th><th>Access</th></tr>";
+       global $perms, $nextorder;
+       $order = 'odd';
+       foreach ($perms as $username => $pages)
+               foreach ($pages as $page => $tabs)
+                       foreach ($tabs as $tab => $access)
+                       {
+                               echo "<tr class=row_${order}><td class=tdleft>$username</td><td>$page</td><td>$tab</td><td>$access</td></tr>\n";
+                               $order = $nextorder[$order];
+                       }
+       echo "</table>\n";
+       finishPortlet();
+}
+
+function renderAccounts ()
+{
+       global $nextorder, $accounts;
+       startPortlet ('User accounts');
+       echo "<table class=cooltable border=0 cellpadding=5 cellspacing=0 align=center>\n";
+       echo "<tr><th class=tdleft>Username</th><th class=tdleft>Real name</th></tr>";
+       $order = 'odd';
+       foreach ($accounts as $user)
+       {
+               echo "<tr class=row_${order}><td class=tdleft>${user['user_name']}</td><td class=tdleft>${user['user_realname']}</td></li>";
+               $order = $nextorder[$order];
+       }
+       echo '</table>';
+       finishPortlet();
+}
+
+function renderAccountsEditForm ()
+{
+       global $root, $pageno, $tabno, $accounts;
+       startPortlet ('User accounts');
+       showMessageOrError();
+       echo "<table cellspacing=0 cellpadding=5 align=center class=widetable>\n";
+       echo "<tr><th>op</th><th>Username</th><th>Real name</th><th>Password</th><th>&nbsp;</th></tr>\n";
+       foreach ($accounts as $account)
+       {
+               echo "<form action='${root}process.php'>";
+               echo "<input type=hidden name=op value=updateAccount>";
+               echo "<input type=hidden name=page value='${pageno}'>\n";
+               echo "<input type=hidden name=tab value='${tabno}'>\n";
+               echo "<input type=hidden name=id value='${account['user_id']}'><tr>";
+               echo "<td>";
+               if ($account['user_enabled'] == 'yes' && $account['user_id'] != 1)
+               {
+                       echo "<a href='${root}process.php?op=disableAccount&page=${pageno}&tab=${tabno}&id=${account['user_id']}'>";
+                       printImageHREF ('blockuser', 'disable account');
+                       echo "</a>\n";
+               }
+               if ($account['user_enabled'] == 'no' && $account['user_id'] != 1)
+               {
+                       echo "<a href='${root}process.php?op=enableAccount&page=${pageno}&tab=${tabno}&id=${account['user_id']}'>";
+                       printImageHREF ('unblockuser', 'enable account');
+                       echo "</a>\n";
+               }
+               // Otherwise skip icon.
+               echo "</td>";
+               echo "<td><input type=text name=username value='${account['user_name']}' size=16></td>";
+               echo "<td><input type=text name=realname value='${account['user_realname']}' size=24></td>";
+               echo "<td><input type=password name=password value='${account['user_password_hash']}' size=64></td>";
+               echo "<td><input type='submit' value='OK'></td>";
+               echo "</form></tr>\n";
+       }
+       echo "<form action='${root}process.php' method=post><tr>";
+       echo "<input type=hidden name=op value=createAccount>\n";
+       echo "<input type=hidden name=page value='${pageno}'>\n";
+       echo "<input type=hidden name=tab value='${tabno}'>\n";
+       echo "<td colspan=2><input type=text size=16 name=username tabindex=100></td>\n";
+       echo "<td><input type=text size=24 name=realname tabindex=101></td>";
+       echo "<td><input type=password size=64 name=password tabindex=102></td>";
+       echo "<td colspan=4><input type=submit value='Create account' tabindex=103></td></tr></form>";
+       echo "</table><br>\n";
+       finishPortlet();
+}
+
+function printChildrenAsOptions ($root, $depth = 0)
+{
+       echo "<option value=${root['title']}>";
+       if ($depth == 0)
+               echo '* ';
+       for ($i = 0; $i < $depth; $i++)
+               echo '-- ';
+       echo $root['title'];
+       echo "</option>\n";
+       foreach ($root['kids'] as $kid)
+               printChildrenAsOptions ($kid, $depth + 1);
+}
+
+// 1. Find all parentless pages.
+// 2. For each of them recursively find all children.
+// 3. Output the tree with recursion tree display.
+function printPagesTree ()
+{
+       global $page;
+       echo '<pre>';
+       foreach ($page as $ctitle => $cpage)
+               if (!isset ($cpage['parent']))
+               {
+                       $croot['title'] = $ctitle;
+                       $croot['kids'] = getAllChildPages ($ctitle);
+                       printChildrenAsOptions ($croot);
+               }
+       echo '</pre>';
+}
+
+function renderPermissionsEditForm ()
+{
+       global $root, $pageno, $tabno, $perms, $accounts;
+       startPortlet ('User permissions');
+       showMessageOrError();
+       echo "<table cellspacing=0 cellpadding='5' align='center' class='widetable'>\n";
+       echo "<tr><th>&nbsp;</th><th>Username</th><th>Page</th><th>Tab</th><th>Access</th></tr>\n";
+       foreach ($perms as $username => $pages)
+               foreach ($pages as $access_page => $tabs)
+                       foreach ($tabs as $access_tab => $access)
+                       {
+                               echo "<td>";
+                               if ($username != '%')
+                                       $userid = $accounts[$username]['user_id'];
+                               else
+                                       $userid = 0;
+                               echo "<a href='${root}process.php?op=revoke&page=${pageno}&tab=${tabno}&access_userid=${userid}&access_page=${access_page}&access_tab=${access_tab}'>";
+                               printImageHREF ('revoke', 'Revoke permission');
+                               echo "</a></td>";
+                               echo "<td>${username}</td>";
+                               echo "<td>${access_page}</td>";
+                               echo "<td>${access_tab}</td>";
+                               echo "<td>${access}</td>";
+                               echo "</tr>\n";
+                       }
+       echo "<form action='${root}process.php' method=post><tr>";
+       echo "<input type=hidden name=op value=grant>\n";
+       echo "<input type=hidden name=page value='${pageno}'>\n";
+       echo "<input type=hidden name=tab value='${tabno}'>\n";
+       // FIXME: border=0 doesn't work here for unknown reason
+       echo "<td>";
+       printImageInput ('grant', 103);
+       echo "</td>";
+       echo "<td><select name=access_userid>";
+       echo "<option value=0>ANY</option>";
+       foreach ($accounts as $account)
+               echo "<option value=${account['user_id']}>${account['user_name']}</option>";
+       echo "</select></td>\n";
+       echo "<td><select name=access_page>";
+       echo "<option value='%'>ANY</option>";
+       printPagesTree();
+       echo "</select></td>";
+       echo "<td><input type=text size=16 name=access_tab tabindex=102 value=default></td>";
+       echo "<td><input type=radio name=access_value value=no checked>no <input type=radio name=access_value value=yes>yes</td>";
+       echo "</tr></form>";
+       echo "</table><br>\n";
+       finishPortlet();
+}
+
+function renderReadonlyParameters ()
+{
+       global $color, $default_port_type;
+       startPortlet ('config.php');
+       echo '<table border=0 align=center>';
+       echo "<tr><th class=tdright>version:</th><td class=tdleft>" . VERSION . "</td></tr>\n";
+       echo "<tr><th class=tdright>password hash:</th><td class=tdleft>" . PASSWORD_HASH . "</td></tr>\n";
+       echo "<tr><th class=tdright>default port type:</th><td class=tdleft>${default_port_type}</td></tr>\n";
+       foreach ($color as $class => $value)
+               echo "<tr><th class=tdright bgcolor='#${value}'>color for class ${class}</th><td class=tdleft>${value}</td></tr>\n";
+       echo "</table>\n";
+       finishPortlet();
+}
+
+function renderPortMap ($editable = FALSE)
+{
+       global $nextorder, $root, $pageno, $tabno;
+       showMessageOrError();
+       startPortlet ("Port compatibility map");
+       $ptlist = getPortTypes();
+       $pclist = getPortCompat();
+       $pctable = buildPortCompatMatrixFromList ($ptlist, $pclist);
+       if ($editable)
+       {
+               echo "<form method=post action='${root}process.php'>";
+               echo "<input type=hidden name=page value='${pageno}'>";
+               echo "<input type=hidden name=tab value='${tabno}'>";
+               echo "<input type=hidden name=op value=save>";
+       }
+       echo "<table class=cooltable border=0 cellpadding=5 cellspacing=0 align=center>\n";
+       echo "<tr><th class=vert_th>&nbsp;</th>";
+       foreach ($ptlist as $name2)
+               echo "<th>to ${name2}</th>";
+       echo "</tr>";
+       // Make a copy to have an independent array pointer.
+       $ptlistY = $ptlist;
+       $order = 'odd';
+       foreach ($ptlistY as $type1 => $name1)
+       {
+               echo "<tr class=row_${order}><th class=vert_th style='border-bottom: 0px;'>from $name1</th>";
+               foreach ($ptlist as $type2 => $name2)
+               {
+                       echo '<td><input type=checkbox' . ($editable ? " name=atom_${type1}_${type2}" : ' disabled');
+                       echo ($pctable[$type1][$type2] ? ' checked' : '') . '></td>';
+               }
+               echo "</tr>\n";
+               $order = $nextorder[$order];
+       }
+       echo '</table><br>';
+       if ($editable)
+       {
+               echo "<input type=submit value='Save changes'>";
+               echo "</form>";
+       }
+       finishPortlet();
+}
+
+function renderConfigMainpage ()
+{
+       global $pageno, $root;
+       $children = getDirectChildPages ($pageno);
+       echo '<ul>';
+       foreach ($children as $cpageno => $child)
+       {
+               $ctitle = $child['title']($cpageno);
+               echo "<li><a href='${root}?page=${cpageno}'>" . $ctitle['name'] . "</li>\n";
+       }
+       echo '';
+       echo '</ul>';
+}
+
+function renderRackPage ($rack_id)
+{
+       if ($rack_id == 0)
+       {
+               showError ('Invalid rack_id in renderRack()');
+               return;
+       }
+       if (($rackData = getRackData ($rack_id)) == NULL)
+       {
+               showError ('getRackData() failed in renderRack()');
+               return;
+       }
+       echo "<table border=0 class=objectview cellspacing=0 cellpadding=0><tr>";
+
+       // Left column with information.
+       echo "<td class=pcleft>";
+       startPortlet ('Rack information');
+       echo "<table border=0 cellspacing=0 cellpadding=3 width='100%'>\n";
+       echo "<tr><th width='50%' class=tdright>Rack row:</th><td class=tdleft>${rackData['row_name']}</td></tr>\n";
+       echo "<tr><th width='50%' class=tdright>Name:</th><td class=tdleft>${rackData['name']}</td></tr>\n";
+       echo "<tr><th width='50%' class=tdright>Height:</th><td class=tdleft>${rackData['height']}</td></tr>\n";
+       if (!empty ($rackData['comment']))
+               echo "<tr><th width='50%' class=tdright>Comment:</th><td class=tdleft>${rackData['comment']}</td></tr>\n";
+       echo '</table>';
+       finishPortlet();
+       echo '</td>';
+
+       // Right column with rendered rack.
+       echo '<td>';
+       startPortlet ('Rack diagram');
+       renderRack ($rack_id);
+       finishPortlet();
+       echo '</td>';
+
+       echo '</tr></table>';
+}
+
+function renderDictionary ()
+{
+       global $nextorder;
+       $dict = getDict();
+       echo "<table border=0><tr>";
+       foreach ($dict as $chapter)
+       {
+               echo "<td class=pcleft>";
+               startPortlet ($chapter['name'] . ' (' . count ($chapter['word']) . ')');
+               echo "<table class=cooltable border=0 cellpadding=5 cellspacing=0 align=center>\n";
+               $order = 'odd';
+               foreach ($chapter['word'] as $value)
+               {
+                       echo "<tr class=row_${order}><td class=tdleft>${value}</td></tr>";
+                       $order = $nextorder[$order];
+               }
+               echo "</table>";
+               finishPortlet();
+               echo "</td>";
+       }
+       echo "</tr></table>";
+}
+
+function renderDictionaryEditor ()
+{
+       global $root, $pageno, $tabno;
+       showMessageOrError();
+       $dict = getDict();
+       echo "<table border=0><tr>";
+       foreach ($dict as $chapter)
+       {
+               echo "<td class=pcleft>";
+               startPortlet ($chapter['name'] . ' (' . count ($chapter['word']) . ')');
+               echo "<table cellspacing=0 cellpadding=5 align=center class=widetable>\n";
+               foreach ($chapter['word'] as $key => $value)
+               {
+                       echo "<form action='${root}process.php' method=post>";
+                       echo "<input type=hidden name=page value='${pageno}'>";
+                       echo "<input type=hidden name=tab value='${tabno}'>";
+                       echo "<input type=hidden name=op value='upd'>";
+                       echo "<input type=hidden name=chapter_no value='${chapter['no']}'>";
+                       echo "<input type=hidden name=dict_key value='${key}'>";
+                       echo '<tr>';
+                       echo "<td><a href='${root}process.php?page=${pageno}&tab=${tabno}&op=del&chapter_no=${chapter['no']}&dict_key=${key}'>";
+                       printImageHREF ('delete', 'Delete word');
+                       echo "</a></td>";
+                       echo "<td class=tdright><input type=text name=dict_value size=32 value='${value}'></td>";
+                       echo "<td><input type=submit value=OK></td>";
+                       echo '</tr></form>';
+               }
+               echo "<form action='${root}process.php' method=post>";
+               echo "<input type=hidden name=page value='${pageno}'>";
+               echo "<input type=hidden name=tab value='${tabno}'>";
+               echo "<input type=hidden name=op value=add>";
+               echo "<input type=hidden name=chapter_no value='${chapter['no']}'>";
+               echo '<tr>';
+               echo "<td>&nbsp;</td>";
+               echo "<td class=tdright><input type=text name=dict_value size=32></td>";
+               echo "<td><input type=submit value=OK></td>";
+               echo '</tr></form>';
+               echo "</table>";
+               finishPortlet();
+               echo "</td>";
+       }
+       echo "</tr></table>";
+}
+
+// We don't allow to rename/delete a sticky chapter and we don't allow
+// to delete a non-empty chapter.
+function renderChaptersEditor ()
+{
+       global $root, $pageno, $tabno;
+       showMessageOrError();
+       $dict = getDict();
+       echo "<table cellspacing=0 cellpadding=5 align=center class=widetable>\n";
+       echo '<tr><th>&nbsp;</th><th>Chapter name</th><th>Words</th><th>&nbsp;</th></tr>';
+       foreach ($dict as $chapter)
+       {
+               $wordcount = count ($chapter['word']);
+               $sticky = $chapter['sticky'];
+               echo "<form action='${root}process.php' method=post>";
+               echo "<input type=hidden name=page value='${pageno}'>";
+               echo "<input type=hidden name=tab value='${tabno}'>";
+               echo "<input type=hidden name=op value=upd>";
+               echo "<input type=hidden name=chapter_no value='${chapter['no']}'>";
+               echo '<tr>';
+               echo '<td>';
+               if ($sticky or $wordcount > 0)
+                       echo '&nbsp;';
+               else
+               {
+                       echo "<a href='${root}process.php?page=${pageno}&tab=${tabno}&op=del&chapter_no=${chapter['no']}'>";
+                       printImageHREF ('delete', 'Remove chapter');
+                       echo "</a>";
+               }
+               echo '</td>';
+               echo "<td><input type=text name=chapter_name value='${chapter['name']}'" . ($sticky ? ' disabled' : '') . "></td>";
+               echo "<td class=tdleft>${wordcount}</td><td>";
+               if ($sticky)
+                       echo '&nbsp;';
+               else
+                       echo "<input type=submit value='OK'>";
+               echo '</td></tr>';
+               echo '</form>';
+       }
+       echo "<form action='${root}process.php' method=post>";
+       echo "<input type=hidden name=page value='${pageno}'>";
+       echo "<input type=hidden name=tab value='${tabno}'>";
+       echo "<input type=hidden name=op value=add>";
+       echo '<tr><td>';
+       printImageInput ('add');
+       echo "</td><td colspan=3><input type=text name=chapter_name></td>";
+       echo '</tr>';
+       echo '</form>';
+       echo "</table>\n";
+}
+
+function renderAttributes ()
+{
+       global $nextorder;
+       $attrMap = getAttrMap();
+       startPortlet ('Optional attributes');
+       echo "<table class=cooltable border=0 cellpadding=5 cellspacing=0 align=center>\n";
+       echo "<tr><th class=tdleft>Attribute name</th><th class=tdleft>Attribute type</th><th class=tdleft>Applies to</th></tr>";
+       $order = 'odd';
+       foreach ($attrMap as $attr)
+       {
+               echo "<tr class=row_${order}>";
+               echo "<td class=tdleft>${attr['name']}</td>";
+               echo "<td class=tdleft>${attr['type']}</td>";
+               echo '<td class=tdleft>';
+               if (count ($attr['application']) == 0)
+                       echo '&nbsp;';
+               else
+                       foreach ($attr['application'] as $app)
+                               if ($attr['type'] == 'dict')
+                                       echo "${app['objtype_name']} (values from '${app['chapter_name']}')<br>";
+                               else
+                                       echo "${app['objtype_name']}<br>";
+               echo '</td>';
+               echo "</tr>\n";
+               $order = $nextorder[$order];
+       }
+       echo "</table><br>\n";
+       finishPortlet();
+}
+
+function renderEditAttributesForm ()
+{
+       global $root, $pageno, $tabno;
+       $attrMap = getAttrMap();
+       showMessageOrError();
+       startPortlet ('Optional attributes');
+       echo "<table cellspacing=0 cellpadding=5 align=center class=widetable>\n";
+       echo '<tr><th>&nbsp;</th><th>Name</th><th>Type</th><th>&nbsp;</th></tr>';
+       foreach ($attrMap as $attr)
+       {
+               echo "<form action='${root}process.php' method=post>";
+               echo "<input type=hidden name=page value='${pageno}'>";
+               echo "<input type=hidden name=tab value='${tabno}'>";
+               echo "<input type=hidden name=op value=upd>";
+               echo "<input type=hidden name=attr_id value='${attr['id']}'>";
+               echo '<tr>';
+               echo "<td><a href='${root}process.php?page=${pageno}&tab=${tabno}&op=del&attr_id=${attr['id']}'>";
+               printImageHREF ('delete', 'Remove attribute');
+               echo '</a></td>';
+               echo "<td><input type=text name=attr_name value='${attr['name']}'></td>";
+               echo "<td>${attr['type']}</td>";
+               echo "<td><input type=submit value='OK'></td>";
+               echo '</tr>';
+               echo '</form>';
+       }
+       echo "<form action='${root}process.php' method=post>";
+       echo "<input type=hidden name=page value='${pageno}'>";
+       echo "<input type=hidden name=tab value='${tabno}'>";
+       echo "<input type=hidden name=op value=add>";
+       echo '<tr><td>';
+       printImageInput ('add');
+       echo "</td><td><input type=text name=attr_name></td>";
+       echo '<td><select name=attr_type>';
+       echo '<option value=uint>uint</option>';
+       echo '<option value=float>float</option>';
+       echo '<option value=string>string</option>';
+       echo '<option value=dict>dict</option>';
+       echo '</select></td>';
+       echo '</tr>';
+       echo '</form>';
+       echo "</table>\n";
+       finishPortlet();
+}
+
+function renderEditAttrMapForm ()
+{
+       global $root, $pageno, $tabno;
+       $attrMap = getAttrMap();
+       showMessageOrError();
+       startPortlet ('Attribute map');
+       echo "<table cellspacing=0 cellpadding=5 align=center class=widetable>\n";
+       echo '<tr><th>&nbsp;</th><th>Attribute name</th><th>Object type</th><th>Dictionary chapter</th></tr>';
+       foreach ($attrMap as $attr)
+       {
+               if (count ($attr['application']) == 0)
+                       continue;
+               foreach ($attr['application'] as $app)
+               {
+                       echo '<tr>';
+                       echo '<td>';
+                       echo "<a href='${root}process.php?page=${pageno}&tab=${tabno}&op=del&";
+                       echo "attr_id=${attr['id']}&objtype_id=${app['objtype_id']}'>";
+                       printImageHREF ('delete', 'Remove mapping');
+                       echo "</a>";
+                       echo '</td>';
+                       echo "<td>${attr['name']}</td>";
+                       echo "<td>${app['objtype_name']}</td>";
+                       echo "<td>";
+                       if ($attr['type'] == 'dict')
+                               echo "${app['chapter_name']}";
+                       else
+                               echo '&nbsp;';
+                       echo "</td></tr>\n";
+               }
+       }
+       echo "<form action='${root}process.php' method=post>";
+       echo "<input type=hidden name=page value='${pageno}'>";
+       echo "<input type=hidden name=tab value='${tabno}'>";
+       echo "<input type=hidden name=op value=add>";
+       echo '<tr><td>';
+       printImageInput ('add');
+       echo "</td><td><select name=attr_id>";
+       $shortType['uint'] = 'U';
+       $shortType['float'] = 'F';
+       $shortType['string'] = 'S';
+       $shortType['dict'] = 'D';
+       foreach ($attrMap as $attr)
+               echo "<option value=${attr['id']}>[" . $shortType[$attr['type']] . "] ${attr['name']}</option>";
+       echo "</select></td>";
+       echo '<td>';
+       printSelect (getObjectTypeList(), 'objtype_id');
+       echo '</td>';
+       $dict = getDict();
+       echo '<td><select name=chapter_no>';
+       foreach ($dict as $chapter)
+               if (!$chapter['sticky'])
+                       echo "<option value='${chapter['no']}'>${chapter['name']}</option>";
+       echo '</select></td>';
+       echo '</tr>';
+       echo '</form>';
+       echo "</table>\n";
+       finishPortlet();
+}
+
+function printImageHREF ($tag, $title = '')
+{
+       global $root, $image;
+       if (!isset ($image[$tag]))
+               $tag = 'error';
+       $img = $image[$tag];
+       echo
+               "<img " .
+               "src='${root}${img['path']}' " .
+               "width=${img['width']} " .
+               "height=${img['height']} " .
+               "border=0 " .
+               (empty ($title) ? '' : "title='${title}'") .
+               ">";
+}
+
+function printImageInput ($tag, $tabindex = 0)
+{
+       global $root, $image;
+       if (!isset ($image[$tag]))
+               $tag = 'error';
+       $img = $image[$tag];
+       echo
+               "<input type=image name=submit " .
+               "src='${root}${img['path']}' " .
+               "border=0 " .
+               ($tabindex ? '' : "tabindex=${tabindex}") .
+               ">";
+}
+
+// This function returns URL for favourite icon.
+function getFaviconURL ()
+{
+       global $root, $image;
+       return $root . $image['favicon']['path'];
+}
+
+function renderReportSummary ()
+{
+       echo "Here be dragons :-P";
+}
+
+function renderUIConfig ()
+{
+       echo "Here be dragons :-P";
+}
+
+?>
diff --git a/inc/navigation.php b/inc/navigation.php
new file mode 100644 (file)
index 0000000..cb62824
--- /dev/null
@@ -0,0 +1,283 @@
+<?
+/*
+*
+*  This file implements generic navigation for RackTables.
+*
+*/
+
+$page['index']['title'] = 'static_title';
+$page['index']['handler'] = 'renderIndex';
+
+$page['rackspace']['title'] = 'static_title';
+$page['rackspace']['handler'] = 'handler_rackspace';
+$page['rackspace']['parent'] = 'index';
+$tab['rackspace']['default'] = 'Browse';
+$tab['rackspace']['history'] = 'History';
+
+$page['objects']['title'] = 'static_title';
+$page['objects']['handler'] = 'handler_objects';
+$page['objects']['parent'] = 'index';
+$tab['objects']['default'] = 'View';
+$tab['objects']['newobj'] = 'Add an object';
+$tab['objects']['newmulti'] = 'Add multiple objects';
+
+$page['row']['title'] = 'dynamic_title_row';
+$page['row']['handler'] = 'handler_row';
+$page['row']['bypass'] = 'row_id';
+$page['row']['parent'] = 'rackspace';
+$tab['row']['default'] = 'View';
+$tab['row']['newrack'] = 'Add new rack';
+
+$page['rack']['title'] = 'dynamic_title_rack';
+$page['rack']['handler'] = 'handler_rack';
+$page['rack']['bypass'] = 'rack_id';
+$page['rack']['parent'] = 'row';
+$tab['rack']['default'] = 'View';
+$tab['rack']['edit'] = 'Properties';
+$tab['rack']['design'] = 'Design';
+$tab['rack']['problems'] = 'Problems';
+
+$page['objgroup']['title'] = 'dynamic_title_objgroup';
+$page['objgroup']['handler'] = 'handler_objgroup';
+$page['objgroup']['bypass'] = 'group_id';
+$page['objgroup']['parent'] = 'objects';
+
+$page['object']['title'] = 'dynamic_title_object';
+$page['object']['handler'] = 'handler_object';
+$page['object']['bypass'] = 'object_id';
+$page['object']['parent'] = 'objgroup';
+$tab['object']['default'] = 'View';
+$tab['object']['edit'] = 'Properties';
+$tab['object']['rackspace'] = 'Rackspace';
+$tab['object']['ports'] = 'Ports';
+$tab['object']['network'] = 'IPv4';
+$tab['object']['portfwrd'] = 'Port Forwarding';
+$ophandler['object']['ports']['addPort'] = 'addPortForObject';
+$ophandler['object']['ports']['delPort'] = 'delPortFromObject';
+$ophandler['object']['ports']['editPort'] = 'editPortForObject';
+$ophandler['object']['ports']['linkPort'] = 'linkPortForObject';
+$ophandler['object']['ports']['unlinkPort'] = 'unlinkPortForObject';
+$ophandler['object']['ports']['addMultiPorts'] = 'addMultiPorts';
+$ophandler['object']['ports']['useup'] = 'useupPort';
+$ophandler['object']['network']['editAddressFromObject'] = 'editAddressFromObject';
+$ophandler['object']['network']['addAddrFObj'] = 'addAddressToObject';
+$ophandler['object']['network']['delAddrFObj'] = 'delAddressFromObject';
+$ophandler['object']['edit']['del'] = 'resetAttrValue';
+$ophandler['object']['edit']['upd'] = 'updateAttrValue';
+$ophandler['object']['portfwrd']['forwardPorts'] = 'addPortForwarding';
+$ophandler['object']['portfwrd']['delPortForwarding'] = 'delPortForwarding';
+$ophandler['object']['portfwrd']['updPortForwarding'] = 'updPortForwarding';
+
+$page['ipv4space']['title'] = 'static_title';
+$page['ipv4space']['handler'] = 'handler_ipv4space';
+$page['ipv4space']['parent'] = 'index';
+$tab['ipv4space']['default'] = 'Browse';
+$tab['ipv4space']['newrange'] = 'Manage ranges';
+$ophandler['ipv4space']['newrange']['addRange'] = 'addNewrange';
+$ophandler['ipv4space']['newrange']['delRange'] = 'delRange';
+
+$page['iprange']['title'] = 'dynamic_title_iprange';
+$page['iprange']['handler'] = 'handler_iprange';
+$page['iprange']['parent'] = 'ipv4space';
+$page['iprange']['bypass'] = 'id';
+$tab['iprange']['default'] = 'Browse';
+$tab['iprange']['properties'] = 'Properties';
+$ophandler['iprange']['properties']['editRange'] = 'editRange';
+
+$page['ipaddress']['title'] = 'dynamic_title_ipaddress';
+$page['ipaddress']['handler'] = 'handler_ipaddress';
+$page['ipaddress']['parent'] = 'iprange';
+$page['ipaddress']['bypass'] = 'ip';
+$tab['ipaddress']['default'] = 'Browse';
+$tab['ipaddress']['properties'] = 'Properties';
+$tab['ipaddress']['assignment'] = 'Allocation';
+$ophandler['ipaddress']['properties']['editAddress'] = 'editAddress';
+$ophandler['ipaddress']['assignment']['delIpAssignment'] = 'delIpAssignment';
+$ophandler['ipaddress']['assignment']['editBondForAddress'] = 'editIpAssignment';
+$ophandler['ipaddress']['assignment']['bindObjectToIp'] = 'addIpAssignment';
+
+$page['search']['title'] = 'dynamic_title_search';
+$page['search']['handler'] = 'handler_search';
+$page['search']['parent'] = 'index';
+$page['search']['bypass'] = 'q';
+
+$page['config']['title'] = 'static_title';
+$page['config']['handler'] = 'handler_config';
+$page['config']['parent'] = 'index';
+
+$page['accounts']['title'] = 'static_title';
+$page['accounts']['handler'] = 'handler_accounts';
+$page['accounts']['parent'] = 'config';
+$tab['accounts']['default'] = 'View';
+$tab['accounts']['edit'] = 'Change';
+$ophandler['accounts']['edit']['updateAccount'] = 'updateUserAccount';
+$ophandler['accounts']['edit']['createAccount'] = 'createUserAccount';
+$ophandler['accounts']['edit']['disableAccount'] = 'disableUserAccount';
+$ophandler['accounts']['edit']['enableAccount'] = 'enableUserAccount';
+
+$page['perms']['title'] = 'static_title';
+$page['perms']['handler'] = 'handler_perms';
+$page['perms']['parent'] = 'config';
+$tab['perms']['default'] = 'View';
+$tab['perms']['edit'] = 'Change';
+$ophandler['perms']['edit']['revoke'] = 'revokePermission';
+$ophandler['perms']['edit']['grant'] = 'grantPermission';
+
+$page['portmap']['title'] = 'static_title';
+$page['portmap']['handler'] = 'handler_portmap';
+$page['portmap']['parent'] = 'config';
+$tab['portmap']['default'] = 'View';
+$tab['portmap']['edit'] = 'Change';
+$ophandler['portmap']['edit']['save'] = 'savePortMap';
+
+$page['attrs']['title'] = 'static_title';
+$page['attrs']['handler'] = 'handler_attrs';
+$page['attrs']['parent'] = 'config';
+$tab['attrs']['default'] = 'View';
+$tab['attrs']['editattrs'] = 'Edit attributes';
+$tab['attrs']['editmap'] = 'Edit map';
+$ophandler['attrs']['editattrs']['add'] = 'createAttribute';
+$ophandler['attrs']['editattrs']['upd'] = 'changeAttribute';
+$ophandler['attrs']['editattrs']['del'] = 'deleteAttribute';
+$ophandler['attrs']['editmap']['add'] = 'supplementAttrMap';
+$ophandler['attrs']['editmap']['del'] = 'reduceAttrMap';
+
+$page['dict']['title'] = 'static_title';
+$page['dict']['handler'] = 'handler_dict';
+$page['dict']['parent'] = 'config';
+$tab['dict']['default'] = 'View';
+$tab['dict']['edit'] = 'Edit words';
+$tab['dict']['chapters'] = 'Manage chapters';
+$ophandler['dict']['edit']['del'] = 'reduceDictionary';
+$ophandler['dict']['edit']['upd'] = 'updateDictionary';
+$ophandler['dict']['edit']['add'] = 'supplementDictionary';
+$ophandler['dict']['chapters']['del'] = 'delChapter';
+$ophandler['dict']['chapters']['upd'] = 'updateChapter';
+$ophandler['dict']['chapters']['add'] = 'addChapter';
+
+$page['ui']['title'] = 'static_title';
+$page['ui']['handler'] = 'handler_ui';
+$page['ui']['parent'] = 'config';
+
+$page['ro']['title'] = 'static_title';
+$page['ro']['handler'] = 'handler_ro';
+$page['ro']['parent'] = 'config';
+
+$page['reports']['title'] = 'static_title';
+$page['reports']['handler'] = 'handler_reports';
+$page['reports']['parent'] = 'index';
+$tab['reports']['default'] = 'View';
+
+// This function returns array if page numbers leading to the target page
+// plus page number of target page itself. The first element is the target
+// page number and the last element is the index page number.
+function getPath ($targetno)
+{
+       global $page;
+       $path = array();
+       // Recursion breaks at first parentless page.
+       if (!isset ($page[$targetno]['parent']))
+               $path = array ($targetno);
+       else
+       {
+               $path = getPath ($page[$targetno]['parent']);
+               $path[] = $targetno;
+       }
+       return $path;
+}
+
+function showPathAndSearch ($pageno)
+{
+       global $root, $page, $enterprise;
+       // Path.
+       echo "<td class=activemenuitem width='99%'>${enterprise} RackTables";
+       if (isset ($page[$pageno]['title']))
+       {
+               $path = getPath ($pageno);
+               foreach ($path as $dummy => $no)
+               {
+                       $title = $page[$no]['title']($no);
+                       echo ": <a href='${root}?page=${no}";
+                       foreach ($title['params'] as $param_name => $param_value)
+                               echo "&${param_name}=${param_value}";
+                       echo "'>" .$title['name'] . "</a>";
+               }
+       }
+       echo "</td>";
+       // Search form.
+       echo "<td><table border=0 cellpadding=0 cellspacing=0><tr><td>Search:</td>";
+       echo "<form method=get action='${root}'><td>";
+       echo '<input type=hidden name=page value=search>';
+       // This input will be the first, if we don't add ports or addresses.
+       echo "<input type=text name=q size=20 tabindex=1000></td></form></tr></table></td>";
+}
+
+function getTitle ($pageno, $tabno)
+{
+       global $page, $enterprise;
+       if (isset ($page[$pageno]['title']))
+       {
+               $ret = $page[$pageno]['title']($pageno);
+               $ret = $ret['name'];
+       }
+       else
+               $ret = $enterprise;
+       return $ret;
+}
+
+function showTabs ($pageno, $tabno)
+{
+       global $tab, $root, $page, $remote_username;
+       if (!isset ($tab[$pageno]['default']))
+               return;
+       echo "<td><div class=greynavbar><ul id=foldertab style='margin-bottom: 0px; padding-top: 10px;'>";
+       foreach ($tab[$pageno] as $tabidx => $tabtitle)
+       {
+               // Hide forbidden tabs.
+               if (authorized ($remote_username, $pageno, $tabidx) == FALSE)
+                       continue;
+               echo '<li><a' . (($tabidx == $tabno) ? ' class=current' : '');
+               echo " href='${root}?page=${pageno}&tab=${tabidx}";
+               if (isset ($page[$pageno]['bypass']) and isset ($_REQUEST[$page[$pageno]['bypass']]))
+               {
+                       $bpname = $page[$pageno]['bypass'];
+                       $bpval = $_REQUEST[$bpname];
+                       echo "&${bpname}=${bpval}";
+               }
+               echo "'>${tabtitle}</a></li>\n";
+       }
+       echo "</ul></div></td>\n";
+}
+
+// This function returns pages, which are direct children of the requested
+// page and are accessible by the current user.
+function getDirectChildPages ($pageno)
+{
+       global $page, $remote_username;
+       $children = array();
+       foreach ($page as $cpageno => $cpage)
+               if
+               (
+                       isset ($cpage['parent']) and
+                       $cpage['parent'] == $pageno and
+                       authorized ($remote_username, $cpageno, 'default') == TRUE
+               )
+                       $children[$cpageno] = $cpage;
+       return $children;
+}
+
+function getAllChildPages ($parent)
+{
+       global $page;
+       // Array pointer is global, so if we don't create local copies of
+       // the global array, we can't advance any more after nested call
+       // of getAllChildPages returns.
+       $mypage = $page;
+       $mykids = array();
+       foreach ($mypage as $ctitle => $cpage)
+               if (isset ($cpage['parent']) and $cpage['parent'] == $parent)
+                       $mykids[] = array ('title' => $ctitle, 'kids' => getAllChildPages ($ctitle));
+       return $mykids;
+}
+
+?>
diff --git a/inc/ophandlers.php b/inc/ophandlers.php
new file mode 100644 (file)
index 0000000..410def3
--- /dev/null
@@ -0,0 +1,822 @@
+<?
+/*
+*
+*  This file is a library of operation handlers for RackTables.
+*
+*/
+
+function addPortForwarding ()
+{
+       global $root, $pageno, $tabno;
+
+       $object_id = $_REQUEST['object_id'];
+       $localip = $_REQUEST['localip'];
+       $remoteip = $_REQUEST['remoteip'];
+       $localport = $_REQUEST['localport'];
+       $remoteport = $_REQUEST['remoteport'];
+       $proto = $_REQUEST['proto'];
+       $mode = $_REQUEST['mode'];
+       $description = $_REQUEST['description'];
+
+       $retpage="${root}?page=${pageno}&tab=${tabno}&object_id=$object_id";
+
+
+       $error=newPortForwarding($object_id, $localip, $localport, $remoteip, $remoteport, $proto, $description);
+
+       if ($error == '')
+               return "${retpage}&message=".urlencode('Port forwarding successfully added.');
+       else
+       {
+               return "${retpage}&error=".urlencode($error);
+       }
+       
+}
+
+function delPortForwarding ()
+{
+       global $root, $pageno, $tabno;
+
+       $object_id = $_REQUEST['object_id'];
+       $localip = $_REQUEST['localip'];
+       $remoteip = $_REQUEST['remoteip'];
+       $localport = $_REQUEST['localport'];
+       $remoteport = $_REQUEST['remoteport'];
+       $proto = $_REQUEST['proto'];
+       $mode = $_REQUEST['mode'];
+
+       $retpage="${root}?page=${pageno}&tab=${tabno}&object_id=$object_id";
+
+       $error=deletePortForwarding($object_id, $localip, $localport, $remoteip, $remoteport, $proto);
+       if ($error == '')
+               return "${retpage}&message=".urlencode('Port forwarding successfully deleted.');
+       else
+       {
+               return "${retpage}&error=".urlencode($error);
+       }
+       
+}
+
+function updPortForwarding ()
+{
+       global $root, $pageno, $tabno;
+
+       $object_id = $_REQUEST['object_id'];
+       $localip = $_REQUEST['localip'];
+       $remoteip = $_REQUEST['remoteip'];
+       $localport = $_REQUEST['localport'];
+       $remoteport = $_REQUEST['remoteport'];
+       $proto = $_REQUEST['proto'];
+       $description = $_REQUEST['description'];
+
+       $retpage="${root}?page=${pageno}&tab=${tabno}&object_id=$object_id";
+
+       $error=updatePortForwarding($object_id, $localip, $localport, $remoteip, $remoteport, $proto, $description);
+       if ($error == '')
+               return "${retpage}&message=".urlencode('Port forwarding successfully updated.');
+       else
+       {
+               return "${retpage}&error=".urlencode($error);
+       }
+       
+}
+
+
+
+function addPortForObject ()
+{
+       global $root, $pageno, $tabno;
+
+       $object_id = $_REQUEST['object_id'];
+       $port_name = $_REQUEST['port_name'];
+       $port_l2address = $_REQUEST['port_l2address'];
+       $port_label = $_REQUEST['port_label'];
+       $port_type_id = $_REQUEST['port_type_id'];
+
+
+       if ($port_name == '')
+               return "${root}?page=${pageno}&tab=${tabno}&object_id=${object_id}&error=".urlencode('Port name cannot be empty');
+       else
+       {
+               $error = commitAddPort ($object_id, $port_name, $port_type_id, $port_label, $port_l2address);
+               if ($error != '')
+               {
+                       return "${root}?page=${pageno}&tab=${tabno}&object_id=${object_id}&error=".urlencode($error);
+               }
+               else
+               {
+                       return "${root}?page=${pageno}&tab=${tabno}&object_id=${object_id}&message=".urlencode("Port $port_name added successfully");
+               }
+       }
+       
+}
+
+function editPortForObject ()
+{
+       global $root, $pageno, $tabno;
+
+       assertUIntArg ('port_id');
+       assertUIntArg ('object_id');
+       assertStringArg ('name');
+       $port_id = $_REQUEST['port_id'];
+       $object_id = $_REQUEST['object_id'];
+       $port_name = $_REQUEST['name'];
+       $port_l2address = $_REQUEST['l2address'];
+       $port_label = $_REQUEST['label'];
+       if (isset ($_REQUEST['reservation_comment']) and !empty ($_REQUEST['reservation_comment']))
+               $port_rc = '"' . $_REQUEST['reservation_comment'] . '"';
+       else
+               $port_rc = 'NULL';
+
+       if ($port_name == '')
+               return "${root}?page=${pageno}&tab=${tabno}&object_id=${object_id}&error=".urlencode('Port name cannot be empty');
+       else
+       {
+               $error = commitUpdatePort ($port_id, $port_name, $port_label, $port_l2address, $port_rc);
+               if ($error != '')
+               {
+                       return "${root}?page=${pageno}&tab=${tabno}&object_id=${object_id}&error=".urlencode($error);
+               }
+               else
+               {
+                       return "${root}?page=${pageno}&tab=${tabno}&object_id=${object_id}&message=".urlencode("Port $port_name updated successfully");
+               }
+       }
+       
+}
+
+function delPortFromObject ()
+{
+       global $root, $pageno, $tabno;
+
+       $port_id = $_REQUEST['port_id'];
+       $port_name = $_REQUEST['port_name'];
+       $object_id = $_REQUEST['object_id'];
+       $error = delObjectPort($port_id);
+
+       if ($error != '')
+       {
+               return "${root}?page=${pageno}&tab=${tabno}&object_id=${object_id}&error=".urlencode($error);
+       }
+       else
+       {
+               return "${root}?page=${pageno}&tab=${tabno}&object_id=${object_id}&message=".urlencode("Port $port_name deleted successfully");
+       }
+}
+
+function linkPortForObject ()
+{
+       global $root, $pageno, $tabno;
+
+       $port_id = $_REQUEST['port_id'];
+       $remote_port_id = $_REQUEST['remote_port_id'];
+       $object_id = $_REQUEST['object_id'];
+       $port_name = $_REQUEST['port_name'];
+       $remote_port_name = $_REQUEST['remote_port_name'];
+       $remote_object_name = $_REQUEST['remote_object_name'];
+
+       $error = linkPorts($port_id, $remote_port_id);
+       if ($error != '')
+       {
+               return "${root}?page=${pageno}&tab=${tabno}&object_id=${object_id}&error=".urlencode($error);
+       }
+       else
+       {
+               return "${root}?page=${pageno}&tab=${tabno}&object_id=${object_id}&message=".urlencode("Port $port_name successfully linked with port $remote_port_name at object $remote_object_name");
+       }
+}
+
+function unlinkPortForObject ()
+{
+       global $root, $pageno, $tabno;
+
+       $port_id = $_REQUEST['port_id'];
+       $object_id = $_REQUEST['object_id'];
+       $port_name = $_REQUEST['port_name'];
+       $remote_port_name = $_REQUEST['remote_port_name'];
+       $remote_object_name = $_REQUEST['remote_object_name'];
+
+       $error = unlinkPort($port_id);
+       if ($error != '')
+       {
+               return "${root}?page=${pageno}&tab=${tabno}&object_id=${object_id}&error=".urlencode($error);
+       }
+       else
+       {
+               return "${root}?page=${pageno}&tab=${tabno}&object_id=${object_id}&message=".urlencode("Port $port_name successfully unlinked from port $remote_port_name at object $remote_object_name");
+       }
+}
+
+function addMultiPorts ()
+{
+       global $root, $pageno, $tabno;
+       // Parse data.
+       assertStringArg ('format');
+       assertStringArg ('input');
+       assertUIntArg ('port_type');
+       assertUIntArg ('object_id');
+       $format = $_REQUEST['format'];
+       $port_type = $_REQUEST['port_type'];
+       $object_id = $_REQUEST['object_id'];
+       // Input lines are escaped, so we have to explode and to chop by 2-char
+       // \n and \r respectively.
+       $lines1 = explode ('\n', $_REQUEST['input']);
+       foreach ($lines1 as $line)
+       {
+               $parts = explode ('\r', $line);
+               reset ($parts);
+               if (empty ($parts[0]))
+                       continue;
+               else
+                       $lines2[] = rtrim ($parts[0]);
+       }
+       $ports = array();
+       foreach ($lines2 as $line)
+       {
+               switch ($format)
+               {
+                       case 'fisxii':
+                               $words = explode (' ', ereg_replace ('[[:space:]]+', ' ', $line));
+                               list ($slot, $port) = explode ('/', $words[0]);
+                               $ports[] = array
+                               (
+                                       'name' => "e ${slot}/${port}",
+                                       'l2address' => $words[8],
+                                       'label' => "slot ${slot} port ${port}"
+                               );
+                               break;
+                       case 'c3600asy':
+                               $words = explode (' ', ereg_replace ('[[:space:]]+', ' ', trim (substr ($line, 3))));
+/*
+How Async Lines are Numbered in Cisco 3600 Series Routers
+http://www.cisco.com/en/US/products/hw/routers/ps274/products_tech_note09186a00801ca70b.shtml
+
+Understanding 16- and 32-Port Async Network Modules
+http://www.cisco.com/en/US/products/hw/routers/ps274/products_tech_note09186a00800a93f0.shtml
+*/
+                               $async = $words[0];
+                               $slot = floor (($async - 1) / 32);
+                               $octalgroup = floor (($async - 1 - $slot * 32) / 8);
+                               $cable = $async - $slot * 32 - $octalgroup * 8;
+                               $og_label[0] = 'async 0-7';
+                               $og_label[1] = 'async 8-15';
+                               $og_label[2] = 'async 16-23';
+                               $og_label[3] = 'async 24-31';
+                               $ports[] = array
+                               (
+                                       'name' => "async ${async}",
+                                       'l2address' => '',
+                                       'label' => "slot ${slot} " . $og_label[$octalgroup] . " cable ${cable}"
+                               );
+                               break;
+                       case 'fiwg':
+                               $words = explode (' ', ereg_replace ('[[:space:]]+', ' ', $line));
+                               $ifnumber = $words[0] * 1;
+                               $ports[] = array
+                               (
+                                       'name' => "e ${ifnumber}",
+                                       'l2address' => "${words[8]}",
+                                       'label' => "${ifnumber}"
+                               );
+                               break;
+                       default:
+                               return
+                                       "${root}?page=${pageno}&tab=${tabno}&object_id=${object_id}&error=" .
+                                       urlencode('Cannot process submitted data: unknown format code.');
+                               break;
+               }
+       }
+       // Create ports, if they don't exist.
+       $added_count = $updated_count = $error_count = 0;
+       foreach ($ports as $port)
+       {
+               $port_id = getPortID ($object_id, $port['name']);
+               if ($port_id === NULL)
+               {
+                       $result = commitAddPort ($object_id, $port['name'], $port_type, $port['label'], $port['l2address']);
+                       if ($result == '')
+                               $added_count++;
+                       else
+                               $error_count++;
+               }
+               else
+               {
+                       $result = commitUpdatePort ($port_id, $port['name'], $port['label'], $port['l2address']);
+                       if ($result == '')
+                               $updated_count++;
+                       else
+                               $error_count++;
+               }
+       }
+       return
+               "${root}?page=${pageno}&tab=${tabno}&object_id=${object_id}&message=" .
+               urlencode("Added ${added_count} ports, updated ${updated_count} ports, encountered ${error_count} errors.");
+}
+
+function editAddressFromObject ()
+{
+       global $root;
+
+       $ip = $_REQUEST['ip'];
+       $object_id = $_REQUEST['object_id'];
+       $name = $_REQUEST['bond_name'];
+       $type = $_REQUEST['bond_type'];
+       $error = updateBond($ip, $object_id, $name, $type);
+       if ($error != '')
+       {
+               return "${root}?page=object&tab=network&object_id=$object_id&error=".urlencode($error);
+       }
+       else
+       {
+               return "${root}?page=object&tab=network&object_id=$object_id&message=".urlencode("Interface successfully updated");
+       }
+}
+
+function delAddressFromObject ()
+{
+       global $root;
+
+       $ip = $_REQUEST['ip'];
+       $object_id = $_REQUEST['object_id'];
+       $error = unbindIpFromObject($ip, $object_id);
+       if ($error != '')
+       {
+               return "${root}?page=object&tab=network&object_id=$object_id&error=".urlencode($error);
+       }
+       else
+       {
+               return "${root}?page=object&tab=network&object_id=$object_id&message=".urlencode("Interface successfully deleted");
+       }
+}
+
+function delIpAssignment ()
+{
+       global $root;
+
+       $ip = $_REQUEST['ip'];
+       $object_id = $_REQUEST['object_id'];
+       $error = unbindIpFromObject($ip, $object_id);
+       if ($error != '')
+       {
+               return "${root}?page=ipaddress&tab=assignment&ip=$ip&error=".urlencode($error);
+       }
+       else
+       {
+               return "${root}?page=ipaddress&tab=assignment&ip=$ip&message=".urlencode("Interface successfully deleted");
+       }
+}
+
+function editIpAssignment ()
+{
+       global $root;
+
+       $ip = $_REQUEST['ip'];
+       $object_id = $_REQUEST['object_id'];
+       $name = $_REQUEST['bond_name'];
+       $type = $_REQUEST['bond_type'];
+       $error = updateBond($ip, $object_id, $name, $type);
+
+       if ($error != '')
+       {
+               return "${root}?page=ipaddress&tab=assignment&ip=$ip&error=".urlencode($error);
+       }
+       else
+       {
+               return "${root}?page=ipaddress&tab=assignment&ip=$ip&message=".urlencode("Interface successfully updated");
+       }
+}
+
+function addIpAssignment ()
+{
+       global $root;
+
+       $ip = $_REQUEST['ip'];
+       $object_id = $_REQUEST['object_id'];
+       $name = $_REQUEST['bond_name'];
+       $type = $_REQUEST['bond_type'];
+       $error = bindIpToObject($ip, $object_id, $name, $type);
+       if ($error != '')
+       {
+               return "${root}?page=ipaddress&tab=assignment&ip=$ip&error=".urlencode($error);
+       }
+       else
+       {
+               return "${root}?page=ipaddress&tab=assignment&ip=$ip&message=".urlencode("Interface successfully added");
+       }
+}
+
+function addNewRange ()
+{
+       global $root, $pageno, $tabno;
+
+       $ip = $_REQUEST['ip'];
+       $mask = $_REQUEST['mask'];
+       $name = $_REQUEST['name'];
+       $error = addRange($ip, $mask, $name);
+       if ($error != '')
+       {
+               return "${root}?page=${pageno}&tab=${tabno}&error=".urlencode($error);
+       }
+       else
+       {
+               return "${root}?page=${pageno}&tab=${tabno}&message=".urlencode("Range successfully added");
+       }
+}
+
+function editRange ()
+{
+       global $root, $pageno, $tabno;
+
+       $id = $_REQUEST['id'];
+       $name = $_REQUEST['name'];
+       $error = updateRange($id, $name);
+       if ($error != '')
+       {
+               return "${root}?page=${pageno}&tab=${tabno}&id=$id&error=".urlencode($error);
+       }
+       else
+       {
+               return "${root}?page=${pageno}&tab=${tabno}&id=$id&message=".urlencode("Range updated");
+       }
+
+}
+
+function delRange ()
+{
+       global $root, $pageno, $tabno;
+
+       $id = $_REQUEST['id'];
+       $error = commitDeleteRange ($id);
+       if ($error != '')
+       {
+               return "${root}?page=${pageno}&tab=${tabno}&error=".urlencode($error);
+       }
+       else
+       {
+               return "${root}?page=${pageno}&tab=${tabno}&message=".urlencode("Range deleted");
+       }
+
+}
+
+function editAddress ()
+{
+       global $root, $pageno, $tabno;
+
+       $ip = $_REQUEST['ip'];
+       $name = $_REQUEST['name'];
+       if (isset ($_REQUEST['reserved']))
+               $reserved = $_REQUEST['reserved'];
+       else
+               $reserved = 'off';
+       $error = updateAddress($ip, $name, $reserved=='on'?'yes':'no');
+       if ($error != '')
+       {
+               return "${root}?page=${pageno}&tab=${tabno}&ip=$ip&error=".urlencode($error);
+       }
+       else
+       {
+               return "${root}?page=${pageno}&tab=${tabno}&ip=$ip&message=".urlencode("Address updated");
+       }
+
+}
+
+function addAddressToObject ()
+{
+       global $root, $pageno, $tabno;
+
+       $ip = $_REQUEST['ip'];
+       $object_id = $_REQUEST['object_id'];
+       $name = $_REQUEST['name'];
+       $type = $_REQUEST['type'];
+       $error = bindIpToObject($ip, $object_id, $name, $type);
+       if ($error != '')
+       {
+               return "${root}?page=${pageno}&tab=${tabno}&object_id=$object_id&error=".urlencode($error);
+       }
+       else
+       {
+               return "${root}?page=$pageno&tab=${tabno}&object_id=$object_id&message=".urlencode("Interface successfully added");
+       }
+}
+
+function createUserAccount ()
+{
+       global $root, $pageno, $tabno;
+       assertStringArg ('username');
+       assertStringArg ('realname', TRUE);
+       assertStringArg ('password');
+       $username = $_REQUEST['username'];
+       $password = hash (PASSWORD_HASH, $_REQUEST['password']);
+       $result = commitCreateUserAccount ($username, $_REQUEST['realname'], $password);
+       if ($result == TRUE)
+               return "${root}?page=${pageno}&tab=${tabno}&message=" . urlencode ("User account ${username} created.");
+       else
+               return "${root}?page=${pageno}&tab=${tabno}&error=" . urlencode ("Error creating user account ${username}.");
+}
+
+function updateUserAccount ()
+{
+       global $root, $pageno, $tabno;
+       assertUIntArg ('id');
+       assertStringArg ('username');
+       assertStringArg ('realname', TRUE);
+       assertStringArg ('password');
+       // We might be asked to change username, so use user ID only.
+       $id = $_REQUEST['id'];
+       $username = $_REQUEST['username'];
+       $new_password = $_REQUEST['password'];
+       $old_hash = getHashByID ($id);
+       if ($old_hash == NULL)
+       {
+               showError ('getHashByID() failed in updateUserAccount()');
+               return;
+       }
+       // Update user password only if provided password is not the same as current password hash.
+       if ($new_password != $old_hash)
+               $new_password = hash (PASSWORD_HASH, $new_password);
+       $result = commitUpdateUserAccount ($_REQUEST['id'], $username, $_REQUEST['realname'], $new_password);
+       if ($result == TRUE)
+               return "${root}?page=${pageno}&tab=${tabno}&message=" . urlencode ("User account ${username} updated.");
+       else
+               return "${root}?page=${pageno}&tab=${tabno}&error=" . urlencode ("Error updating user account ${username}.");
+}
+
+function enableUserAccount ()
+{
+       global $root, $pageno, $tabno;
+       assertUIntArg ('id');
+       $id = $_REQUEST['id'];
+       $result = commitEnableUserAccount ($id, 'yes');
+       if ($result == TRUE)
+               return "${root}?page=${pageno}&tab=${tabno}&message=" . urlencode ("User account enabled.");
+       else
+               return "${root}?page=${pageno}&tab=${tabno}&error=" . urlencode ("Error enabling user account.");
+}
+
+function disableUserAccount ()
+{
+       global $root, $pageno, $tabno;
+       assertUIntArg ('id');
+       $id = $_REQUEST['id'];
+       if ($id == 1)
+               $result = FALSE;
+       else
+               $result = commitEnableUserAccount ($id, 'no');
+       if ($result == TRUE)
+               return "${root}?page=${pageno}&tab=${tabno}&message=" . urlencode ("User account disabled.");
+       else
+               return "${root}?page=${pageno}&tab=${tabno}&error=" . urlencode ("Error disabling user account.");
+}
+
+function revokePermission ()
+{
+       global $root, $pageno, $tabno;
+       assertUIntArg ('access_userid', TRUE);
+       assertStringArg ('access_page');
+       assertStringArg ('access_tab');
+       $result = commitRevokePermission
+       (
+               $_REQUEST['access_userid'],
+               $_REQUEST['access_page'],
+               $_REQUEST['access_tab']
+       );
+       if ($result == TRUE)
+               return "${root}?page=${pageno}&tab=${tabno}&message=" . urlencode ("Revoke successfull.");
+       else
+               return "${root}?page=${pageno}&tab=${tabno}&error=" . urlencode ("Error revoking permission.");
+}
+
+function grantPermission ()
+{
+       global $root, $pageno, $tabno;
+       assertUIntArg ('access_userid', TRUE);
+       assertStringArg ('access_page');
+       assertStringArg ('access_tab');