<?php
/**
  * Ahead WebSoft Technologies Pvt Ltd.
  * @copyright  Copyright 2006-2026 Ahead WebSoft Technologies Pvt Ltd.
  * @license    https://www.socialengine.com/policies
*/
class Engine_IP
{
  // Constants

  const MASK = '::ffff:';



  // Static
  
  static protected $_preferIPv6 = false;

  static public function setPreferIPv6($flag = true)
  {
    self::$_preferIPv6 = (bool) $flag;
  }


  
  // Properties
  
  protected $_address;



  // General
  
  public function __construct($address = null)
  {
    if( null === $address ) {
      $address = self::getRealRemoteAddress();
    }
    $this->_address = self::normalizeAddress($address);
  }



  // Type
  
  public function isIPv4()
  {
    return ( false === strpos($this->_address, ':') );
  }

  public function isIPv6()
  {
    return ( false !== strpos($this->_address, ':') );
  }



  // Address

  public function isValid()
  {
    return ( false !== $this->_address );
  }
  
  public function toString()
  {
    return $this->_address;
  }

  public function toBinary()
  {
    return self::inet_pton($this->_address);
  }

  public function toLong()
  {
    if( $this->isIPv4() ) {
      return ip2long($this->_address);
    } else {
      return false;
    }
  }

  public function toHex()
  {
    return bin2hex($this->toBinary());
  }

  public function toIPv4()
  {
    return self::convertIPv6to4($this->_address);
  }

  public function toIPv6()
  {
    return self::convertIPv4to6($this->_address);
  }



  // Magic
  
  public function __toString()
  {
    return $this->toString();
  }



  // Static utility

  static public function normalizeAddress($address)
  {
    if( is_int($address) ) {
      // Probably an IPv4 address as an integer
      $address = long2ip($address);
    } else if( !is_string($address) ) {
      // Not a string?
      return false;
    } else if( preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', $address) ) {
      // An IPv4 address as a string
      //$address = $address;
    } else if( strlen($address) == 4 ) {
      // This is probably a binary IPv4 address created using inet_pton
      $address = self::inet_ntop($address);
    } else if( strlen($address) == 16 ) {
      // This is probably a binary IPv6 address created using inet_pton
      $address = self::inet_ntop($address);
    } else if( is_numeric($address) && false !== ($tmp = long2ip($address)) ) {
      // An IPv4 address created using ip2long but stored as a string?
      $address = $tmp;
    } else if( false !== ($tmp = self::inet_pton($address)) ) {
      // This is simply checking if running the IP through inet_pton does not
      // return false. This should validate if it's a proper address
      //$address = $address;
    } else {
      return false;
    }

    // Convert if necessary
    if( self::$_preferIPv6 ) {
      if( false !== ($tmp = self::convertIPv4to6($address)) ) {
        $address = $tmp;
      }
    } else {
      if( false !== ($tmp = self::convertIPv6to4($address)) ) {
        $address = $tmp;
      }
    }

    return $address;
  }

  static public function normalizeAddressToBinary($address)
  {
    $address = self::normalizeAddress($address);
    if( $address ) {
      $address = self::inet_pton($address);
    }
    return $address;
  }

  static public function inet_pton($address)
  {
    if( function_exists('inet_pton') && !empty($address) ) {
      return inet_pton($address);
    } else {
      // Compat
      if( false !== strpos($address, ':') ) {
        // IPv6
        $ip = explode(':', $address);
        $res = str_pad('', (4 * (8 - count($ip))), '0000', STR_PAD_LEFT);
        foreach( $ip as $seg ) {
            $res .= str_pad($seg, 4, '0', STR_PAD_LEFT);
        }
        return pack('H' . strlen($res), $res);
      } else if( false !== strpos($address, '.') ) {
        // IPv4
        return pack('N', ip2long($address));
      } else {
        // Unknown
        return false;
      }
    }
  }

