<?php

/**
 * ---------------------------------------------------------------------
 *
 * GLPI - Gestionnaire Libre de Parc Informatique
 *
 * http://glpi-project.org
 *
 * @copyright 2015-2023 Teclib' and contributors.
 * @copyright 2003-2014 by the INDEPNET Development Team.
 * @licence   https://www.gnu.org/licenses/gpl-3.0.html
 *
 * ---------------------------------------------------------------------
 *
 * LICENSE
 *
 * This file is part of GLPI.
 *
 * 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 3 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, see <https://www.gnu.org/licenses/>.
 *
 * ---------------------------------------------------------------------
 */

/**
 * Represent an IPv4 or an IPv6 address. Both textual (ie. human readable)
 * and binary (ie. : used for request) are present
 * @since 0.84
 */


/** Class IPAddress : Represents an IPv4 or an IPv6 address. Both textual (ie. human readable)
 * and binary (ie. : used for SQL requests) are present inside the DB.
 * The class itself contains three protected attributes. If the address is valid, then, these
 * attributes are not empty.
 * This object is usefull for SQL research and binary<=>textual conversions.
 * @warning textual (ie. human readable) representation is not unique for IPv6 addresses :
 * 2001:db8:0:85a3\::ac1f:8001 = 2001:db8:0:85a3:0:0:ac1f:8001
 * @warning All textual representation of IPv6 addresses conforms to RFC 5952 : they are
 * automatically converted by IPAddress::setAddressFromString().
 * @since 0.84
 **/
class IPAddress extends CommonDBChild
{
   // From CommonDBChild
    public static $itemtype       = 'itemtype';
    public static $items_id       = 'items_id';
    public $dohistory             = false;

    public $history_blacklist     = ['binary_0', 'binary_1', 'binary_2', 'binary_3'];

   /// $version (integer) : version of the adresse. Should be 4 or 6, or empty if not valid address
    protected $version = '';
   /// $this->textual (string) : human readable of the IP adress (for instance : 192.168.0.0,
   /// 2001:db8:0:85a3\::ac1f:8001)
    protected $textual = '';
   /// $this->binary (bytes[4]) : binary version for the SQL requests. For IPv4 addresses, the
   /// first three bytes are set to [0, 0, 0xffff]
    protected $binary  = [0, 0, 0, 0];
   //to know is IPV4 is Dotted quoad Format
    protected $isDottedQuoadFormat = false;

    public static $rightname  = 'internet';

   //////////////////////////////////////////////////////////////////////////////
   // CommonDBTM related methods
   //////////////////////////////////////////////////////////////////////////////


    /**
     * @param IPAddress|string|integer[] $ipaddress (default '')
     **/
    public function __construct($ipaddress = '')
    {

       // First, be sure that the parent is correctly initialised
        parent::__construct();

       // If $ipaddress if empty, then, empty address !
        if ($ipaddress != '') {
           // If $ipaddress if an IPAddress, then just clone it
            if ($ipaddress instanceof IPAddress) {
                $this->version = $ipaddress->version;
                $this->textual = $ipaddress->textual;
                $this->binary  = $ipaddress->binary;
                $this->fields  = $ipaddress->fields;
            } else {
               // Else, check a binary then a string
                if (!$this->setAddressFromBinary($ipaddress)) {
                    $this->setAddressFromString($ipaddress);
                }
            }
        }
    }


    public static function getTypeName($nb = 0)
    {
        return _n('IP address', 'IP addresses', $nb);
    }


    /**
     * @param $input
     **/
    public function prepareInput($input)
    {

       // If $input['name'] does not exists, then, don't check anything !
        if (isset($input['name'])) {
           // WARNING: we must in every case, because, sometimes, fields are partially feels

           // If previous value differs from current one, then check it !
            $this->setAddressFromString($input['name']);
            if (!$this->is_valid()) {
                if (isset($input['is_dynamic']) && $input['is_dynamic']) {
                   // We allow invalid IPs that are dynamics !
                    $input['version']  = 0;
                    $input['binary_0'] = 0;
                    $input['binary_1'] = 0;
                    $input['binary_2'] = 0;
                    $input['binary_3'] = 0;
                    return $input;
                }
                //TRANS: %s is the invalid address
                $msg = sprintf(__('%1$s: %2$s'), __('Invalid IP address'), $input['name']);
                Session::addMessageAfterRedirect($msg, false, ERROR);
                return false;
            }
        }
        if (isset($input['itemtype']) && isset($input['items_id'])) {
            $input['mainitemtype'] = 'NULL';
            $input['mainitems_id'] = 0;
            if ($input['itemtype'] == 'NetworkName') {
                $name = new NetworkName();
                if ($name->getFromDB($input['items_id'])) {
                    if ($port = getItemForItemtype($name->getField('itemtype'))) {
                        if ($port->getFromDB($name->getField('items_id'))) {
                            if (isset($port->fields['itemtype']) && isset($port->fields['items_id'])) {
                                $input['mainitemtype'] = $port->fields['itemtype'];
                                $input['mainitems_id'] = $port->fields['items_id'];
                            }
                        }
                    }
                }
            }
        }

        return array_merge($input, $this->setArrayFromAddress($input, "version", "name", "binary"));
    }


    public function prepareInputForAdd($input)
    {

        return parent::prepareInputForAdd($this->prepareInput($input));
    }


