You are here

IpAddress.php in IP address fields 2.0.x

Same filename and directory in other branches
  1. 8 src/IpAddress.php

File

src/IpAddress.php
View source
<?php

namespace Drupal\field_ipaddress;


/**
 * IpTools class.
 */
class IpAddress {
  const IP_FAMILY_4 = 4;
  const IP_FAMILY_6 = 6;
  const IP_FAMILY_ALL = 10;
  const IP_RANGE_SIMPLE = 2;
  const IP_RANGE_CIDR = 3;
  const IP_RANGE_NONE = 0;
  protected $family = NULL;
  protected $type = NULL;
  protected $start = NULL;
  protected $end = NULL;
  protected $raw = NULL;

  /**
   * Getter for $this->family.
   */
  public function family() {
    return $this->family;
  }

  /**
   * Getter for $this->type.
   */
  public function type() {
    return $this->type;
  }

  /**
   * Getter for $this->start.
   */
  public function start() {
    return $this->start;
  }

  /**
   * Getter for $this->end.
   */
  public function end() {
    return $this->end;
  }

  /**
   * On construction, parse the given value.
   */
  public function __construct($value) {
    $this->raw = $value;
    $result = $this
      ->parse($value);
    if ($result === FALSE) {
      $this->family = NULL;
      $this->type = NULL;
      $this->start = NULL;
      $this->end = NULL;
      throw new \Exception('Invalid value.');
    }
  }

  /**
   * Checks if the stored IP is within $min and $max IPs.
   */
  public function inRange($min, $max) {
    if (!$this
      ->isIpAddress($min) || !$this
      ->isIpAddress($max)) {
      throw new \Exception('Invalid value.');
    }

    // IPs in different families are by default not within range.
    if ($this
      ->getFamily($min) != $this->family || $this
      ->getFamily($max) != $this->family) {
      return FALSE;
    }
    if ($this->family == self::IP_FAMILY_4) {
      return $this
        ->inRange4($min, $max);
    }
    else {
      return $this
        ->inRange6($min, $max);
    }
  }

  /**
   * Checks if the given IP is valid.
   */
  private function isIpAddress($ip) {
    return filter_var($ip, FILTER_VALIDATE_IP);
  }

  /**
   * Checks if the given IP is valid IPv6.
   */
  private function isIpV6($ip) {
    return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
  }

  /**
   * Checks if the given IP is valid IPv4.
   */
  private function isIpV4($ip) {
    return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
  }

  /**
   * Find if the IP family is IPv4 or IPv6.
   */
  private function getFamily($ip) {
    if ($this
      ->isIpV4($ip)) {
      return self::IP_FAMILY_4;
    }
    return self::IP_FAMILY_6;
  }

  /**
   * Checks that zero or one range delimiter exists in a raw value.
   */
  private function checkSingleDelimiter($value) {
    $count = 0;
    $delimiters = [
      '/',
      '*',
      '-',
    ];
    foreach ($delimiters as $delimiter) {
      if (strpos($value, $delimiter) !== FALSE) {
        $count++;
      }
    }
    if ($count > 1) {
      throw new \Exception('Cannot combine range delimiters, only one of - / * must be present.');
    }
  }

  /**
   * Find if the value given is an IP, IP range, or other.
   */
  private function parse($value) {
    $value = trim(str_replace(' ', '', $value));

    // Check that we have zero or one range delimiter.
    $this
      ->checkSingleDelimiter($value);

    // Check if this is a simple range.
    if (strpos($value, '-') !== FALSE) {

      // Break its parts apart.
      list($start, $end) = explode('-', $value, 2);
      if (!$this
        ->isIpAddress($start)) {

        // Return false on failure.
        return FALSE;
      }
      if ($this
        ->isIpV4($start)) {
        $this->family = self::IP_FAMILY_4;

        // End IP may be partial
        if (strpos($end, '.') === FALSE) {

          // Break $start into octets, and replace the last one.
          $end_parts = explode('.', $start);
          if (count($end_parts) != 4) {
            throw new \Exception('Invalid IP range format.');
          }
          $end_parts[3] = $end;
          $end = implode('.', $end_parts);
          if (!$this
            ->isIpAddress($end)) {
            throw new \Exception('Invalid IP range format.');
          }
        }
        else {

          // Should be a simple IP for the end as well.
          if (!$this
            ->isIpAddress($end)) {
            throw new \Exception('Invalid end of range IP.');
          }
        }
      }
      else {
        $this->family = self::IP_FAMILY_6;
        if (!$this
          ->isIpAddress($end)) {
          throw new \Exception('Invalid end of range IP.');
        }
      }
      if ($this
        ->getFamily($start) !== $this
        ->getFamily($end)) {
        throw new \Exception('Mismatching IP families in range.');
      }

      // Simple is ... simple, assign the bounds.
      $this->start = $start;
      $this->end = $end;
      $this->type = self::IP_RANGE_SIMPLE;
    }
    elseif (strpos($value, '*') !== FALSE) {
      list($prefix, $suffix) = explode('*', $value, 2);

      // Replace wildcard with lower end, which is 0 in both IPv4 and IPv6.
      $start = $prefix . '0' . $suffix;

      // And check this is a valid IP.
      if (!$this
        ->isIpAddress($start)) {
        throw new \Exception('Invalid IP address.');
      }
      if ($this
        ->isIpV4($start)) {
        $this->family = self::IP_FAMILY_4;
        $end = $prefix . '255' . $suffix;
      }
      if ($this
        ->isIpV6($start)) {
        $this->family = self::IP_FAMILY_6;
        $end = $prefix . 'ff' . $suffix;
      }
      if (!$this
        ->isIpAddress($end)) {
        throw new \Exception('Invalid IP address.');
      }
      $this->start = $start;
      $this->end = $end;
      $this->type = self::IP_RANGE_SIMPLE;
    }
    elseif (strpos($value, '/') !== FALSE) {

      // Break its parts apart.
      list($ip, $prefix) = explode('/', $value, 2);
      if (!$this
        ->isIpAddress($ip) || !is_numeric($prefix) || $prefix <= 0) {
        return FALSE;
      }
      $this->family = $this
        ->getFamily($ip);

      // Check that the prefix is not larger than address.
      if ($this->family == self::IP_FAMILY_4 && $prefix > 32 || $this->family == self::IP_FAMILY_6 && $prefix > 128) {
        return FALSE;
      }
      $this->type = self::IP_RANGE_CIDR;

      // Calculate CIDR address bounds.
      if ($this->family == self::IP_FAMILY_4) {
        $this
          ->calcCidr4($ip, $prefix);
      }
      else {
        $this
          ->calcCidr6($ip, $prefix);
      }
    }
    elseif ($this
      ->isIpAddress($value)) {
      $this->type = self::IP_RANGE_NONE;
      $this->family = $this
        ->getFamily($value);
      $this->start = $value;
      $this->end = $value;
    }
    else {
      return FALSE;
    }
  }