  /**
   * This function converts a 32bit IPv4, or 128bit IPv6 address into an 
   * address family appropriate string representation. Currently wraps the 
   * PHP inet_ntop function, which means PHP 5.3 is required for Windows
   * users. This function was not available for PHP versions prior to 5.3
   * on Windows.
   *
   * @param string $address 
   * @return mixed
   */
  static public function inet_ntop($address)
  {
    if( function_exists('inet_ntop') ) {
      return inet_ntop($address);
    } else {
      // Compat
      if( strlen($address) == 16 ) {
        // IPv6
        // @TODO
      } else if( strlen($address) == 4 ) {
        // IPv4
        // @TODO
      } else {
        return false;
      }
    }
  }

  static public function expandIPv6Notation($address, $padGroups = true)
  {
    // From: http://stackoverflow.com/questions/444966/working-with-ipv6-addresses-in-php
    if( false !== strpos($address, '::') ) {
      $address = str_replace('::', str_repeat(':0', 8 - substr_count($address, ':')) . ':', $address);
    }
    if( 0 === strpos($address, ':') ) {
      $address = '0' . $address;
    }
    if( $padGroups ) {
      $parts = explode(':', $address);
      foreach( $parts as &$part ) {
        if( strlen($part) < 4 ) {
          $part = str_pad($part, 4, 0, STR_PAD_LEFT);
        }
      }
      $address = join(':', $parts);
    }
    return $address;
  }

  static public function convertIPv4to6($address)
  {
    // From: http://stackoverflow.com/questions/444966/working-with-ipv6-addresses-in-php
    $IPv6 = ( 0 === strpos($address, '::') );
    $IPv4 = ( 0 < strpos($address, '.') );

    if( !$IPv4 && !$IPv6 ) {
      return false;
    } else if( $IPv6 && $IPv4 ) {
      // Strip IPv4 Compatibility notation
      $address = substr($address, strrpos($address, ':') + 1); 
    } else if( !$IPv4 ) {
      return $address; // Seems to be IPv6 already?
    }
    $address = array_pad(explode('.', $address), 4, 0);
    if( count($address) > 4 ) {
      return false;
    }
    for( $i = 0; $i < 4; $i++ ) {
      if( $address[$i] > 255 ) {
        return false;
      }
    }

    $Part7 = base_convert(($address[0] * 256) + $address[1], 10, 16);
    $Part8 = base_convert(($address[2] * 256) + $address[3], 10, 16);
    return self::MASK . $Part7 . ':' . $Part8;
  }

  static public function convertIPv6to4($address)
  {
    $IPv6 = ( 0 === strpos($address, '::') );
    $IPv4 = ( 0 < strpos($address, '.') );

    if( $IPv4 && !$IPv6 ) {
      // Already IPv4?
      return $address;
    } else if( $IPv6 && self::MASK == substr($address, 0, strlen(self::MASK)) ) {
      // Strip mask
      $address = substr($address, strlen(self::MASK));
      // If in hex format, convert to IPv4 format
      if( false !== strpos($address, ':') ) {
        list($part7, $part8) = explode(':', $address);
        $part7 = hexdec($part7);
        $part8 = hexdec($part8);
        $address = sprintf('%d.%d.%d.%d',
          floor($part7 / 256),
          ($part7 % 256),
          floor($part8 / 256),
          ($part8 % 256)
        );
      }
    } else {
      return false;
    }
  }

  static public function getRealRemoteAddress($asIPv6 = null)
  {
    // From: http://stackoverflow.com/questions/444966/working-with-ipv6-addresses-in-php
    if( !empty($_SERVER['HTTP_CLIENT_IP']) ) {
        $address = $_SERVER['HTTP_CLIENT_IP'];
    } else if( !empty($_SERVER['HTTP_X_FORWARDED_FOR']) ) {
        $address = $_SERVER['HTTP_X_FORWARDED_FOR'];
    } else if( !empty($_SERVER['REMOTE_ADDR']) ) {
        $address = $_SERVER['REMOTE_ADDR'];
    }
    if( false !== ($pos = strpos($address, ',')) ) {
      $address = substr($address, 0, $pos - 1);
    }

    if( !filter_var($address, FILTER_VALIDATE_IP) ) {
      return;
    }

    if( null === $asIPv6 ) {
      $asIPv6 = self::$_preferIPv6;
    }

    if( $asIPv6 ) {
      return self::convertIPv4to6($address);
    } else {
      return $address;
    }
  }
}