    public function prepareInputForUpdate($input)
    {
        return parent::prepareInputForUpdate($this->prepareInput($input));
    }


    public function post_addItem()
    {
        IPAddress_IPNetwork::addIPAddress($this);
        parent::post_addItem();
    }


    public function post_updateItem($history = 1)
    {

        if (
            (isset($this->oldvalues['name']))
            || (isset($this->oldvalues['entities_id']))
        ) {
            $link = new IPAddress_IPNetwork();
            $link->cleanDBonItemDelete($this->getType(), $this->getID());
            $link->addIPAddress($this);
        }

        parent::post_updateItem($history);
    }


    public function cleanDBonPurge()
    {

        $this->deleteChildrenAndRelationsFromDb(
            [
                IPAddress_IPNetwork::class,
            ]
        );
    }


    public function post_getFromDB()
    {

       // Don't forget set local object from DB field
        $this->setAddressFromArray($this->fields, "version", "name", "binary");
    }


    public static function showForItem(CommonGLPI $item, $withtemplate = 0)
    {
        global $CFG_GLPI;

        if ($item->getType() == 'IPNetwork') {
            if (isset($_GET["start"])) {
                $start = $_GET["start"];
            } else {
                $start = 0;
            }

            if (!empty($_GET["order"])) {
                $table_options['order'] = $_GET["order"];
            } else {
                $table_options['order'] = 'ip';
            }

            $order_by_itemtype             = ($table_options['order'] == 'itemtype');

            $table_options['SQL_options']  = [
                'LIMIT'  => $_SESSION['glpilist_limit'],
                'START'  => $start
            ];

            $table           = new HTMLTableMain();
            $content         = "<a href='javascript:reloadTab(\"order=ip\");'>" .
                              self::getTypeName(Session::getPluralNumber()) . "</a>";
            $internet_column = $table->addHeader('IP Address', $content);
            $content         = sprintf(
                __('%1$s - %2$s'),
                _n('Item', 'Items', Session::getPluralNumber()),
                "<a href='javascript:reloadTab(\"order=itemtype\");'>" .
                __('Order by item type') . "</a>"
            );
            $item_column     = $table->addHeader('Item', $content);

            if ($order_by_itemtype) {
                foreach ($CFG_GLPI["networkport_types"] as $itemtype) {
                    $table_options['group_' . $itemtype] = $table->createGroup(
                        $itemtype,
                        $itemtype::getTypeName(Session::getPluralNumber())
                    );

                    self::getHTMLTableHeader(
                        $item->getType(),
                        $table_options['group_' . $itemtype],
                        $item_column,
                        null,
                        $table_options
                    );
                }
            }

            $table_options['group_None'] = $table->createGroup('Main', __('Other kind of items'));

            self::getHTMLTableHeader(
                $item->getType(),
                $table_options['group_None'],
                $item_column,
                null,
                $table_options
            );

            self::getHTMLTableCellsForItem(null, $item, null, $table_options);

            if ($table->getNumberOfRows() > 0) {
                 $count = self::countForItem($item);
                 Html::printAjaxPager(self::getTypeName(Session::getPluralNumber()), $start, $count);

                 Session::initNavigateListItems(
                     __CLASS__,
                     //TRANS : %1$s is the itemtype name,
                                           //        %2$s is the name of the item (used for headings of a list)
                                           sprintf(
                                               __('%1$s = %2$s'),
                                               $item->getTypeName(1),
                                               $item->getName()
                                           )
                 );
                 $table->display(['display_title_for_each_group' => $order_by_itemtype,
                     'display_super_for_each_group' => false,
                     'display_tfoot'                => false
                 ]);

                 Html::printAjaxPager(self::getTypeName(Session::getPluralNumber()), $start, $count);
            } else {
                echo "<table class='tab_cadre_fixe'>";
                echo "<tr><th>" . __('No IP address found') . "</th></tr>";
                echo "</table>";
            }
        }
    }


