Subversion Repositories php_utils

Rev

Rev 24 | Blame | Compare with Previous | Last modification | View Log | RSS feed

  1. <?php
  2.  
  3. /*
  4.  * OID-Utilities for PHP
  5.  * Copyright 2011 - 2023 Daniel Marschall, ViaThinkSoft
  6.  * Version 2023-08-25
  7.  *
  8.  * Licensed under the Apache License, Version 2.0 (the "License");
  9.  * you may not use this file except in compliance with the License.
  10.  * You may obtain a copy of the License at
  11.  *
  12.  *     http://www.apache.org/licenses/LICENSE-2.0
  13.  *
  14.  * Unless required by applicable law or agreed to in writing, software
  15.  * distributed under the License is distributed on an "AS IS" BASIS,
  16.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17.  * See the License for the specific language governing permissions and
  18.  * limitations under the License.
  19.  */
  20.  
  21. // All functions in this library are compatible with leading zeroes (not recommended) and leading dots
  22.  
  23. // TODO: change some function names, so that they have a uniform naming schema, and rename "oid identifier" into "ASN.1 alphanumeric identifier"
  24. //       oid_id_is_valid() => asn1_alpha_id_valid()
  25.  
  26. define('OID_DOT_FORBIDDEN', 0);
  27. define('OID_DOT_OPTIONAL',  1);
  28. define('OID_DOT_REQUIRED',  2);
  29.  
  30. /**
  31.  * Checks if an OID has a valid dot notation.
  32.  * @author  Daniel Marschall, ViaThinkSoft
  33.  * @version 2014-12-09
  34.  * @param   string $oid<br/>
  35.  *              An OID in dot notation.
  36.  * @param   boolean $allow_leading_zeroes<br/>
  37.  *              true of leading zeroes are allowed or not.
  38.  * @param   boolean $allow_leading_dot<br/>
  39.  *              true of leading dots are allowed or not.
  40.  * @return  boolean true if the dot notation is valid.
  41.  **/
  42. function oid_valid_dotnotation($oid, $allow_leading_zeroes=true, $allow_leading_dot=false, $min_len=0) {
  43.         $regex = oid_validation_regex($allow_leading_zeroes, $allow_leading_dot, $min_len);
  44.  
  45.         $m = array();
  46.         return preg_match($regex, $oid, $m) ? true : false;
  47. }
  48.  
  49. /**
  50.  * Returns a full regular expression to validate an OID in dot-notation
  51.  * @author  Daniel Marschall, ViaThinkSoft
  52.  * @version 2014-12-09
  53.  * @param   boolean $allow_leading_zeroes<br/>
  54.  *              true of leading zeroes are allowed or not.
  55.  * @param   boolean $allow_leading_dot<br/>
  56.  *              true of leading dots are allowed or not.
  57.  * @return  string The regular expression
  58.  **/
  59. function oid_validation_regex($allow_leading_zeroes=true, $allow_leading_dot=false, $min_len=0) {
  60.         $leading_dot_policy = $allow_leading_dot ? OID_DOT_OPTIONAL : OID_DOT_FORBIDDEN;
  61.  
  62.         $part_regex = oid_part_regex($min_len, $allow_leading_zeroes, $leading_dot_policy);
  63.  
  64.         return '@^'.$part_regex.'$@';
  65. }
  66.  
  67. /**
  68.  * Returns a partial regular expression which matches valid OIDs in dot notation.
  69.  * It can be inserted into regular expressions.
  70.  * @author  Daniel Marschall, ViaThinkSoft
  71.  * @version 2014-12-09
  72.  * @param   int $min_len<br/>
  73.  *              0="." and greater will be recognized, but not ""<br/>
  74.  *              1=".2" and greater will be recognized<br/>
  75.  *              2=".2.999" and greater will be recognized (default)<br/>
  76.  *              etc.
  77.  * @param   boolean $allow_leading_zeroes<br/>
  78.  *              true:  ".2.0999" will be recognized<br/>
  79.  *              false: ".2.0999" won't be recognized (default)
  80.  * @param   int $leading_dot_policy<br/>
  81.  *              0 (OID_DOT_FORBIDDEN): forbidden<br/>
  82.  *              1 (OID_DOT_OPTIONAL) : optional (default)<br/>
  83.  *              2 (OID_DOT_REQUIRED) : enforced
  84.  * @return  string|false A regular expression which matches OIDs in dot notation
  85.  **/
  86. function oid_part_regex($min_len=2, $allow_leading_zeroes=false, $leading_dot_policy=OID_DOT_OPTIONAL) {
  87.         switch ($leading_dot_policy) {
  88.                 case 0: // forbidden
  89.                         $lead_dot = '';
  90.                         break;
  91.                 case 1: // optional
  92.                         $lead_dot = '\\.{0,1}';
  93.                         break;
  94.                 case 2: // enforced
  95.                         $lead_dot = '\\.';
  96.                         break;
  97.                 default:
  98.                         assert(false);
  99.                         return false;
  100.         }
  101.  
  102.         $lead_zero            = $allow_leading_zeroes ? '0*' : '';
  103.         $zero_till_thirtynine = '(([0-9])|([1-3][0-9]))'; // second arc is limited to 0..39 if root arc is 0..1
  104.         $singledot_option     = ($min_len == 0) && ($leading_dot_policy != OID_DOT_FORBIDDEN) ? '|\\.' : '';
  105.         $only_root_option     = ($min_len <= 1) ? '|('.$lead_dot.$lead_zero.'[0-2])' : '';
  106.  
  107.         $regex = '
  108.         (
  109.                 (
  110.                         (
  111.                                 ('.$lead_dot.$lead_zero.'[0-1])
  112.                                 \\.'.$lead_zero.$zero_till_thirtynine.'
  113.                                 (\\.'.$lead_zero.'(0|[1-9][0-9]*)){'.max(0, $min_len-2).',}
  114.                         )|(
  115.                                 ('.$lead_dot.$lead_zero.'[2])
  116.                                 (\\.'.$lead_zero.'(0|[1-9][0-9]*)){'.max(0, $min_len-1).',}
  117.                         )
  118.                         '.$only_root_option.'
  119.                         '.$singledot_option.'
  120.                 )
  121.         )';
  122.  
  123.         // Remove the indentations which are used to maintain this large regular expression in a human friendly way
  124.         $regex = str_replace("\n", '', $regex);
  125.         $regex = str_replace("\r", '', $regex);
  126.         $regex = str_replace("\t", '', $regex);
  127.         $regex = str_replace(' ',  '', $regex);
  128.  
  129.         return $regex;
  130. }
  131.  
  132. /**
  133.  * Searches all OIDs in $text and outputs them as array.
  134.  * @author  Daniel Marschall, ViaThinkSoft
  135.  * @version 2014-12-09
  136.  * @param   string $text<br/>
  137.  *              The text to be parsed
  138.  * @param   int $min_len<br/>
  139.  *              0="." and greater will be recognized, but not ""<br/>
  140.  *              1=".2" and greater will be recognized<br/>
  141.  *              2=".2.999" and greater will be recognized (default)<br/>
  142.  *              etc.
  143.  * @param   boolean $allow_leading_zeroes<br/>
  144.  *              true:  ".2.0999" will be recognized<br/>
  145.  *              false: ".2.0999" won't be recognized (default)
  146.  * @param   int $leading_dot_policy<br/>
  147.  *              0 (OID_DOT_FORBIDDEN): forbidden<br/>
  148.  *              1 (OID_DOT_OPTIONAL) : optional (default)<br/>
  149.  *              2 (OID_DOT_REQUIRED) : enforced
  150.  * @param   boolean $requires_whitespace_delimiters<br/>
  151.  *              true:  "2.999" will be recognized, as well as " 2.999 " (default)<br/>
  152.  *              false: "2.999!" will be reconigzed, as well as "2.999.c" (this might be used in in documentations with templates)
  153.  * @return  string[] An array of OIDs in dot notation
  154.  **/
  155. function parse_oids($text, $min_len=2, $allow_leading_zeroes=false, $leading_dot_policy=OID_DOT_OPTIONAL, $requires_whitespace_delimiters=true) {
  156.         $regex = oid_detection_regex($min_len, $allow_leading_zeroes, $leading_dot_policy, $requires_whitespace_delimiters);
  157.  
  158.         $matches = array();
  159.         preg_match_all($regex, $text, $matches);
  160.         return $matches[1];
  161. }
  162.  
  163. /**
  164.  * Returns a full regular expression for detecting OIDs in dot notation inside a text.
  165.  * @author  Daniel Marschall, ViaThinkSoft
  166.  * @version 2014-12-09
  167.  * @param   int $min_len<br/>
  168.  *              0="." and greater will be recognized, but not ""<br/>
  169.  *              1=".2" and greater will be recognized<br/>
  170.  *              2=".2.999" and greater will be recognized (default)<br/>
  171.  *              etc.
  172.  * @param   boolean $allow_leading_zeroes<br/>
  173.  *              true:  ".2.0999" will be recognized<br/>
  174.  *              false: ".2.0999" won't be recognized (default)
  175.  * @param   int $leading_dot_policy<br/>
  176.  *              0 (OID_DOT_FORBIDDEN): forbidden<br/>
  177.  *              1 (OID_DOT_OPTIONAL) : optional (default)<br/>
  178.  *              2 (OID_DOT_REQUIRED) : enforced
  179.  * @param   boolean $requires_whitespace_delimiters<br/>
  180.  *              true:  "2.999" will be recognized, as well as " 2.999 " (default)<br/>
  181.  *              false: "2.999!" will be reconigzed, as well as "2.999.c" (this might be used in in documentations with templates)
  182.  * @return  string The regular expression
  183.  **/
  184. function oid_detection_regex($min_len=2, $allow_leading_zeroes=false, $leading_dot_policy=OID_DOT_OPTIONAL, $requires_whitespace_delimiters=true) {
  185.         if ($requires_whitespace_delimiters) {
  186.                 // A fully qualified regular expression which can be used by preg_match()
  187.                 $begin_condition = '(?<=^|\\s)';
  188.                 $end_condition   = '(?=\\s|$)';
  189.         } else {
  190.                 // A partial expression which can be used inside another regular expression
  191.                 $begin_condition = '(?<![\d])';
  192.                 $end_condition   = '(?![\d])';
  193.         }
  194.  
  195.         $part_regex = oid_part_regex($min_len, $allow_leading_zeroes, $leading_dot_policy);
  196.  
  197.         return '@'.$begin_condition.$part_regex.$end_condition.'@';
  198. }
  199.  
  200. /**
  201.  * Returns the parent of an OID in dot notation or the OID itself, if it is the root.<br/>
  202.  * Leading dots and leading zeroes are tolerated.
  203.  * @author  Daniel Marschall, ViaThinkSoft
  204.  * @version 2014-12-16
  205.  * @param   string $oid<br/>
  206.  *              An OID in dot notation.
  207.  * @return  string|false The parent OID in dot notation.
  208.  **/
  209. function oid_up($oid) {
  210.         $oid = sanitizeOID($oid, 'auto');
  211.         if ($oid === false) return false;
  212.  
  213.         $p = strrpos($oid, '.');
  214.         if ($p === false) return $oid;
  215.         if ($p == 0) return '.';
  216.  
  217.         return substr($oid, 0, $p);
  218. }
  219.  
  220. /**
  221.  * Outputs the depth of an OID.
  222.  * @author  Daniel Marschall, ViaThinkSoft
  223.  * @version 2014-12-09
  224.  * @param   string $oid An OID in dot notation (with or without leading dot)
  225.  * @return  int The depth of the OID, e.g. 2.999 and .2.999 has the length 2.
  226.  **/
  227. function oid_len($oid) {
  228.         if ($oid == '') return 0;
  229.         if (substr($oid,0,1) == '.') $oid = substr($oid, 1);
  230.         return substr_count($oid, '.')+1;
  231. }
  232. function oid_depth($oid) {
  233.         return oid_len($oid);
  234. }
  235.  
  236. /**
  237.  * Lists all parents of an OID.
  238.  * This function tolerates leading dots. The parent of '.' stays '.'.
  239.  * The OID will not be checked for validity!
  240.  * @author  Daniel Marschall, ViaThinkSoft
  241.  * @version 2014-12-17
  242.  * @param   string $oid<br/>
  243.  *              An OID in dot notation.
  244.  * @return  string[] An array with all parent OIDs.
  245.  **/
  246. function oid_parents($oid) {
  247.         $parents = array();
  248.  
  249.         while (oid_len($oid) > 1) {
  250.                 $oid = oid_up($oid);
  251.                 $parents[] = $oid;
  252.         }
  253.  
  254.         if (substr($oid, 0, 1) == '.') $parents[] = '.';
  255.  
  256.         return $parents;
  257. }
  258.  
  259. /*
  260. assert(oid_parents('.1.2.999') == array('.1.2', '.1', '.'));
  261. assert(oid_parents('1.2.999') == array('1.2', '1'));
  262. assert(oid_parents('.') == array('.'));
  263. assert(oid_parents('') == array());
  264. */
  265.  
  266. /**
  267.  * Sorts an array containing OIDs in dot notation.
  268.  * @author  Daniel Marschall, ViaThinkSoft
  269.  * @version 2014-12-09
  270.  * @param   string[] $ary<br/>
  271.  *              An array of OIDs in dot notation.<br/>
  272.  *              This array will be changed by this method.
  273.  * @param   boolean $output_with_leading_dot<br/>
  274.  *              true: The array will be normalized to OIDs with a leading dot.
  275.  *              false: The array will be normalized to OIDs without a leading dot. (default)
  276.  **/
  277. function oidSort(&$ary, $output_with_leading_dot=false) {
  278.         $out = array();
  279.  
  280.         $none = $output_with_leading_dot ? '.' : '';
  281.  
  282.         $d = array();
  283.         $oid = null;
  284.         foreach ($ary as &$oid) {
  285.                 if (($oid == '') || ($oid == '.')) {
  286.                         $out[] = $none;
  287.                 } else {
  288.                         $oid = sanitizeOID($oid, 'auto'); // strike leading zeroes
  289.                         $bry = explode('.', $oid, 2);
  290.                         $firstarc = $bry[0];
  291.                         $rest     = (isset($bry[1])) ? $bry[1] : '';
  292.                         $d[$firstarc][] = $rest;
  293.                 }
  294.         }
  295.         unset($oid);
  296.         ksort($d);
  297.  
  298.         $data = null;
  299.         foreach ($d as $firstarc => &$data) {
  300.                 oidSort($data);
  301.                 foreach ($data as &$rest) {
  302.                         $out[] = ($output_with_leading_dot ? '.' : '')."$firstarc" . (($rest != $none) ? ".$rest" : '');
  303.                 }
  304.         }
  305.         unset($data);
  306.  
  307.         $ary = $out;
  308. }
  309.  
  310. /**
  311.  * Checks if two OIDs in dot-notation are equal
  312.  * @author  Daniel Marschall, ViaThinkSoft
  313.  * @version 2020-05-27
  314.  * @param   string $oidA<br/>
  315.  *              First OID
  316.  * @param   string $oidB<br/>
  317.  *              Second OID
  318.  * @return  boolean|null True if the OIDs are equal, null if one of the OIDs are invalid
  319.  **/
  320. function oid_dotnotation_equal($oidA, $oidB) {
  321.         $oidA = sanitizeOID($oidA, false);
  322.         if ($oidA === false) return null;
  323.  
  324.         $oidB = sanitizeOID($oidB, false);
  325.         if ($oidB === false) return null;
  326.  
  327.         return $oidA === $oidB;
  328. }
  329.  
  330. /**
  331.  * Removes leading zeroes from an OID in dot notation.
  332.  * @author  Daniel Marschall, ViaThinkSoft
  333.  * @version 2015-08-17
  334.  * @param   string $oid<br/>
  335.  *              An OID in dot notation.
  336.  * @param   mixed $leading_dot<br/>
  337.  *              true: The OID is valid, if it contains a leading dot.<br/>
  338.  *              false (default): The OID is valid, if it does not contain a leading dot.
  339.  *              'auto: Allow both
  340.  * @return  string|false The OID without leading dots, or <code>false</code> if the OID is syntactically wrong.
  341.  **/
  342. function sanitizeOID($oid, $leading_dot=false) {
  343.         static $oid_sanitize_cache = array();
  344.  
  345.         if ($leading_dot) $leading_dot = substr($oid,0,1) == '.';
  346.  
  347.         // We are using a cache, since this function is used very often by OIDplus
  348.         global $oid_sanitize_cache;
  349.         $v = ($leading_dot ? 'T' : 'F').$oid;
  350.         if (isset($oid_sanitize_cache[$v])) return $oid_sanitize_cache[$v];
  351.  
  352.         if ($leading_dot) {
  353.                 if ($oid == '.') return '';
  354.         } else {
  355.                 if ($oid == '') return '';
  356.         }
  357.  
  358.         $out = '';
  359.         $ary = explode('.', $oid);
  360.         foreach ($ary as $n => &$a) {
  361.                 if (($leading_dot) && ($n == 0)) {
  362.                         if ($a != '') return false;
  363.                         continue;
  364.                 }
  365.  
  366.                 if (!preg_match('@^(\\d+)$@', $a, $m)) return false; // does contain something other than digits
  367.  
  368.                 // strike leading zeroes
  369.                 $a = preg_replace("@^0+@", '', $a);
  370.                 if ($a == '') $a = 0;
  371.  
  372.                 if (($leading_dot) || ($n != 0)) $out .= '.';
  373.                 $out .= $a;
  374.         }
  375.         unset($a);
  376.         unset($ary);
  377.  
  378.         $oid_sanitize_cache[$v] = $out;
  379.         return $out;
  380. }
  381.  
  382. /**
  383.  * Shows the top arc of an OID.
  384.  * This function tolerates leading dots.
  385.  * @author  Daniel Marschall, ViaThinkSoft
  386.  * @version 2014-12-16
  387.  * @param   string $oid<br/>
  388.  *              An OID in dot notation.
  389.  * @return  string|false The top arc of the OID or empty string if it is already the root ('.')
  390.  **/
  391. function oid_toparc($oid) {
  392.         $leadingdot = substr($oid,0,1) == '.';
  393.  
  394.         $oid = sanitizeOID($oid, $leadingdot);
  395.         if ($oid === false) return false;
  396.  
  397.         if (!$leadingdot) $oid = '.'.$oid;
  398.  
  399.         $p = strrpos($oid, '.');
  400.         if ($p === false) return false;
  401.         $r = substr($oid, $p+1);
  402.  
  403.         if ($leadingdot) {
  404.         #       if ($r == '') return '.';
  405.                 return $r;
  406.         } else {
  407.                 return substr($r, 1);
  408.         }
  409. }
  410.  
  411. /**
  412.  * Calculates the distance between two OIDs.
  413.  * This function tolerates leading dots and leading zeroes.
  414.  * @author  Daniel Marschall, ViaThinkSoft
  415.  * @version 2014-12-20
  416.  * @param   string $a<br/>
  417.  *              An OID.
  418.  * @param   string $b<br/>
  419.  *              An OID.
  420.  * @return  int|false  false if both OIDs do not have a child-parent or parent-child relation, e.g. oid_distance('2.999.1.2.3', '2.999.4.5') = false, or if one of the OIDs is syntactially invalid<br/>
  421.  *              >0 if $a is more specific than $b , e.g. oid_distance('2.999.1.2', '2.999') = 2<br/>
  422.  *              <0 if $a is more common than $b , e.g. oid_distance('2.999', '2.999.1.2') = -2
  423.  **/
  424. function oid_distance($a, $b) {
  425.         if (substr($a,0,1) == '.') $a = substr($a,1);
  426.         if (substr($b,0,1) == '.') $b = substr($b,1);
  427.  
  428.         $a = sanitizeOID($a, false);
  429.         if ($a === false) return false;
  430.         $b = sanitizeOID($b, false);
  431.         if ($b === false) return false;
  432.  
  433.         $ary = explode('.', $a);
  434.         $bry = explode('.', $b);
  435.  
  436.         $min_len = min(count($ary), count($bry));
  437.  
  438.         for ($i=0; $i<$min_len; $i++) {
  439.                 if ($ary[$i] != $bry[$i]) return false;
  440.         }
  441.  
  442.         return count($ary) - count($bry);
  443. }
  444. /*
  445. assert(oid_distance('2.999.1.2.3', '2.999.4.5') === false);
  446. assert(oid_distance('2.999.1.2', '2.999') === 2);
  447. assert(oid_distance('2.999', '2.999.1.2') === -2);
  448. */
  449.  
  450. /**
  451.  * Adds a leading dot to an OID.
  452.  * Leading zeroes are tolerated.
  453.  * @author  Daniel Marschall, ViaThinkSoft
  454.  * @version 2014-12-20
  455.  * @param   string $oid<br/>
  456.  *              An OID.
  457.  * @return  string|false The OID with a leading dot or false if the OID is syntactially wrong.
  458.  **/
  459. function oid_add_leading_dot($oid) {
  460.         $oid = sanitizeOID($oid, 'auto');
  461.         if ($oid === false) return false;
  462.  
  463.         if (substr($oid,0,1) != '.') $oid = '.'.$oid;
  464.         return $oid;
  465. }
  466.  
  467. /**
  468.  * Removes a leading dot to an OID.
  469.  * Leading zeroes are tolerated.
  470.  * @author  Daniel Marschall, ViaThinkSoft
  471.  * @version 2014-12-20
  472.  * @param   string $oid<br/>
  473.  *              An OID.
  474.  * @return  string|false The OID without a leading dot or false if the OID is syntactially wrong.
  475.  **/
  476. function oid_remove_leading_dot($oid) {
  477.         $oid = sanitizeOID($oid, 'auto');
  478.         if ($oid === false) return false;
  479.  
  480.         if (substr($oid,0,1) == '.') $oid = substr($oid, 1);
  481.         return $oid;
  482. }
  483.  
  484. /**
  485.  * Find the common ancestor of two or more OIDs
  486.  * @author  Daniel Marschall, ViaThinkSoft
  487.  * @version 2020-05-27
  488.  * @param   string[] $oids<br/>
  489.  *              An array of multiple OIDs, e.g. 2.999.1 and 2.999.2.3.4
  490.  * @return  string|false The common ancestor, e.g. 2.999, or false if there is no common ancestor.
  491.  **/
  492. function oid_common_ancestor(array $oids) {
  493.         $shared = array();
  494.  
  495.         if (!is_array($oids)) return false;
  496.         if (count($oids) === 0) return false;
  497.  
  498.         foreach ($oids as &$oid) {
  499.                 $oid = sanitizeOID($oid, false);
  500.                 if ($oid === false) return false;
  501.                 $oid = explode('.', $oid);
  502.         }
  503.  
  504.         $max_ok = strlen($oids[0]);
  505.         for ($i=1; $i<count($oids); $i++) {
  506.                 for ($j=0; $j<min(strlen($oids[$i]),strlen($oids[0])); $j++) {
  507.                         if ($oids[$i][$j] != $oids[0][$j]) {
  508.                                 if ($j < $max_ok) $max_ok = $j;
  509.                                 break;
  510.                         }
  511.                 }
  512.                 if ($j < $max_ok) $max_ok = $j;
  513.         }
  514.  
  515.         $out = array();
  516.         for ($i=0; $i<$max_ok; $i++) {
  517.                 $out[] = $oids[0][$i];
  518.         }
  519.         return implode('.', $out);
  520. }
  521. /*
  522. assert(oid_shared_ancestor(array('2.999.4.5.3', '2.999.4.5')) === "2.999.4.5");
  523. assert(oid_shared_ancestor(array('2.999.4.5', '2.999.4.5.3')) === "2.999.4.5");
  524. assert(oid_shared_ancestor(array('2.999.1.2.3', '2.999.4.5')) === "2.999");
  525. */
  526.  
  527.  
  528. # === OID-IRI NOTATION FUNCTIONS ===
  529.  
  530. if (!function_exists('mb_ord')) {
  531.         # http://stackoverflow.com/a/24755772/3544341
  532.         function mb_ord($char, $encoding = 'UTF-8') {
  533.                 if ($encoding === 'UCS-4BE') {
  534.                         list(, $ord) = (strlen($char) === 4) ? @unpack('N', $char) : @unpack('n', $char);
  535.                         return $ord;
  536.                 } else {
  537.                         return mb_ord(mb_convert_encoding($char, 'UCS-4BE', $encoding), 'UCS-4BE');
  538.                 }
  539.         }
  540. }
  541.  
  542. function iri_char_valid($c, $firstchar, $lastchar) {
  543.         // see Rec. ITU-T X.660, clause 7.5
  544.  
  545.         if (($firstchar || $lastchar) && ($c == '-')) return false;
  546.  
  547.         if ($c == '-') return true;
  548.         if ($c == '.') return true;
  549.         if ($c == '_') return true;
  550.         if ($c == '~') return true;
  551.         if (($c >= '0') && ($c <= '9') && (!$firstchar)) return true;
  552.         if (($c >= 'A') && ($c <= 'Z')) return true;
  553.         if (($c >= 'a') && ($c <= 'z')) return true;
  554.  
  555.         $v = mb_ord($c);
  556.  
  557.         if (($v >= 0x000000A0) && ($v <= 0x0000DFFE)) return true;
  558.         if (($v >= 0x0000F900) && ($v <= 0x0000FDCF)) return true;
  559.         if (($v >= 0x0000FDF0) && ($v <= 0x0000FFEF)) return true;
  560.         if (($v >= 0x00010000) && ($v <= 0x0001FFFD)) return true;
  561.         if (($v >= 0x00020000) && ($v <= 0x0002FFFD)) return true;
  562.         if (($v >= 0x00030000) && ($v <= 0x0003FFFD)) return true;
  563.         if (($v >= 0x00040000) && ($v <= 0x0004FFFD)) return true;
  564.         if (($v >= 0x00050000) && ($v <= 0x0005FFFD)) return true;
  565.         if (($v >= 0x00060000) && ($v <= 0x0006FFFD)) return true;
  566.         if (($v >= 0x00070000) && ($v <= 0x0007FFFD)) return true;
  567.         if (($v >= 0x00080000) && ($v <= 0x0008FFFD)) return true;
  568.         if (($v >= 0x00090000) && ($v <= 0x0009FFFD)) return true;
  569.         if (($v >= 0x000A0000) && ($v <= 0x000AFFFD)) return true;
  570.         if (($v >= 0x000B0000) && ($v <= 0x000BFFFD)) return true;
  571.         if (($v >= 0x000C0000) && ($v <= 0x000CFFFD)) return true;
  572.         if (($v >= 0x000D0000) && ($v <= 0x000DFFFD)) return true;
  573.         if (($v >= 0x000E1000) && ($v <= 0x000EFFFD)) return true;
  574.  
  575.         // Note: Rec. ITU-T X.660, clause 7.5.3 would also forbid ranges which are marked in ISO/IEC 10646 as "(This position shall not be used)"
  576.         // But tool implementers should be tolerate them, since these limitations can be removed in future.
  577.  
  578.         return false;
  579. }
  580.  
  581. function iri_arc_valid($arc, $allow_numeric=true) {
  582.         if ($arc == '') return false;
  583.  
  584.         $m = array();
  585.         if ($allow_numeric && preg_match('@^(\\d+)$@', $arc, $m)) return true; # numeric arc
  586.  
  587.         // Question: Should we strip RTL/LTR characters?
  588.  
  589.         if (mb_substr($arc, 2, 2) == '--') return false; // see Rec. ITU-T X.660, clause 7.5.4
  590.  
  591.         $array = array();
  592.         preg_match_all('/./u', $arc, $array, PREG_SET_ORDER);
  593.         $len = count($array);
  594.         foreach ($array as $i => $char) {
  595.                 if (!iri_char_valid($char[0], $i==0, $i==$len-1)) return false;
  596.         }
  597.  
  598.         return true;
  599. }
  600.  
  601. /**
  602.  * Checks if an IRI identifier is valid or not.
  603.  * @author  Daniel Marschall, ViaThinkSoft
  604.  * @version 2014-12-17
  605.  * @param   string $iri<br/>
  606.  *              An OID in OID-IRI notation, e.g. /Example/test
  607.  * @return  boolean true if the IRI identifier is valid.
  608.  **/
  609. function iri_valid($iri) {
  610.         if ($iri == '/') return true; // OK?
  611.  
  612.         if (substr($iri, 0, 1) != '/') return false;
  613.  
  614.         $ary = explode('/', $iri);
  615.         array_shift($ary);
  616.         foreach ($ary as $a) {
  617.                 if (!iri_arc_valid($a)) return false;
  618.         }
  619.  
  620.         return true;
  621. }
  622.  
  623. /*
  624. assert(iri_arc_valid('ABCDEF'));
  625. assert(!iri_arc_valid('-ABCDEF'));
  626. assert(!iri_arc_valid('ABCDEF-'));
  627. assert(!iri_arc_valid(' ABCDEF'));
  628. assert(!iri_arc_valid('2 ABCDEF'));
  629. assert(!iri_arc_valid(''));
  630.  
  631. assert(!iri_valid(''));
  632. assert(iri_valid('/'));
  633. assert(iri_valid('/hello/world'));
  634. assert(iri_valid('/123/world'));
  635. assert(!iri_valid('/hello/0world'));
  636. assert(!iri_valid('/hello/xo--test'));
  637. assert(!iri_valid('/hello/-super-/sd'));
  638. */
  639.  
  640. /**
  641.  * Returns an associative array in the form 'ASN.1' => '/2/1' .
  642.  * @author  Daniel Marschall, ViaThinkSoft
  643.  * @version 2018-01-05
  644.  * @see http://itu.int/go/X660
  645.  * @return array<string,string> An associative array in the form 'ASN.1' => '/2/1' .
  646.  **/
  647. function iri_get_long_arcs() {
  648.         $iri_long_arcs = array();
  649.         $iri_long_arcs['ASN.1'] = '/2/1';
  650.         $iri_long_arcs['Country'] = '/2/16';
  651.         $iri_long_arcs['International-Organizations'] = '/2/23';
  652.         $iri_long_arcs['UUID'] = '/2/25';
  653.         $iri_long_arcs['Tag-Based'] = '/2/27';
  654.         $iri_long_arcs['BIP'] = '/2/41';
  655.         $iri_long_arcs['Telebiometrics'] = '/2/42';
  656.         $iri_long_arcs['Cybersecurity'] = '/2/48';
  657.         $iri_long_arcs['Alerting'] = '/2/49';
  658.         $iri_long_arcs['OIDResolutionSystem'] = '/2/50';
  659.         $iri_long_arcs['GS1'] = '/2/51';
  660.         $iri_long_arcs['Example'] = '/2/999'; // English
  661.         $iri_long_arcs['Exemple'] = '/2/999'; // French
  662.         $iri_long_arcs['Ejemplo'] = '/2/999'; // Spanish
  663.         $iri_long_arcs["\u{0627}\u{0644}\u{0645}\u{062B}\u{0627}\u{0644}"] = '/2/999'; // Arabic
  664.         $iri_long_arcs["\u{8303}\u{4F8B}"] = '/2/999'; // Chinese
  665.         $iri_long_arcs["\u{041F}\u{0440}\u{0438}\u{043C}\u{0435}\u{0440}"] = '/2/999'; // Russian
  666.         $iri_long_arcs["\u{C608}\u{C81C}"] = '/2/999'; // Korean
  667.         $iri_long_arcs["\u{4F8B}"] = '/2/999'; // Japanese
  668.         $iri_long_arcs['Beispiel'] = '/2/999'; // German
  669.         return $iri_long_arcs;
  670. }
  671.  
  672. /**
  673.  * Tries to shorten/simplify an IRI by applying "long arcs", e.g. /2/999/123 -> /Example/123 .
  674.  * @author  Daniel Marschall, ViaThinkSoft
  675.  * @version 2020-05-22
  676.  * @param   string $iri<br/>
  677.  *              An OID in OID-IRI notation, e.g. /Example/test
  678.  * @return  string|false The modified IRI.
  679.  **/
  680. function iri_add_longarcs($iri) {
  681.         $iri_long_arcs = iri_get_long_arcs();
  682.  
  683.         if (!iri_valid($iri)) return false;
  684.  
  685.         $ary = explode('/', $iri);
  686.  
  687.         $ary_number_iri = $ary;
  688.         if ($ary_number_iri[1] == 'Joint-ISO-ITU-T') $ary_number_iri[1] = '2';
  689.  
  690.         $number_iri = implode('/', $ary_number_iri);
  691.  
  692.         foreach ($iri_long_arcs as $cur_longarc => $cur_iri) {
  693.                 assert(iri_valid($cur_iri));
  694.                 if (strpos($number_iri.'/', $cur_iri.'/') === 0) {
  695.                         $cnt = substr_count($cur_iri, '/');
  696.                         for ($i=1; $i<$cnt; $i++) {
  697.                                 array_shift($ary);
  698.                         }
  699.                         $ary[0] = '';
  700.                         $ary[1] = $cur_longarc;
  701.                         $iri = implode('/', $ary);
  702.                         break;
  703.                 }
  704.         }
  705.  
  706.         return $iri;
  707. }
  708. /*
  709. assert(iri_add_longarcs('/2/999/123') === '/Example/123');
  710. */
  711.  
  712. # === FUNCTIONS FOR OIDS IN ASN.1 NOTATION ===
  713.  
  714. /**
  715.  * Checks if an ASN.1 identifier is valid.
  716.  * @author  Daniel Marschall, ViaThinkSoft
  717.  * @version 2020-05-22
  718.  * @param   string $id<br/>
  719.  *              An ASN.1 identifier, e.g. "example". Not "example(99)" or "99" and not a path like "{ 2 999 }"
  720.  *              Note: Use asn1_path_valid() for validating a whole ASN.1 notation path.
  721.  * @return  boolean true, if the identifier is valid: It begins with an lowercase letter and contains only 0-9, a-z, A-Z and "-"
  722.  **/
  723. function oid_id_is_valid($id) {
  724.         // see Rec. ITU-T X.660 | ISO/IEC 9834-1, clause 7.7
  725.         // and Rec. ITU-T X.680 | ISO/IEC 8824-1, clause 12.3
  726.         if (substr($id,-1,1) == '-') return false;
  727.         if (strstr($id,'--')) return false;
  728.         return preg_match('/^([a-z][a-zA-Z0-9-]*)$/', $id) != 0;
  729. }
  730.  
  731. /**
  732.  * Checks if the ASN.1 notation of an OID is valid.
  733.  * This function does not tolerate leading zeros.
  734.  * This function will fail (return false) if there are unresolved symbols, e.g. {iso test} is not valid while { iso 123 } is valid.
  735.  * @author  Daniel Marschall, ViaThinkSoft
  736.  * @version 2014-12-17
  737.  * @param   string $asn1<br/>
  738.  *              An OID in ASN.1 notation.
  739.  * @return  boolean true if the identifier is valid.
  740.  **/
  741. function asn1_path_valid($asn1) {
  742.         return asn1_to_dot($asn1) != false;
  743. }
  744.  
  745. /**
  746.  * Returns an array of standardized ASN.1 alphanumeric identifiers which do not require a numeric identifier, e.g. { 2 example }
  747.  * The array has the form '0.0.a' -> '0.0.1'
  748.  * @author  Daniel Marschall, ViaThinkSoft
  749.  * @version 2019-03-25
  750.  * @see http://www.oid-info.com/name-forms.htm
  751.  * @return  array<string,string> Associative array of standardized ASN.1 alphanumeric identifiers
  752.  **/
  753. function asn1_get_standardized_array() {
  754.  
  755.         // Taken from oid-info.com
  756.         // http://www.oid-info.com/name-forms.htm
  757.         $standardized = array();
  758.         $standardized['itu-t'] = '0';
  759.         $standardized['ccitt'] = '0';
  760.         $standardized['iso'] = '1';
  761.         $standardized['joint-iso-itu-t'] = '2';
  762.         $standardized['joint-iso-ccitt'] = '2';
  763.         $standardized['0.recommendation'] = '0.0';
  764.         $standardized['0.0.a'] = '0.0.1';
  765.         $standardized['0.0.b'] = '0.0.2';
  766.         $standardized['0.0.c'] = '0.0.3';
  767.         $standardized['0.0.d'] = '0.0.4';
  768.         $standardized['0.0.e'] = '0.0.5';
  769.         $standardized['0.0.f'] = '0.0.6';
  770.         $standardized['0.0.g'] = '0.0.7';
  771.         $standardized['0.0.h'] = '0.0.8';
  772.         $standardized['0.0.i'] = '0.0.9';
  773.         $standardized['0.0.j'] = '0.0.10';
  774.         $standardized['0.0.k'] = '0.0.11';
  775.         $standardized['0.0.l'] = '0.0.12';
  776.         $standardized['0.0.m'] = '0.0.13';
  777.         $standardized['0.0.n'] = '0.0.14';
  778.         $standardized['0.0.o'] = '0.0.15';
  779.         $standardized['0.0.p'] = '0.0.16';
  780.         $standardized['0.0.q'] = '0.0.17';
  781.         $standardized['0.0.r'] = '0.0.18';
  782.         $standardized['0.0.s'] = '0.0.19';
  783.         $standardized['0.0.t'] = '0.0.20';
  784.         $standardized['0.0.u'] = '0.0.21';
  785.         $standardized['0.0.v'] = '0.0.22';
  786.         $standardized['0.0.w'] = '0.0.23'; // actually, this OID does not exist
  787.         $standardized['0.0.x'] = '0.0.24';
  788.         $standardized['0.0.y'] = '0.0.25';
  789.         $standardized['0.0.z'] = '0.0.26';
  790.         $standardized['0.question'] = '0.1';
  791.         $standardized['0.administration'] = '0.2';
  792.         $standardized['0.network-operator'] = '0.3';
  793.         $standardized['0.identified-organization'] = '0.4';
  794.         $standardized['1.standard'] = '1.0';
  795.         $standardized['1.registration-authority'] = '1.1';
  796.         $standardized['1.member-body'] = '1.2';
  797.         $standardized['1.identified-organization'] = '1.3';
  798.         return $standardized;
  799. }
  800.  
  801. /**
  802.  * Converts an OID in ASN.1 notation into an OID in dot notation and tries to resolve well-known identifiers.<br/>
  803.  * e.g. {joint-iso-itu-t(2) example(999) 1 2 3} --> 2.999.1.2.3<br/>
  804.  * e.g. {iso 3} --> 1.3
  805.  * This function does not tolerate leading zeros.
  806.  * This function will fail (return false) if there are unresolved symbols, e.g. {iso test} will not be resolved to 1.test
  807.  * @author  Daniel Marschall, ViaThinkSoft
  808.  * @version 2014-12-17
  809.  * @param   string $asn<br/>
  810.  *              An OID in ASN.1 notation.
  811.  * @return  string|false An OID in dot notation without leading dot or false if the path is invalid.
  812.  **/
  813. function asn1_to_dot($asn) {
  814.         $standardized = asn1_get_standardized_array();
  815.  
  816.         // Clean up
  817.         $count = -1;
  818.         $asn = preg_replace('@^\\{(.+)\\}$@', '\\1', $asn, -1, $count);
  819.         if ($count == 0) return false; // { and } are required. The ASN.1 path will NOT be trimmed by this function
  820.  
  821.         // If identifier is set, apply it (no check if it overrides a standardized identifier)
  822.         $asn = preg_replace('|\s*([a-z][a-zA-Z0-9-]*)\s*\((\d+)\)|', ' \\2', $asn);
  823.         $asn = trim($asn);
  824.  
  825.         // Set dots
  826.         $asn = preg_replace('|\s+|', '.', $asn);
  827.  
  828.         // Apply standardized identifiers (case sensitive)
  829.         $asn .= '.';
  830.         foreach ($standardized as $s => $r) {
  831.                 $asn = preg_replace("@^".preg_quote($s,"@")."@", $r, $asn);
  832.         }
  833.         $asn = substr($asn, 0, strlen($asn)-1);
  834.  
  835.         // Check if all numbers are OK
  836.         // -> every arc must be resolved
  837.         // -> numeric arcs must not have a leading zero
  838.         // -> invalid stuff will be recognized, e.g. a "(1)" without an identifier in front of it
  839.         $ary = explode('.', $asn);
  840.         foreach ($ary as $a) {
  841.                 $m = array();
  842.                 if (!preg_match('@^(0|([1-9]\\d*))$@', $a, $m)) return false;
  843.         }
  844.  
  845.         return $asn;
  846. }
  847.  
  848. /*
  849. assert(asn1_to_dot('{2 999 (1)}') == false);
  850. assert(asn1_to_dot('{2 999 test}') == false);
  851. assert(asn1_to_dot('{2 999 1}') == '2.999.1');
  852. assert(asn1_to_dot(' {2 999 1} ') == false);
  853. assert(asn1_to_dot('2 999 1') == false);
  854. assert(asn1_to_dot('{2 999 01}') == false);
  855. assert(asn1_to_dot('{  0   question 123  }') == '0.1.123');
  856. assert(asn1_to_dot('{  iso  }') == '1');
  857. assert(asn1_to_dot('{  iso(1)  }') == '1');
  858. assert(asn1_to_dot('{  iso(2)  }') == '2');
  859. assert(asn1_to_dot('{  iso 3 }') == '1.3');
  860. */
  861.  
  862. /**
  863.  * Gets the last numeric identifier of an ASN.1 notation OID.
  864.  * @author  Daniel Marschall, ViaThinkSoft
  865.  * @version 2020-06-11
  866.  * @param   string $asn1id<br/>
  867.  *              An ASN.1 identifier string, e.g. { 2 example(999) test(1) }
  868.  * @return  int|false The last numeric identifier arc, e.g. "1" or false if the ID is invalid
  869.  **/
  870. function asn1_last_identifier($asn1id) {
  871.         $asn1id = preg_replace('@\(\s*\d+\s*\)@', '', $asn1id);
  872.         $asn1id = trim(str_replace(array('{', '}', "\t"), ' ', $asn1id));
  873.         $ary = explode(' ', $asn1id);
  874.         $asn1id = $ary[count($ary)-1];
  875.         return preg_match('#[^0-9]#',$asn1id) ? (int)$asn1id : false;
  876. }
  877.  
  878. /**
  879.  * "Soft corrects" an invalid ASN.1 identifier.<br/>
  880.  * Attention, by "soft correcting" the ID, it is not authoritative anymore, and might not be able to be resolved by ORS.
  881.  * @author  Daniel Marschall, ViaThinkSoft
  882.  * @version 2020-05-22
  883.  * @param   string $id<br/>
  884.  *              An ASN.1 identifier.
  885.  * @param   boolean $append_id_prefix<br/>
  886.  *              true (default): If the identifier doesn't start with a-Z, the problem will be solved by prepending "id-" to the identifier.<br/>
  887.  *              false: If the identifier doesn't start with a-Z, then the problem cannot be solved (method returns empty string).
  888.  * @return  string The "soft corrected" ASN.1 identifier.<br/>
  889.  *              Invalid characters will be removed.<br/>
  890.  *              Uncorrectable start elements (0-9 or "-") will be either removed or solved by prepending "id-" (see <code>$append_id_prefix</code>)<br/>
  891.  *              If the identifier begins with an upper case letter, the letter will be converted into lower case.
  892.  **/
  893. function oid_soft_correct_id($id, $append_id_prefix = true) {
  894.         // Convert "_" to "-"
  895.         $id = str_replace('_', '-', $id);
  896.  
  897.         // Convert "--" to "-"
  898.         $id = str_replace('--', '-', $id);
  899.  
  900.         // Remove invalid characters
  901.         $id = preg_replace('/[^a-zA-Z0-9-]+/', '', $id);
  902.  
  903.         // Remove uncorrectable start elements (0-9 or "-")
  904.         if ($append_id_prefix) {
  905.                 $id = preg_replace('/^([^a-zA-Z]+)/', 'id-$1', $id);
  906.         } else {
  907.                 $id = preg_replace('/^([^a-zA-Z]+)/', '', $id);
  908.         }
  909.  
  910.         // "Correct" upper case beginning letter by converting it to lower case
  911.         if (preg_match('/^[A-Z]/', $id)) {
  912.                 $id = strtolower($id[0]) . substr($id, 1);
  913.         }
  914.  
  915.         return $id;
  916. }
  917.