  /**
   * Calculates the IP range for an IPv4 CIDR formatted range.
   *
   * @see https://stackoverflow.com/questions/15961557/calculate-ip-range-using-php-and-cidr#answer-55229198
   */
  private function calcCidr4($ip, $prefix) {
    $this->start = long2ip(ip2long($ip) & -1 << 32 - (int) $prefix);
    $this->end = long2ip(ip2long($this->start) + pow(2, 32 - (int) $prefix) - 1);
  }

  /**
   * Calculates the IP range for an IPv6 CIDR formatted range.
   *
   * @see https://stackoverflow.com/questions/10085266/php5-calculate-ipv6-range-from-cidr-prefix#answer-10086404
   */
  private function calcCidr6($ip, $prefix) {
    $start_bin = $this
      ->packIp6($ip);
    $this->start = inet_ntop($start_bin);

    // Convert the binary string to a string with hexadecimal characters.
    $start_hex = reset(unpack('H*', $start_bin));

    // Calculate flexible bits.
    $flexbits = 128 - $prefix;
    $end_hex = $start_hex;
    $pos = 31;
    while ($flexbits > 0) {

      // Get the character at this position.
      $orig = substr($end_hex, $pos, 1);

      // Convert it to an integer.
      $origval = hexdec($orig);

      // OR it with (2^flexbits)-1, with flexbits limited to 4 at a time.
      $newval = $origval | pow(2, min(4, $flexbits)) - 1;

      // Convert it back to a hexadecimal character.
      $new = dechex($newval);

      // And put that character back in the string.
      $end_hex = substr_replace($end_hex, $new, $pos, 1);

      // We processed one nibble, move to previous position.
      $flexbits -= 4;
      $pos -= 1;
    }
    $end_bin = pack('H*', $end_hex);

    // And create an IPv6 address from the binary string.
    $this->end = inet_ntop($end_bin);
  }

  /**
   * Strips leading zeros and converts an IPv4 to its binary representation.
   */
  private function packIp4($ip) {
    return inet_pton(preg_replace('/\\b0+(?=\\d)/', '', $ip));
  }

  /**
   * Converts an IPv6 to its binary representation.
   */
  private function packIp6($ip) {
    return inet_pton($ip);
  }

  /**
   * Checks if the current IPv4 is within a given min and max IP.
   *
   * @see https://stackoverflow.com/questions/18336908/php-check-if-ip-address-is-in-a-range-of-ip-addresses/18336909#answer-18336909
   */
  private function inRange4($min, $max) {
    $min_long = ip2long($min);
    $max_long = ip2long($max);
    if ($this->type == self::IP_RANGE_NONE) {
      $start_long = $end_long = ip2long($this->start);
    }
    else {
      $start_long = ip2long($this->start);
      $end_long = ip2long($this->end);
    }
    return $start_long >= $min_long && $start_long <= $max_long && ($end_long >= $min_long && $end_long <= $max_long);
  }

  /**
   * Checks if the current IPv6 is within a given min and max IP.
   */
  private function inRange6($min, $max) {
    $min_bin = inet_pton($min);
    $max_bin = inet_pton($max);
    if ($this->type == self::IP_RANGE_NONE) {
      $start_bin = $end_bin = inet_pton($this->start);
    }
    else {
      $start_bin = inet_pton($this->start);
      $end_bin = inet_pton($this->end);
    }
    return $start_bin >= $min_bin && $start_bin <= $max_bin && ($end_bin >= $min_bin && $end_bin <= $max_bin);
  }

}

Classes

Namesort descending Description
IpAddress IpTools class.