    public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0)
    {

        switch ($item->getType()) {
            case 'IPNetwork':
                self::showForItem($item, $withtemplate);
                break;
        }
        return true;
    }


    /**
     * @param $item      CommonDBTM object
     **/
    public static function countForItem(CommonDBTM $item)
    {
        global $DB;

        switch ($item->getType()) {
            case 'IPNetwork':
                $result = $DB->request([
                    'COUNT'  => 'cpt',
                    'FROM'   => 'glpi_ipaddresses_ipnetworks',
                    'WHERE'  => [
                        'ipnetworks_id'   => $item->getID()
                    ]
                ])->current();
                return $result['cpt'];
        }
    }


    /**
     * @param $item           CommonGLPI object
     * @param $withtemplate   (default 0)
     *
     * @return string
     **/
    public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0)
    {

        if (
            $item->getID()
            && $item->can($item->getField('id'), READ)
        ) {
            $nb = 0;
            if ($_SESSION['glpishow_count_on_tabs']) {
                $nb = self::countForItem($item);
            }
            return self::createTabEntry(self::getTypeName(Session::getPluralNumber()), $nb);
        }
        return '';
    }


   //////////////////////////////////////////////////////////////////////////////
   // IP address specific methods (check, transformation ...)
   //////////////////////////////////////////////////////////////////////////////


    /**
     * Disable the address
     **/
    public function disableAddress()
    {

        $this->version = '';
        $this->textual = '';
        $this->binary  = '';
    }


    /**
     * \brief Fill an array from the the local address object
     * Fill an array from the the local address object. Usefull for feeding $input variable for
     * preparing input to alter database.
     * If the field name is empty, then, the field is not set
     * If the object is not valid, then, version = 0, textual = "" and binary = (0, 0, 0, 0)
     *
     * @param array  $array         the array to Fill
     * @param string $versionField  the name of the key inside $array that contains de IP version number
     * @param string $textualField  the name of the key inside $array that contains de textual version
     * @param string $binaryField   the name of the key inside $array that contains de binary.
     *                              Each element of the array is post-fixed by _i, with i the index
     *
     * @return array the array altered
     **/
    public function setArrayFromAddress(array $array, $versionField, $textualField, $binaryField)
    {

        if (!empty($versionField)) {
            $version = $this->getVersion();
            if ($version !== false) {
                $array[$versionField] = $version;
            } else {
                $array[$versionField] = "0";
            }
        }

        if (!empty($textualField)) {
            $textual = $this->getTextual();
            if ($textual !== false) {
                $array[$textualField] = $textual;
            } else {
                $array[$textualField] = "";
            }
        }

        if (!empty($binaryField)) {
            $binary = $this->getBinary();
            for ($i = 0; $i < 4; ++$i) {
                if ($binary !== false) {
                    $array[$binaryField . "_" . $i] = $binary[$i];
                } else {
                    $array[$binaryField . "_" . $i] = '0';
                }
            }
        }
        return $array;
    }


    /**
     * \brief Fill the local address object from an array
     * Fill the local address object from an array. Usefull for reading $input
     *
     * @param array  $array         the array to Fill
     * @param string $versionField  the name of the key inside $array that contains de IP version number
     * @param string $textualField  the name of the key inside $array that contains de textual version
     * @param string $binaryField   the name of the key inside $array that contains de binary.
     *                              Each element of the array is post-fixed by _i, with i the index
     *
     * If the field name is empty, then, the field is not set
     *
     * @return true is succeffully defined
     **/
    public function setAddressFromArray(array $array, $versionField, $textualField, $binaryField)
    {

       // First, we empty the fields to notify that this address is not valid
        $this->disableAddress();

        if (!isset($array[$versionField])) {
            return false;
        }
        if (!isset($array[$textualField])) {
            return false;
        }
        if (
            (!isset($array[$binaryField . "_0"]) || !is_numeric($array[$binaryField . "_0"]))
            || (!isset($array[$binaryField . "_1"]) || !is_numeric($array[$binaryField . "_0"]))
            || (!isset($array[$binaryField . "_2"]) || !is_numeric($array[$binaryField . "_0"]))
            || (!isset($array[$binaryField . "_3"]) || !is_numeric($array[$binaryField . "_0"]))
        ) {
            return false;
        }

        $this->version    = $array[$versionField];
        $this->textual    = $array[$textualField];
        $this->binary     = [];
        $this->binary[0]  = ($array[$binaryField . "_0"] + 0);
        $this->binary[1]  = ($array[$binaryField . "_1"] + 0);
        $this->binary[2]  = ($array[$binaryField . "_2"] + 0);
        $this->binary[3]  = ($array[$binaryField . "_3"] + 0);
        return true;
    }


    /**
     * Check address validity
     **/
    public function is_valid()
    {
        return (($this->version != '') && ($this->textual != '') && ($this->binary != ''));
    }


    public function getVersion()
    {

        if ($this->version != '') {
            return $this->version;
        }
        return false;
    }


    public function is_ipv4()
    {
        return ($this->getVersion() == 4);
    }


    public function is_ipv6()
    {
        return ($this->getVersion() == 6);
    }


    public function getTextual()
    {

        if ($this->textual != '') {
            return $this->textual;
        }
        return false;
    }


    public function getBinary()
    {

        if ($this->binary != '') {
            return $this->binary;
        }
        return false;
    }


    /**
     * Transform an IPv4 address to IPv6
     *
     * @param integer|integer[] $address (bytes[4] or bytes) the address to transform.
     *
     * @return integer[]|false IPv6 mapped address
     **/
    public static function getIPv4ToIPv6Address($address)
    {

        if (is_numeric($address)) {
            return [0, 0, 0xffff, $address];
        }
        if ((is_array($address)) && (count($address) == 4)) {
            return self::getIPv4ToIPv6Address($address[3]);
        }
        return false;
    }


    /**
     * Check an address to see if it is IPv4 mapped to IPv6 address
     *
     * @param integer[] $address (bytes[4]) the address to check
     *
     * @return true if the address is IPv4 mapped to IPv6
     **/
    public static function isIPv4MappedToIPv6Address($address)
    {

        if (is_array($address) && (count($address) == 4)) {
            if (($address[0] == 0) && ($address[1] == 0) && ($address[2] == 0xffff)) {
                return true;
            }
            return false;
        }
        return false;
    }


    /**
     * Replace textual representation by its canonical form.
     *
     * @return void
     **/
    public function canonicalizeTextual()
    {
        $this->setAddressFromBinary($this->getBinary());
    }


    /**
     * \brief define an address from a string
     * Convert a textual address (string) to binary one. Opposite function that
     * setAddressFromBinary(). If item is valid ($itemtype not empty and $items_id > 0) then first
     * try to find it inside the database and load it from database.
     * \warning The resulting binary form is created inside the current object
     *
     * @param string  $address   textual (ie. human readable) address
     * @param string  $itemtype  type of the item this address has to be attached (default '')
     * @param integer $items_id  id of the item this address has to be attached (default -1)
     *
     * @return true if the address is valid.
     **/
    public function setAddressFromString($address, $itemtype = "", $items_id = -1)
    {
        global $DB;

        $this->disableAddress();

        if (!is_string($address)) {
            return false;
        }

        $address = trim($address);

        if (empty($address)) {
            return false;
        }

        if (
            !empty($itemtype)
            && ($items_id > 0)
        ) {
            $iterator = $DB->request([
                'SELECT' => 'id',
                'FROM'   => $this->getTable(),
                'WHERE'  => [
                    'items_id'  => $items_id,
                    'itemtype'  => $itemtype,
                    'name'      => $address
                ]
            ]);

            if (count($iterator) == 1) {
                $line = $iterator->current();
                if ($this->getFromDB($line["id"])) {
                    return true;
                }
            }
        }

       //if it IPV4 dotted-quoad format ::ffff:192.168.1.1
       //remove ::ffff: to manage only IPV4 part
       //keep in memory that have a special format
        $this->isDottedQuoadFormat = false;
        if (preg_match("/^::ffff:(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/", $address, $regs)) {
            $address = substr($address, 7);
            $this->isDottedQuoadFormat = true;
        }

        $binary = null;
        $singletons = explode(".", $address);
       // First, check to see if it is an IPv4 address
        if (count($singletons) == 4) {
            $binary = 0;
            foreach ($singletons as $singleton) {
                if (!is_numeric($singleton)) {
                    return false;
                }
                $singleton = intval($singleton);
                if (($singleton < 0) || ($singleton > 255)) {
                    return false;
                }
                $binary *= 256;
                $binary += intval($singleton);
            }
            $binary  = self::getIPv4ToIPv6Address($binary);
        }

       // Else, it should be an IPv6 address
        $singletons = explode(":", $address);
       // Minimum IPv6 address is "::". So, we check that there is at least 3 singletons in the array
       // And no more than 8 singletons
        if ((count($singletons) >= 3) && (count($singletons) <= 8)) {
            $empty_count = 0;
            foreach ($singletons as $singleton) {
                $singleton = trim($singleton);
               // First, we check that each singleton is 4 hexadecimal !
                if (!preg_match("/^[0-9A-Fa-f]{0,4}$/", $singleton, $regs)) {
                    return false;
                }
                if ($singleton === '') {
                    $empty_count++;
                }
            }

           // EXTREMITY CHECKS :
           // If it starts with colon : the second one must be empty too (ie.: :2001 is not valid)
            $start_with_empty = ($singletons[0] === '');
            if (($start_with_empty) && ($singletons[1] !== '')) {
                return false;
            }

           // If it ends with colon : the previous one must be empty too (ie.: 2001: is not valid)
            $end_with_empty = ($singletons[count($singletons) - 1] === '');
            if (($end_with_empty) && ($singletons[count($singletons) - 2] !== '')) {
                return false;
            }
           // END OF EXTREMITY CHECKS

           // The number of empty singletons depends on the type of contraction
            switch ($empty_count) {
                case 0: // No empty singleton => no contraction at all
                   // Thus, its side must be 8 !
                    if (count($singletons) != 8) {
                        return false;
                    }
                    break;

                case 1:
                   // One empty singleton : must be in the middle, otherwise EXTREMITY CHECKS
                   // would return false
                    break;

                case 2: // If there is two empty singletons then it must be at the beginning or the end
                    if (!($start_with_empty xor $end_with_empty)) {
                        return false;
                    }
                   // Thus remove one of both empty singletons.
                    if ($start_with_empty) {
                        unset($singletons[0]);
                    } else { // $end_with_empty == true
                        unset($singletons[count($singletons) - 1]);
                    }
                    break;

                case 3: // Only '::' allows three empty singletons ('::x::' = four empty singletons)
                    if (!($start_with_empty and $end_with_empty)) {
                        return false;
                    }
                   // Middle value must be '' otherwise EXTREMITY CHECKS returned an error
                    if (count($singletons) != 3) {
                        return false;
                    }
                    $singletons = [''];
                    break;

                default:
                    return false;
            }

           // Here, we are sure that $singletons are valids and only contains 1 empty singleton that
           // will be convert to as many '0' as necessary to reach 8 singletons

            $numberEmpty = 9 - count($singletons); // = 8 - (count($singletons) - 1)

            $epanded = [];
            foreach ($singletons as $singleton) {
                if ($singleton === '') {
                    $epanded = array_merge($epanded, array_fill(0, $numberEmpty, 0));
                } else {
                    $epanded[] = hexdec($singleton);
                }
            }

            $binary = [];
            for ($i = 0; $i < 4; $i++) {
                $binary[$i] = $epanded[2 * $i + 0] * 65536 + $epanded[2 * $i + 1];
            }
        }

       // $binary is an array that is only defined for IPv4 or IPv6 address
        if ($binary !== null && $binary !== false) {
           // Calling setAddressFromBinary is usefull to recheck one more time inside
           // glpi_ipaddresses table and to make canonical textual version
            return $this->setAddressFromBinary($binary, $itemtype, $items_id);
        }

       // Else, it is not IPv4 nor IPv6 address
        return false;
    }


    /**
     * \brief define an address from a binary
     * Convert a binary address (bytes[4]) to textual one. Opposite function that
     * setAddressFromString(). If item is valid ($itemtype not empty and $items_id > 0) then first
     * try to find it inside the database and load it from database. textual version is condensed
     * one (ie : 2001:db8:0:85a3\::ac1f:8001 rather than 2001:0db8:0000:85a3:0000:0000:ac1f:8001)
     * \warning The resulting binary form is created inside the current object
     *
     * @param integer[] $address   (bytes[4]) binary (ie. SQL requests) address
     * @param string    $itemtype  type of the item this address has to be attached (default '')
     * @param integer   $items_id  id of the item this address has to be attached (default -1)
     *
     * @return true if the address is valid.
     **/
    public function setAddressFromBinary($address, $itemtype = "", $items_id = -1)
    {
        global $DB;

        $this->disableAddress();
        if ((!is_array($address)) || (count($address) != 4)) {
            return false;
        }
        if (
            !empty($itemtype)
            && ($items_id > 0)
        ) {
            $where = [
                'itemtype'  => $itemtype,
                'items_id'  => $items_id
            ];

            for ($i = 0; $i < 4; ++$i) {
                $where["binary_$i"] = $address[$i];
            }

            $iterator = $DB->request([
                'SELECT' => 'id',
                'FROM'   => $this->getTable(),
                'WHERE'  => $where
            ]);

            if (count($iterator) == 1) {
                $line = $iterator->current();
                if ($this->getFromDB($line["id"])) {
                    return true;
                }
            }
        }
        $binary      = [];
        $textual     = [];
        $currentNull = "";
        foreach ($address as $singleton) {
            if (!is_numeric($singleton)) {
                return false;
            }
            $singleton = (int)$singleton;
            $binary[]  = $singleton;
            $singleton = str_pad(dechex($singleton), 8, "0", STR_PAD_LEFT);
            $elt       = ltrim(substr($singleton, 0, 4), "0");
            if (empty($elt)) {
                $textual[]    = "0";
                $currentNull .= "1";
            } else {
                $currentNull .= "0";
                $textual[]    = $elt;
            }
            $elt = ltrim(substr($singleton, 4, 4), "0");
            if (empty($elt)) {
                $textual[]    = "0";
                $currentNull .= "1";
            } else {
                $currentNull .= "0";
                $textual[]    = $elt;
            }
        }

        if (count($binary) == 4) {
            if (self::isIPv4MappedToIPv6Address($binary)) {
                $this->version = 4;
            } else {
                $this->version = 6;
            }
        } else {
            return false;
        }

        $this->binary = $binary;
        if ($this->getVersion() == 4) {
            $hexValue = str_pad($textual[6], 4, "0", STR_PAD_LEFT) . str_pad(
                $textual[7],
                4,
                "0",
                STR_PAD_LEFT
            );
            $textual  = [];
            for ($i = 0; $i < 4; $i++) {
                $textual[] = hexdec($hexValue[2 * $i + 0] . $hexValue[2 * $i + 1]);
            }
            $textual = implode('.', $textual);
        } else {
            foreach (["11111111", "1111111", "111111", "11111", "1111", "111", "11"] as $elt) {
                $pos = strpos($currentNull, $elt);
                if ($pos !== false) {
                    $first = array_slice($textual, 0, $pos);
                    if (count($first) == 0) {
                        $first = [""];
                    }
                    $second = array_slice($textual, $pos + strlen($elt));
                    if (count($second) == 0) {
                        $second = [""];
                    }
                    $textual = array_merge($first, [""], $second);
                    break;
                }
            }
            $textual = implode(':', $textual);
        }

        $prefix = "";

       //If it a special format, add prefix previsouly removed (to manage IPV4 part)
        if ($this->isDottedQuoadFormat) {
            $prefix = "::ffff:";
        }

        $this->textual = $prefix . $textual;
        return true;
    }


    /**
     * \brief add value to the address for iterator on addresses
     *
     * @param integer[] $address   (in and out) the address to increment or decrement
     * @param integer   $value     the value to add or remove. Must be betwwen -0xffffffff and +0xffffffff
     *
     * @return true if the increment is valid
     **/
    public static function addValueToAddress(&$address, $value)
    {

        if (
            !is_array($address)
            || (count($address) != 4)
            || !is_numeric($value)
            || ($value < -0xffffffff)
            || ($value > 0xffffffff)
        ) {
            return false;
        }

        for ($i = 3; $i >= 0; --$i) {
            $address[$i] += $value;
            if ($address[$i] < 0) {
                $address[$i] += (0x80000000 * 2);
                $value        = -1; // For next value for right to left ...
            } else if ($address[$i] > 0xffffffff) {
                $address[$i] -= (0x80000000 * 2);
                $value        = 1; // For next value for right to left ...
            } else {
                break;
            }
        }

        return true;
    }


    /**
     * \brief get absolute value of an integer
     * Convert a negative integer to positiv float. That is usefull as integer, in PHP are signed 32
     * bits values. As such, they are limited from +2 147 483 647 to ???2 147 483 648. Thus, when
     * working on integer with bit-wise boolean operations (&, |, ^, ~), the sign of the operand
     * remain inside the result. That make problem as IP address are only positiv ones.
     *
     * @param integer $value the integer that we want the absolute value
     *
     * @return float value that is the absolute of $value
     *
     **/
    public static function convertNegativeIntegerToPositiveFloat($value)
    {

        if (intval($value) && ($value < 0)) {
            $value = floatval($value) + floatval(0x80000000 * 2);
        }
        return $value;
    }

    /**
     * Search IP Addresses
     *
     * @param string $IPaddress  the address to search
     *
     * @return array  each value of the array (corresponding to one IPAddress) is an array of the
     *                items from the master item to the IPAddress
     **/
    public static function getItemsByIPAddress($IPaddress)
    {
        global $DB;

       // We must resolv binary address :
       //    1??) we don't know if the IP address is valid
       //    2??) we don't know its version
       //    3??) binary request is more efficient than textual one (polymorphism of IPv6 addresses)
        $address = new self();

        if (!$address->setAddressFromString($IPaddress)) {
            return [];
        }

        $criteria = [
            'SELECT' => 'gip.id',
            'FROM'   => 'glpi_ipaddresses AS gip',
            'WHERE'  => ['gip.version' => $address->version]
        ];
        $startIndex = (($address->version == 4) ? 3 : 1);
        $binaryIP = $address->getBinary();
        for ($i = $startIndex; $i < 4; ++$i) {
            $criteria['WHERE']["gip.binary_$i"] = $binaryIP[$i];
        }
        $iterator = $DB->request($criteria);
        $addressesWithItems = [];
        foreach ($iterator as $result) {
            if ($address->getFromDB($result['id'])) {
                $addressesWithItems[] = array_merge(
                    array_reverse($address->recursivelyGetItems()),
                    [clone $address]
                );
            }
        }
        return $addressesWithItems;
    }


    /**
     * Get an Object ID by its IP address (only if one result is found in the entity)
     *
     * @param string  $value   the ip address
     * @param integer $entity  the entity to look for
     *
     * @return array containing the object ID
     *         or an empty array is no value of serverals ID where found
     **/
    public static function getUniqueItemByIPAddress($value, $entity)
    {

        $addressesWithItems = self::getItemsByIPAddress($value);

       // Filter : Do not keep ip not linked to asset
        if (count($addressesWithItems)) {
            foreach ($addressesWithItems as $key => $tab) {
                if (
                    isset($tab[0])
                    && (($tab[0] instanceof NetworkName)
                    || ($tab[0] instanceof IPAddress)
                    || ($tab[0] instanceof NetworkPort)
                    || $tab[0]->isDeleted()
                    || $tab[0]->isTemplate()
                    || ($tab[0]->getEntityID() != $entity))
                ) {
                    unset($addressesWithItems[$key]);
                }
            }
        }

        if (count($addressesWithItems)) {
           // Get the first item that is matching entity
            foreach ($addressesWithItems as $items) {
                foreach ($items as $item) {
                    if ($item->getEntityID() == $entity) {
                        $result = ["id"       => $item->getID(),
                            "itemtype" => $item->getType()
                        ];
                        unset($addressesWithItems);
                        return $result;
                    }
                }
            }
        }
        return [];
    }


    /**
     * Check if two addresses are equals
     *
     * @param IPAddress|string|integer[] $ipaddress  the ip address to check with this
     *
     * @return boolean true if and only if both addresses are binary equals.
     **/
    public function equals($ipaddress)
    {

       // To normalise the address, just make new one
        $ipaddress = new self($ipaddress);

        if (
            !is_array($this->binary)
            || (count($this->binary) != 4)
            || (count($ipaddress->binary) != 4)
            || ($this->version != $ipaddress->version)
        ) {
            return false;
        }

        for ($index = 0; $index < 4; $index++) {
            if ($this->binary[$index] != $ipaddress->binary[$index]) {
                return false;
            }
        }

        return true;
    }


    /**
     * @param $itemtype
     * @param $base                  HTMLTableBase object
     * @param $super                 HTMLTableSuperHeader object (default NULL)
     * @param $father                HTMLTableHeader object (default NULL)
     * @param $options      array
     **/
    public static function getHTMLTableHeader(
        $itemtype,
        HTMLTableBase $base,
        HTMLTableSuperHeader $super = null,
        HTMLTableHeader $father = null,
        array $options = []
    ) {

        $column_name = __CLASS__;

        $content = self::getTypeName();

        if ($itemtype == 'IPNetwork') {
            $base->addHeader('Item', _n('Item', 'Items', 1), $super, $father);
            $base->addHeader('NetworkPort', NetworkPort::getTypeName(0), $super, $father);
            $base->addHeader('NetworkName', NetworkName::getTypeName(1), $super, $father);
            $base->addHeader('Entity', Entity::getTypeName(1), $super, $father);
        } else {
            if (isset($options['dont_display'][$column_name])) {
                return;
            }

            if (isset($options['column_links'][$column_name])) {
                $content = "<a href='" . $options['column_links'][$column_name] . "'>$content</a>";
            }

            $father = $base->addHeader($column_name, $content, $super, $father);

            if (isset($options['display_isDynamic']) && ($options['display_isDynamic'])) {
                $father = $base->addHeader(
                    $column_name . '_dynamic',
                    __('Automatic inventory'),
                    $super,
                    $father
                );
            }

            IPNetwork::getHTMLTableHeader(__CLASS__, $base, $super, $father, $options);
        }
    }


    /**
     * @param $row                HTMLTableRow object (default NULL)
     * @param $item               CommonDBTM object (default NULL)
     * @param $father             HTMLTableCell object (default NULL)
     * @param $options   array
     **/
    public static function getHTMLTableCellsForItem(
        HTMLTableRow $row = null,
        CommonDBTM $item = null,
        HTMLTableCell $father = null,
        array $options = []
    ) {
        global $DB, $CFG_GLPI;

        if (
            ($item !== null)
            && ($item->getType() == 'IPNetwork')
        ) {
            $queries = [];
            $main_criteria = [
                'SELECT'       => [
                    'ADDR.binary_0 AS binary_0',
                    'ADDR.binary_1 AS binary_1',
                    'ADDR.binary_2 AS binary_2',
                    'ADDR.binary_3 AS binary_3',
                    'ADDR.name AS ip',
                    'ADDR.id AS id',
                    'ADDR.itemtype AS addr_item_type',
                    'ADDR.items_id AS addr_item_id',
                    'glpi_entities.completename AS entity',
                ],
                'FROM'         => 'glpi_ipaddresses_ipnetworks AS LINK',
                'INNER JOIN'   => [
                    'glpi_ipaddresses AS ADDR' => [
                        'ON' => [
                            'ADDR'   => 'id',
                            'LINK'   => 'ipaddresses_id', [
                                'AND' => [
                                    'ADDR.itemtype' => 'NetworkName',
                                    'ADDR.is_deleted' => 0
                                ]
                            ]
                        ]
                    ]
                ],
                'LEFT JOIN'    => [
                    'glpi_entities'             => [
                        'ON' => [
                            'ADDR'            => 'entities_id',
                            'glpi_entities'   => 'id'
                        ]
                    ]
                ],
                'WHERE'        => [
                    'LINK.ipnetworks_id' => $item->getID(),
                ]
            ];

            foreach ($CFG_GLPI["networkport_types"] as $itemtype) {
                $table = getTableForItemType($itemtype);
                $criteria = $main_criteria;
                $criteria['SELECT'] = array_merge($criteria['SELECT'], [
                    'NAME.id AS name_id',
                    'PORT.id AS port_id',
                    'ITEM.id AS item_id',
                    new \QueryExpression("'$itemtype' AS " . $DB->quoteName('item_type'))
                ]);
                $criteria['INNER JOIN'] = $criteria['INNER JOIN'] + [
                    'glpi_networknames AS NAME'   => [
                        'ON' => [
                            'NAME'   => 'id',
                            'ADDR'   => 'items_id', [
                                'AND' => [
                                    'NAME.itemtype' => 'NetworkPort'
                                ]
                            ]
                        ]
                    ],
                    'glpi_networkports AS PORT'   => [
                        'ON' => [
                            'NAME'   => 'items_id',
                            'PORT'   => 'id', [
                                'AND' => [
                                    'PORT.itemtype' => $itemtype
                                ]
                            ]
                        ]
                    ],
                    "$table AS ITEM"              => [
                        'ON' => [
                            'ITEM'   => 'id',
                            'PORT'   => 'items_id'
                        ]
                    ]
                ];
                $queries[] = $criteria;
            }

            $criteria = $main_criteria;
            $criteria['SELECT'] = array_merge($criteria['SELECT'], [
                'NAME.id AS name_id',
                'PORT.id AS port_id',
                new \QueryExpression('NULL AS ' . $DB->quoteName('item_id')),
                new \QueryExpression("NULL AS " . $DB->quoteName('item_type')),
            ]);
            $criteria['INNER JOIN'] = $criteria['INNER JOIN'] + [
                'glpi_networknames AS NAME'   => [
                    'ON' => [
                        'NAME'   => 'id',
                        'ADDR'   => 'items_id', [
                            'AND' => [
                                'NAME.itemtype' => 'NetworkPort'
                            ]
                        ]
                    ]
                ],
                'glpi_networkports AS PORT'   => [
                    'ON' => [
                        'NAME'   => 'items_id',
                        'PORT'   => 'id', [
                            'AND' => [
                                'NOT' => [
                                    'PORT.itemtype' => $CFG_GLPI['networkport_types']
                                ]
                            ]
                        ]
                    ]
                ]
            ];
            $queries[] = $criteria;

            $criteria = $main_criteria;
            $criteria['SELECT'] = array_merge($criteria['SELECT'], [
                'NAME.id AS name_id',
                new \QueryExpression("NULL AS " . $DB->quoteName('port_id')),
                new \QueryExpression('NULL AS ' . $DB->quoteName('item_id')),
                new \QueryExpression("NULL AS " . $DB->quoteName('item_type'))
            ]);
            $criteria['INNER JOIN'] = $criteria['INNER JOIN'] + [
                'glpi_networknames AS NAME'   => [
                    'ON' => [
                        'NAME'   => 'id',
                        'ADDR'   => 'items_id', [
                            'AND' => [
                                'NAME.itemtype' => ['!=', 'NetworkPort']
                            ]
                        ]
                    ]
                ]
            ];
            $queries[] = $criteria;

            $criteria = $main_criteria;
            $criteria['SELECT'] = array_merge($criteria['SELECT'], [
                new \QueryExpression("NULL AS name_id"),
                new \QueryExpression("NULL AS port_id"),
                new \QueryExpression('NULL AS item_id'),
                new \QueryExpression("NULL AS item_type")
            ]);
            $criteria['INNER JOIN']['glpi_ipaddresses AS ADDR']['ON'][0]['AND']['ADDR.itemtype'] = ['!=', 'NetworkName'];
            $queries[] = $criteria;

            $union = new \QueryUnion($queries);
            $criteria = [
                'FROM'   => $union,
            ];

            if (
                ($options['order'] == 'ip')
                || ($options['order'] == 'itemtype')
            ) {
                $criteria['ORDERBY'] = [
                    'binary_0',
                    'binary_1',
                    'binary_2',
                    'binary_3'
                ];
            }

            if (isset($options['SQL_options'])) {
                $criteria = array_merge($criteria, $options['SQL_options']);
            }
            $iterator = $DB->request($criteria);

            $canedit              = (isset($options['canedit']) && $options['canedit']);
            $options['createRow'] = false;
            $address              = new self();

            $ipaddress   = new self();
            $networkname = new NetworkName();
            $networkport = new NetworkPort();

            $item = null;
            foreach ($iterator as $line) {
                unset($row);

                if (
                    ($options['order'] == 'itemtype')
                    && !empty($line['item_type'])
                ) {
                    $row = $options['group_' . $line['item_type']]->createRow();
                }

                if (!isset($row)) {
                    $row = $options['group_None']->createRow();
                }

                $ip_header  = $row->getGroup()->getSuperHeaderByName('IP Address');
                $item_header = $row->getGroup()->getHeaderByName('Item', 'Item');
                $port_header = $row->getGroup()->getHeaderByName('Item', 'NetworkPort');
                $name_header = $row->getGroup()->getHeaderByName('Item', 'NetworkName');
                $entity_header = $row->getGroup()->getHeaderByName('Item', 'Entity');

                $row->addCell($ip_header, $line['ip'], $father);

                if (!empty($line['name_id'])) {
                    $networkname->getFromDB($line['name_id']);
                    $row->addCell($name_header, $networkname->getLink(), $father);

                    if (!empty($line['port_id'])) {
                        $networkport->getFromDB($line['port_id']);
                        $row->addCell($port_header, $networkport->getLink(), $father);

                        if ((!empty($line['item_id'])) && (!empty($line['item_type']))) {
                             $itemtype = $line['item_type'];
                             $item     = new $itemtype();
                             $item->getFromDB($line['item_id']);
                             $row->addCell($item_header, $item->getLink(), $father);
                        }
                    }
                    $row->addCell($entity_header, $line['entity'], $father);
                } else if ((!empty($line['addr_item_id'])) && (!empty($line['addr_item_type']))) {
                    $itemtype = $line['addr_item_type'];
                    $item     = new $itemtype();
                    $item->getFromDB($line['addr_item_id']);
                    if ($item instanceof CommonDBChild) {
                        $items    = $item->recursivelyGetItems();
                        $elements = [$item->getLink()];
                        foreach ($items as $item_) {
                             $elements[] = $item_->getLink();
                        }
                        $row->addCell($item_header, implode(' > ', $elements), $father);
                    } else {
                        $row->addCell($item_header, $item->getLink(), $father);
                    }
                    $row->addCell($entity_header, $line['entity'], $father);
                }
            }
        } else {
            if (isset($options['dont_display']['IPAddress'])) {
                return;
            }

            $header = $row->getGroup()->getHeaderByName('Internet', __CLASS__);
            if (!$header) {
                return;
            }

            if (empty($item)) {
                if (empty($father)) {
                    return;
                }
                $item = $father->getItem();
            }

            $iterator = $DB->request([
                'SELECT' => 'id',
                'FROM'   => self::getTable(),
                'WHERE'  => [
                    'items_id'     => $item->getID(),
                    'itemtype'     => $item->getType(),
                    'is_deleted'   => 0
                ]
            ]);

            $canedit              = (isset($options['canedit']) && $options['canedit']);
            $createRow            = (isset($options['createRow']) && $options['createRow']);
            $options['createRow'] = false;
            $address              = new self();

            foreach ($iterator as $ipaddress) {
                if ($address->getFromDB($ipaddress['id'])) {
                    if ($createRow) {
                        $row = $row->createRow();
                    }

                    $content   = $address->fields['name'];
                    $this_cell = $row->addCell($header, $content, $father);

                    if (isset($options['display_isDynamic']) && ($options['display_isDynamic'])) {
                        $dyn_header = $row->getGroup()->getHeaderByName('Internet', __CLASS__ . '_dynamic');
                        $this_cell  = $row->addCell(
                            $dyn_header,
                            Dropdown::getYesNo($address->fields['is_dynamic']),
                            $this_cell
                        );
                    }

                    IPNetwork::getHTMLTableCellsForItem($row, $address, $this_cell, $options);
                }
            }
        }
    }
}
