Subversion Repositories oidplus

Rev

Rev 1081 | Rev 1116 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

  1. <?php
  2.  
  3. /*
  4.  * OIDplus 2.0
  5.  * Copyright 2019 - 2023 Daniel Marschall, ViaThinkSoft
  6.  *
  7.  * Licensed under the Apache License, Version 2.0 (the "License");
  8.  * you may not use this file except in compliance with the License.
  9.  * You may obtain a copy of the License at
  10.  *
  11.  *     http://www.apache.org/licenses/LICENSE-2.0
  12.  *
  13.  * Unless required by applicable law or agreed to in writing, software
  14.  * distributed under the License is distributed on an "AS IS" BASIS,
  15.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16.  * See the License for the specific language governing permissions and
  17.  * limitations under the License.
  18.  */
  19.  
  20. namespace ViaThinkSoft\OIDplus;
  21.  
  22. // phpcs:disable PSR1.Files.SideEffects
  23. \defined('INSIDE_OIDPLUS') or die;
  24. // phpcs:enable PSR1.Files.SideEffects
  25.  
  26. class OIDplusOIDIP {
  27.  
  28.         protected $XML_SCHEMA_URN;
  29.         protected $XML_SCHEMA_URL;
  30.         protected $JSON_SCHEMA_URN;
  31.         protected $JSON_SCHEMA_URL;
  32.  
  33.         public function __construct() {
  34.                 // NOTES:
  35.                 // - XML_SCHEMA_URN must be equal to the string in the .xsd file!
  36.                 // - the schema file names and draft version are also written in OIDplusPagePublicWhois.class.php
  37.                 $this->XML_SCHEMA_URN  = 'urn:ietf:id:draft-viathinksoft-oidip-05';
  38.                 $this->XML_SCHEMA_URL  = OIDplus::webpath(__DIR__,OIDplus::PATH_ABSOLUTE).'whois/draft-viathinksoft-oidip-05.xsd';
  39.                 $this->JSON_SCHEMA_URN = 'urn:ietf:id:draft-viathinksoft-oidip-05';
  40.                 $this->JSON_SCHEMA_URL = OIDplus::webpath(__DIR__,OIDplus::PATH_ABSOLUTE).'whois/draft-viathinksoft-oidip-05.json';
  41.         }
  42.  
  43.         public function oidipQuery($query) {
  44.  
  45.                 $out_type = null;
  46.                 $out_content = '';
  47.  
  48.                 // Split input into query and arguments
  49.                 $chunks = explode('$', $query);
  50.                 $query = array_shift($chunks);
  51.                 $original_query = $query;
  52.                 $arguments = array();
  53.                 foreach ($chunks as $chunk) {
  54.                         if (strtolower(substr($chunk,0,5)) !== 'auth=') $original_query .= '$'.$chunk;
  55.  
  56.                         if (strpos($chunk,'=') === false) continue;
  57.                         $tmp = explode('=',$chunk,2);
  58.  
  59.                         //if (!preg_match('@^[a-z0-9]+$@', $tmp[0], $m)) continue; // be strict with the names. They must be lowercase (TODO: show "Service error" message?)
  60.                         $tmp[0] = strtolower($tmp[0]); // be tolerant instead
  61.  
  62.                         $arguments[$tmp[0]] = $tmp[1];
  63.                 }
  64.  
  65.                 $query = str_replace('oid:.', 'oid:', $query); // allow leading dot
  66.  
  67.                 if (isset($arguments['format'])) {
  68.                         $format = $arguments['format'];
  69.                 } else {
  70.                         $format = 'text'; // default
  71.                 }
  72.  
  73.                 if (isset($arguments['auth'])) {
  74.                         $authTokens = explode(',', $arguments['auth']);
  75.                 } else {
  76.                         $authTokens = array();
  77.                 }
  78.  
  79.                 // $lang= is not implemented, since we don't know in which language the OID descriptions are written by the site operator
  80.                 /*
  81.                 if (isset($arguments['lang'])) {
  82.                         $languages = explode(',', $arguments['lang']);
  83.                 } else {
  84.                         $languages = array();
  85.                 }
  86.                 */
  87.  
  88.                 $unimplemented_format = ($format != 'text') && ($format != 'json') && ($format != 'xml');
  89.                 if ($unimplemented_format) {
  90.                         $format = 'text';
  91.                 }
  92.  
  93.                 // Step 1: Collect data
  94.  
  95.                 $out = array();
  96.  
  97.                 // ATTENTION: THE ORDER IS IMPORTANT FOR THE XML VALIDATION!
  98.                 // The order of the RFC is the same as in the XSD
  99.                 $out[] = $this->_oidip_attr('query', $original_query);
  100.  
  101.                 $query = OIDplus::prefilterQuery($query, false);
  102.  
  103.                 if ($unimplemented_format) {
  104.                         $out[] = $this->_oidip_attr('result', 'Service error');
  105.                         $out[] = $this->_oidip_attr('message', 'Format is not implemented');
  106.                         $out[] = $this->_oidip_attr('lang', 'en-US');
  107.                 } else {
  108.  
  109.                         $distance = null;
  110.                         $found = null;
  111.  
  112.                         $obj = OIDplusObject::parse($query);
  113.  
  114.                         $only_wellknown_ids_found = false;
  115.                         $continue = false;
  116.  
  117.                         if (!$obj) {
  118.                                 // Object type not known or invalid syntax of $query
  119.                                 $out[] = $this->_oidip_attr('result', 'Not found'); // DO NOT TRANSLATE!
  120.                                 $continue = false;
  121.                         } else {
  122.  
  123.                                 $query = $obj->nodeId(); // normalize
  124.  
  125.                                 $obj = null;
  126.                                 $distance = 0;
  127.  
  128.                                 $init_query = $query;
  129.                                 while (true) {
  130.  
  131.                                         $obj_test = OIDplusObject::findFitting($query);
  132.                                         if ($obj_test) {
  133.                                                 $obj = $obj_test;
  134.                                         } else {
  135.                                                 $alts = OIDplusPagePublicObjects::getAlternativesForQuery($query);
  136.                                                 foreach ($alts as $alt) {
  137.                                                         $obj_test = OIDplusObject::findFitting($alt);
  138.                                                         if ($obj_test) {
  139.                                                                 $query = $alt;
  140.                                                                 $obj = $obj_test;
  141.                                                         }
  142.                                                 }
  143.                                         }
  144.                                         if ($obj) {
  145.                                                 if ($distance > 0) {
  146.                                                         $out[] = $this->_oidip_attr('result', 'Not found; superior object found'); // DO NOT TRANSLATE!
  147.                                                         $out[] = $this->_oidip_attr('distance', $distance); // DO NOT TRANSLATE
  148.                                                 } else {
  149.                                                         $out[] = $this->_oidip_attr('result', 'Found'); // DO NOT TRANSLATE!
  150.                                                 }
  151.                                                 $continue = true;
  152.                                                 break;
  153.                                         }
  154.  
  155.                                         if (substr($query,0,4) === 'oid:') {
  156.                                                 $query_prev = $query;
  157.                                                 $query = 'oid:'.oid_up(explode(':',$query,2)[1]);
  158.                                                 if ($query == $query_prev) break;
  159.                                                 $distance++;
  160.                                         } else {
  161.                                                 // getParent() will find the parent which DOES exist in the DB.
  162.                                                 // It does not need to be the direct parent (like ->one_up() does)
  163.                                                 $obj = OIDplusObject::parse($query)->getParent(); // For objects, we assume that they are parents of each other
  164.                                                 if ($obj) {
  165.                                                         $distance = $obj->distance($query);
  166.                                                         assert(OIDplusObject::findFitting($query));
  167.  
  168.                                                         $query = $obj->nodeId();
  169.                                                 }
  170.  
  171.                                                 if ($distance > 0) {
  172.                                                         $out[] = $this->_oidip_attr('result', 'Not found; superior object found'); // DO NOT TRANSLATE!
  173.                                                         $out[] = $this->_oidip_attr('distance', $distance); // DO NOT TRANSLATE
  174.                                                 }
  175.                                                 $continue = true;
  176.  
  177.                                                 break;
  178.                                         }
  179.                                 }
  180.  
  181.                                 if ((substr($query,0,4) === 'oid:') && (!$obj)) {
  182.                                         $query = $init_query;
  183.                                         $distance = 0;
  184.                                         while (true) {
  185.                                                 // Checks if there is any identifier (so it is a well-known OID)
  186.                                                 $res_test = OIDplus::db()->query("select * from ###asn1id where oid = ? union select * from ###iri where oid = ?", array($query, $query));
  187.                                                 if ($res_test->any()) {
  188.                                                         $obj = OIDplusObject::parse($query);
  189.                                                         if ($distance > 0) {
  190.                                                                 $out[] = $this->_oidip_attr('result', 'Not found; superior object found'); // DO NOT TRANSLATE!
  191.                                                                 $out[] = $this->_oidip_attr('distance', $distance); // DO NOT TRANSLATE
  192.                                                         } else {
  193.                                                                 $out[] = $this->_oidip_attr('result', 'Found'); // DO NOT TRANSLATE!
  194.                                                         }
  195.                                                         $only_wellknown_ids_found = true; // Information partially available
  196.                                                         $continue = true;
  197.                                                         break;
  198.                                                 }
  199.                                                 $query_prev = $query;
  200.                                                 $query = 'oid:'.oid_up(explode(':',$query,2)[1]);
  201.                                                 if ($query == $query_prev) break;
  202.                                                 $distance++;
  203.                                         }
  204.                                 }
  205.  
  206.                                 if (!$obj) {
  207.                                         $out[] = $this->_oidip_attr('result', 'Not found'); // DO NOT TRANSLATE!
  208.                                         $continue = false;
  209.                                 }
  210.  
  211.                                 $found = $distance == 0;
  212.                         }
  213.  
  214.                         if ($continue) {
  215.                                 $out[] = '';
  216.  
  217.                                 // ATTENTION: THE ORDER IS IMPORTANT FOR THE XML VALIDATION!
  218.                                 // The order of the RFC is the same as in the XSD
  219.                                 $out[] = $this->_oidip_attr('object', $query); // DO NOT TRANSLATE!
  220.                                 if (!$this->allowObjectView($obj, $authTokens)) {
  221.                                         $out[] = $this->_oidip_attr('status', 'Information unavailable'); // DO NOT TRANSLATE!
  222.                                         $out[] = $this->_oidip_attr('attribute', 'confidential'); // DO NOT TRANSLATE!
  223.                                 } else {
  224.                                         if ($only_wellknown_ids_found) {
  225.                                                 $out[] = $this->_oidip_attr('status', 'Information partially available'); // DO NOT TRANSLATE!
  226.                                         } else {
  227.                                                 $out[] = $this->_oidip_attr('status', 'Information available'); // DO NOT TRANSLATE!
  228.                                         }
  229.  
  230.                                         // $this->_oidip_attr('lang', ...); // not implemented (since we don't know the language of the texts written by the page operator)
  231.  
  232.                                         if ($obj) {
  233.                                                 $out[] = $this->_oidip_attr('name', $obj->getTitle()); // DO NOT TRANSLATE!
  234.  
  235.                                                 $cont = $obj->getDescription();
  236.                                                 $cont = preg_replace('@<a[^>]+href\s*=\s*["\']([^\'"]+)["\'][^>]*>(.+)<\s*/\s*a\s*>@ismU', '\2 (\1)', $cont);
  237.                                                 $cont = preg_replace('@<br.*>@', "\n", $cont);
  238.                                                 $cont = preg_replace('@\\n+@', "\n", $cont);
  239.                                                 $out[] = $this->_oidip_attr('description', trim(html_entity_decode(strip_tags($cont)))); // DO NOT TRANSLATE!
  240.                                         }
  241.  
  242.                                         // $this->_oidip_attr('information', ...); Not used. Contains additional information, e.g. Management Information Base (MIB) definitions.
  243.  
  244.                                         if ($only_wellknown_ids_found) {
  245.                                                 if (substr($query,0,4) === 'oid:') {
  246.                                                         // Since it is well-known, oid-info.com will most likely have it described
  247.                                                         $out[] = $this->_oidip_attr('url', 'https://oid-rep.orange-labs.fr/get/'.$obj->nodeId(false));
  248.                                                 }
  249.                                         } else {
  250.                                                 $out[] = $this->_oidip_attr('url', OIDplus::webpath(null,OIDplus::PATH_ABSOLUTE).'?goto='.urlencode($obj->nodeId(true)));
  251.                                         }
  252.  
  253.                                         if (substr($query,0,4) === 'oid:') {
  254.                                                 $out[] = $this->_oidip_attr('asn1-notation', $obj->getAsn1Notation(false)); // DO NOT TRANSLATE!
  255.                                                 $out[] = $this->_oidip_attr('iri-notation', $obj->getIriNotation(false)); // DO NOT TRANSLATE!
  256.  
  257.                                                 $res_asn = OIDplus::db()->query("select * from ###asn1id where oid = ?", array($obj->nodeId()));
  258.                                                 while ($row_asn = $res_asn->fetch_object()) {
  259.                                                         $out[] = $this->_oidip_attr('identifier', $row_asn->name); // DO NOT TRANSLATE!
  260.                                                 }
  261.  
  262.                                                 $res_asn = OIDplus::db()->query("select * from ###asn1id where standardized = ? and oid = ?", array(true, $obj->nodeId()));
  263.                                                 while ($row_asn = $res_asn->fetch_object()) {
  264.                                                         $out[] = $this->_oidip_attr('standardized-id', $row_asn->name); // DO NOT TRANSLATE!
  265.                                                 }
  266.  
  267.                                                 $res_iri = OIDplus::db()->query("select * from ###iri where oid = ?", array($obj->nodeId()));
  268.                                                 while ($row_iri = $res_iri->fetch_object()) {
  269.                                                         $out[] = $this->_oidip_attr('unicode-label', $row_iri->name); // DO NOT TRANSLATE!
  270.                                                 }
  271.  
  272.                                                 $res_iri = OIDplus::db()->query("select * from ###iri where longarc = ? and oid = ?", array(true, $obj->nodeId()));
  273.                                                 while ($row_iri = $res_iri->fetch_object()) {
  274.                                                         $out[] = $this->_oidip_attr('long-arc', $row_iri->name); // DO NOT TRANSLATE!
  275.                                                 }
  276.                                         }
  277.  
  278.                                         // $this->_oidip_attr('oidip-service', ...); Not used.
  279.  
  280.                                         // $this->_oidip_attr('oidip-pubkey', ...); Not used.
  281.  
  282.                                         if ($obj->implementsFeature('1.3.6.1.4.1.37476.2.5.2.3.4')) {
  283.                                                 // Also ask $obj for extra attributes:
  284.                                                 // This way we could add various additional information, e.g. IPv4/6 range analysis, interpretation of GUID, etc.
  285.                                                 $obj->whoisObjectAttributes($obj->nodeId(), $out);
  286.                                         }
  287.  
  288.                                         foreach (OIDplus::getAllPlugins() as $plugin) {
  289.                                                 if ($plugin->implementsFeature('1.3.6.1.4.1.37476.2.5.2.3.4')) {
  290.                                                         $plugin->whoisObjectAttributes($obj->nodeId(), $out);
  291.                                                 }
  292.                                         }
  293.  
  294.                                         if ($obj->isConfidential()) { // yes, we use isConfidential() instead of $this->allowObjectView()!
  295.                                                 $out[] = $this->_oidip_attr('attribute', 'confidential'); // DO NOT TRANSLATE!
  296.                                         }
  297.  
  298.                                         if (substr($query,0,4) === 'oid:') {
  299.                                                 $sParent = 'oid:'.oid_up(explode(':',$query,2)[1]);
  300.  
  301.                                                 $objTest = OIDplusObject::parse($sParent);
  302.                                                 if ($this->allowObjectView($objTest, $authTokens)) {
  303.                                                         $out[] = $this->_oidip_attr('parent', $sParent.$this->show_asn1_appendix($sParent)); // DO NOT TRANSLATE!
  304.                                                 } else {
  305.                                                         $out[] = $this->_oidip_attr('parent', $sParent); // DO NOT TRANSLATE!
  306.                                                 }
  307.                                         } else {
  308.                                                 $objParent = $obj->getParent();
  309.                                                 $sParent = $objParent ? $objParent->nodeId() : '';
  310.                                                 if (!empty($sParent) && (!$this->is_root($sParent))) {
  311.                                                         $out[] = $this->_oidip_attr('parent', $sParent); // DO NOT TRANSLATE!
  312.                                                 }
  313.                                         }
  314.  
  315.                                         $res_children = OIDplus::db()->query("select * from ###objects where parent = ? order by ".OIDplus::db()->natOrder('id'), array($obj->nodeId()));
  316.                                         while ($row_children = $res_children->fetch_object()) {
  317.                                                 $objTest = OIDplusObject::parse($row_children->id);
  318.                                                 if ($this->allowObjectView($objTest, $authTokens)) {
  319.                                                         $out[] = $this->_oidip_attr('subordinate', $row_children->id.$this->show_asn1_appendix($row_children->id)); // DO NOT TRANSLATE!
  320.                                                 } else {
  321.                                                         $out[] = $this->_oidip_attr('subordinate', $row_children->id); // DO NOT TRANSLATE!
  322.                                                 }
  323.                                         }
  324.  
  325.                                         if ($obj) {
  326.                                                 if ($tim = $obj->getCreatedTime()) $out[] = $this->_oidip_attr('created', date('Y-m-d H:i:s', strtotime($tim))); // DO NOT TRANSLATE!
  327.                                                 if ($tim = $obj->getUpdatedTime()) $out[] = $this->_oidip_attr('updated', date('Y-m-d H:i:s', strtotime($tim))); // DO NOT TRANSLATE!
  328.                                         }
  329.  
  330.                                         $out[] = '';
  331.  
  332.                                         // ATTENTION: THE ORDER IS IMPORTANT FOR THE XML VALIDATION!
  333.                                         // The order of the RFC is the same as in the XSD
  334.                                         $res_ra = OIDplus::db()->query("select * from ###ra where email = ?", array($obj ? $obj->getRaMail() : ''));
  335.                                         if ($row_ra = $res_ra->fetch_object()) {
  336.                                                 $out[] = $this->_oidip_attr('ra', (!empty($row_ra->ra_name) ? $row_ra->ra_name : (!empty($row_ra->email) ? $row_ra->email : /*_L*/('Unknown')))); // DO NOT TRANSLATE!
  337.  
  338.                                                 if (!$this->allowRAView($row_ra, $authTokens)) {
  339.                                                         $out[] = $this->_oidip_attr('ra-status', 'Information partially available'); // DO NOT TRANSLATE!
  340.                                                 } else {
  341.                                                         $out[] = $this->_oidip_attr('ra-status', 'Information available'); // DO NOT TRANSLATE!
  342.                                                 }
  343.  
  344.                                                 // $this->_oidip_attr('ra-lang', ...); // not implemented (since we don't know the language of the texts written by the page operator)
  345.  
  346.                                                 $tmp = array();
  347.                                                 if (!empty($row_ra->office)) $tmp[] = $row_ra->office;
  348.                                                 if (!empty($row_ra->organization)) $tmp[] = $row_ra->organization;
  349.                                                 $tmp = implode(', ', $tmp);
  350.  
  351.                                                 $out[] = $this->_oidip_attr('ra-contact-name', $row_ra->personal_name.(!empty($tmp) ? " ($tmp)" : '')); // DO NOT TRANSLATE!
  352.                                                 if (!$this->allowRAView($row_ra, $authTokens)) {
  353.                                                         if (!empty($row_ra->street) || !empty($row_ra->zip_town) || !empty($row_ra->country)) {
  354.                                                                 $out[] = $this->_oidip_attr('ra-address', /*_L*/('(redacted)')); // DO NOT TRANSLATE!
  355.                                                         }
  356.                                                         $out[] = $this->_oidip_attr('ra-phone', (!empty($row_ra->phone) ? /*_L*/('(redacted)') : '')); // DO NOT TRANSLATE!
  357.                                                         $out[] = $this->_oidip_attr('ra-mobile', (!empty($row_ra->mobile) ? /*_L*/('(redacted)') : '')); // DO NOT TRANSLATE!
  358.                                                         $out[] = $this->_oidip_attr('ra-fax', (!empty($row_ra->fax) ? /*_L*/('(redacted)') : '')); // DO NOT TRANSLATE!
  359.                                                 } else {
  360.                                                         $address = array();
  361.                                                         if (!empty($row_ra->street))   $address[] = $row_ra->street; // DO NOT TRANSLATE!
  362.                                                         if (!empty($row_ra->zip_town)) $address[] = $row_ra->zip_town; // DO NOT TRANSLATE!
  363.                                                         if (!empty($row_ra->country))  $address[] = $row_ra->country; // DO NOT TRANSLATE!
  364.                                                         if (count($address) > 0) $out[] = $this->_oidip_attr('ra-address', implode("\n",$address));
  365.                                                         $out[] = $this->_oidip_attr('ra-phone', $row_ra->phone); // DO NOT TRANSLATE!
  366.                                                         $out[] = $this->_oidip_attr('ra-mobile', $row_ra->mobile); // DO NOT TRANSLATE!
  367.                                                         $out[] = $this->_oidip_attr('ra-fax', $row_ra->fax); // DO NOT TRANSLATE!
  368.                                                 }
  369.                                                 $out[] = $this->_oidip_attr('ra-email', $obj->getRaMail()); // DO NOT TRANSLATE!
  370.  
  371.                                                 // $this->_oidip_attr('ra-url', ...); Not used.
  372.  
  373.                                                 $ra = new OIDplusRA($obj->getRaMail());
  374.                                                 if ($ra->implementsFeature('1.3.6.1.4.1.37476.2.5.2.3.4')) {
  375.                                                         $ra->whoisRaAttributes($obj->getRaMail(), $out); /** @phpstan-ignore-line */
  376.                                                 }
  377.  
  378.                                                 foreach (OIDplus::getAllPlugins() as $plugin) {
  379.                                                         if ($plugin->implementsFeature('1.3.6.1.4.1.37476.2.5.2.3.4')) {
  380.                                                                 $plugin->whoisRaAttributes($obj->getRaMail(), $out);
  381.                                                         }
  382.                                                 }
  383.  
  384.                                                 // yes, we use row_ra->privacy() instead of $this->allowRAView(), becuase $this->allowRAView=true if auth token is given; and we want to inform the person that they content they are viewing is confidential!
  385.                                                 if ($row_ra->privacy) {
  386.                                                         $out[] = $this->_oidip_attr('ra-attribute', 'confidential'); // DO NOT TRANSLATE!
  387.                                                 }
  388.  
  389.                                                 if ($row_ra->registered) $out[] = $this->_oidip_attr('ra-created', date('Y-m-d H:i:s', strtotime($row_ra->registered))); // DO NOT TRANSLATE!
  390.                                                 if ($row_ra->updated)    $out[] = $this->_oidip_attr('ra-updated', date('Y-m-d H:i:s', strtotime($row_ra->updated))); // DO NOT TRANSLATE!
  391.                                         } else {
  392.                                                 $out[] = $this->_oidip_attr('ra', ($obj && !empty($obj->getRaMail()) ? $obj->getRaMail() : /*_L*/('Unknown'))); // DO NOT TRANSLATE!
  393.                                                 if ($obj) {
  394.                                                         foreach (OIDplus::getAllPlugins() as $plugin) {
  395.                                                                 if ($plugin->implementsFeature('1.3.6.1.4.1.37476.2.5.2.3.4')) {
  396.                                                                         $plugin->whoisRaAttributes($obj->getRaMail(), $out);
  397.                                                                 }
  398.                                                         }
  399.                                                 }
  400.                                                 $out[] = $this->_oidip_attr('ra-status', 'Information unavailable'); // DO NOT TRANSLATE!
  401.                                         }
  402.                                 }
  403.                         }
  404.                 }
  405.  
  406.                 // Upgrade legacy $out to extended $out format
  407.  
  408.                 $this->_oidip_newout_format($out);
  409.  
  410.                 // Trim all values
  411.  
  412.                 foreach ($out as &$tmp) {
  413.                         $tmp['value'] = trim($tmp['value']);
  414.                 }
  415.  
  416.                 // Step 2: Format output
  417.  
  418.                 if ($format == 'text') {
  419.                         $out_type = 'text/plain; charset=UTF-8';
  420.  
  421.                         $longest_key = 0;
  422.                         foreach ($out as $data) {
  423.                                 $longest_key = max($longest_key, strlen(trim($data['name'])));
  424.                         }
  425.  
  426.                         ob_start();
  427.  
  428.                         //$out_content .= '% ' . str_repeat('*', OIDplus::config()->getValue('webwhois_output_format_max_line_length', 80)-2)."\r\n";
  429.  
  430.                         foreach ($out as $data) {
  431.                                 if ($data['name'] == '') {
  432.                                         $out_content .= "\r\n";
  433.                                         continue;
  434.                                 }
  435.  
  436.                                 $key = $data['name'];
  437.                                 $value = $data['value'];
  438.  
  439.                                 // Normalize line-breaks to \r\n, otherwise mb_wordwrap won't work correctly
  440.                                 $value = str_replace("\r\n", "\n", $value);
  441.                                 $value = str_replace("\r", "\n", $value);
  442.                                 $value = str_replace("\n", "\r\n", $value);
  443.  
  444.                                 $value = mb_wordwrap($value, OIDplus::config()->getValue('webwhois_output_format_max_line_length', 80) - $longest_key - strlen(':') - OIDplus::config()->getValue('webwhois_output_format_spacer', 2), "\r\n");
  445.                                 $value = str_replace("\r\n", "\r\n$key:".str_repeat(' ', $longest_key-strlen($key)) . str_repeat(' ', OIDplus::config()->getValue('webwhois_output_format_spacer', 2)), $value);
  446.  
  447.                                 if (!empty($value)) {
  448.                                         $out_content .= $key.':' . str_repeat(' ', $longest_key-strlen($key)) . str_repeat(' ', OIDplus::config()->getValue('webwhois_output_format_spacer', 2)) . $value . "\r\n";
  449.                                 }
  450.                         }
  451.  
  452.                         //$out_content .= '% ' . str_repeat('*', OIDplus::config()->getValue('webwhois_output_format_max_line_length', 80)-2)."\r\n";
  453.  
  454.                         $cont = ob_get_contents();
  455.                         ob_end_clean();
  456.  
  457.                         $out_content .= $cont;
  458.  
  459.                         if (OIDplus::getPkiStatus()) {
  460.                                 $signature = '';
  461.                                 if (@openssl_sign($cont, $signature, OIDplus::getSystemPrivateKey())) {
  462.                                         $signature = base64_encode($signature);
  463.                                         $signature = mb_wordwrap($signature, OIDplus::config()->getValue('webwhois_output_format_max_line_length', 80) - strlen('% '), "\r\n", true);
  464.                                         $signature = "% -----BEGIN RSA SIGNATURE-----\r\n".
  465.                                                      preg_replace('/^/m', '% ', $signature)."\r\n".
  466.                                                      "% -----END RSA SIGNATURE-----\r\n";
  467.                                         $out_content .= $signature;
  468.                                 }
  469.                         }
  470.                 }
  471.  
  472.                 if ($format == 'json') {
  473.                         $ary = array();
  474.  
  475.                         $current_section = array();
  476.                         $ary[] = &$current_section;
  477.  
  478.                         foreach ($out as $data) {
  479.                                 if ($data['name'] == '') {
  480.                                         unset($current_section);
  481.                                         $current_section = array();
  482.                                         $ary[] = &$current_section;
  483.                                 } else {
  484.                                         $key = $data['name'];
  485.                                         $val = trim($data['value']);
  486.                                         if (!empty($val)) {
  487.                                                 if (!isset($current_section[$key])) {
  488.                                                         $current_section[$key] = $val;
  489.                                                 } elseif (is_array($current_section[$key])) {
  490.                                                         $current_section[$key][] = $val;
  491.                                                 } else {
  492.                                                         $current_section[$key] = array($current_section[$key], $val);
  493.                                                 }
  494.                                         }
  495.                                 }
  496.                         }
  497.  
  498.                         // Change $ary=[["query",...], ["object",...], ["ra",...]]
  499.                         // to     $bry=["querySection"=>["query",...], "objectsection"=>["object",...], "raSection"=>["ra",...]]
  500.                         $bry = array();
  501.                         foreach ($ary as $cry) {
  502.                                 $dry = array_keys($cry);
  503.                                 if (count($dry) == 0) continue;
  504.                                 $bry[$dry[0].'Section'] = $cry; /** @phpstan-ignore-line */ // PHPStan thinks that count($dry) is always 0
  505.                         }
  506.  
  507.                         // Remove 'ra-', 'ra1-', ... field prefixes from JSON (the prefix is only for the text view)
  508.                         foreach ($bry as $section_name => &$cry) { /** @phpstan-ignore-line */ // PHPStan thinks that $bry is empty
  509.                                 if (preg_match('@^(ra\d*)\\Section@', $section_name, $m)) {
  510.                                         $ra_id = str_replace('Section', '', $section_name);
  511.                                         $dry = array();
  512.                                         foreach ($cry as $name => $val) {
  513.                                                 if ($name == $ra_id) $name = 'ra'; // First field is always 'ra' even in 'ra1Section' etc.
  514.                                                 $name = preg_replace('@^('.preg_quote($ra_id,'@').')\\-@', '', $name);
  515.                                                 $dry[$name] = $val;
  516.                                         }
  517.                                         $cry = $dry;
  518.                                 }
  519.                         }
  520.  
  521.                         $ary = array(
  522.                                 // We use the URN here, because $id of the schema also uses the URN
  523.                                 '$schema' => $this->JSON_SCHEMA_URN,
  524.  
  525.                                 // we need this NAMED root, otherwise PHP will name the sections "0", "1", "2" if the array is not sequencial (e.g. because "signature" is added)
  526.                                 'oidip' => $bry
  527.                         );
  528.  
  529.                         $json = json_encode($ary);
  530.  
  531.                         if (OIDplus::getPkiStatus()) {
  532.                                 try {
  533.                                         require_once __DIR__.'/whois/json/security.inc.php';
  534.                                         $json = oidplus_json_sign($json, OIDplus::getSystemPrivateKey(), OIDplus::getSystemPublicKey());
  535.                                 } catch (\Exception $e) {
  536.                                         // die($e->getMessage());
  537.                                 }
  538.                         }
  539.  
  540.                         // Good JSON schema validator here: https://www.jsonschemavalidator.net
  541.                         $out_type = 'application/json; charset=UTF-8';
  542.                         $out_content .= $json;
  543.                 }
  544.  
  545.                 if ($format == 'xml') {
  546.                         $xml_oidip = '<oidip><section>';
  547.                         foreach ($out as $data) {
  548.                                 if ($data['name'] == '') {
  549.                                         $xml_oidip .= '</section><section>';
  550.                                 } else {
  551.                                         if ($data['xmlns'] != '') {
  552.                                                 $key = $data['xmlns'].':'.$data['name'];
  553.                                         } else {
  554.                                                 $key = $data['name'];
  555.                                         }
  556.                                         $val = trim($data['value']);
  557.                                         if (!empty($val)) {
  558.                                                 if (strpos($val,"\n") !== false) {
  559.                                                         $val = str_replace(']]>', ']]]]><![CDATA[>', $val); // Escape ']]>'
  560.                                                         $xml_oidip .= "<$key><![CDATA[$val]]></$key>";
  561.                                                 } else {
  562.                                                         $xml_oidip .= "<$key>".htmlspecialchars($val, ENT_XML1)."</$key>";
  563.                                                 }
  564.                                         }
  565.                                 }
  566.                         }
  567.                         $xml_oidip .= '</section></oidip>';
  568.  
  569.                         $xml_oidip = preg_replace('@<section><(.+)>(.+)</section>@ismU', '<\\1Section><\\1>\\2</\\1Section>', $xml_oidip);
  570.  
  571.                         // Remove 'ra-', 'ra1-', ... field prefixes from XML (the prefix is only for the text view)
  572.                         $xml_oidip = preg_replace('@<(/{0,1})ra\\d*-@', '<\\1', $xml_oidip);
  573.  
  574.                         // <ra1Section><ra1>...</ra1> => <ra1Section><ra>...</ra>
  575.                         $xml_oidip = preg_replace('@<ra(\\d+)Section><ra\\1>(.+)</ra\\1>@U', '<ra\\1Section><ra>\\2</ra>', $xml_oidip);
  576.  
  577.                         /* Debug:
  578.                         $out_type = 'application/xml; charset=UTF-8';
  579.                         $out_content .= $xml_oidip;
  580.                         die();
  581.                         */
  582.  
  583.                         // Very good XSD validator here: https://www.liquid-technologies.com/online-xsd-validator
  584.                         // Also good: https://www.utilities-online.info/xsdvalidation (but does not accept &amp; or &quot; results in "Premature end of data in tag description line ...")
  585.                         // Note:
  586.                         // - These do not support XSD 1.1
  587.                         // - You need to host http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd yourself, otherwise there will be a timeout in the validation!!!
  588.  
  589.                         $extra_schemas = array();
  590.                         foreach ($out as $data) {
  591.                                 if (isset($data['xmlns']) && ($data['xmlns'] != '') && !isset($extra_schemas[$data['xmlns']])) {
  592.                                         $extra_schemas[$data['xmlns']] = array($data['xmlschema'], $data['xmlschemauri']);
  593.                                 }
  594.                         }
  595.  
  596.                         $xml  = '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?'.'>'."\n";
  597.                         $xml .= '<root xmlns="'.$this->XML_SCHEMA_URN.'"'."\n";
  598.                         $xml .= '      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'."\n";
  599.                         foreach ($extra_schemas as $xmlns => list($schema,$schemauri)) {
  600.                                 $xml .= '      xmlns:'.$xmlns.'="'.$schema.'"'."\n";
  601.                         }
  602.                         $xml .= '      xsi:schemaLocation="'.$this->XML_SCHEMA_URN.' '.$this->XML_SCHEMA_URL;
  603.                         foreach ($extra_schemas as $xmlns => list($schema,$schemauri)) {
  604.                                 $xml .= ' '.$schema.' '.$schemauri;
  605.                         }
  606.                         $xml .= '">'."\n";
  607.                         $xml .= $xml_oidip."\n";
  608.                         $xml .= '</root>';
  609.  
  610.                         if (!OIDplus::getPkiStatus()) {
  611.                                 $xml .= '<!-- Cannot add signature: OIDplus PKI is not initialized (OpenSSL missing?) -->';
  612.                         } else if (!class_exists('DOMDocument')) {
  613.                                 $xml .= '<!-- Cannot add signature: "PHP-XML" extension not installed -->';
  614.                         } else {
  615.                                 try {
  616.                                         require_once __DIR__.'/whois/xml/security.inc.php';
  617.                                         $xml = oidplus_xml_sign($xml, OIDplus::getSystemPrivateKey(), OIDplus::getSystemPublicKey());
  618.                                 } catch (\Exception $e) {
  619.                                         $xml .= '<!-- Cannot add signature: '.$e.' -->';
  620.                                 }
  621.                         }
  622.  
  623.                         $out_type = 'application/xml; charset=UTF-8';
  624.                         $out_content .= $xml;
  625.                 }
  626.  
  627.                 return array($out_content, $out_type);
  628.         }
  629.  
  630.         protected function show_asn1_appendix($id) {
  631.                 if (substr($id,0,4) === 'oid:') {
  632.                         $appendix_asn1ids = array();
  633.                         $res_asn = OIDplus::db()->query("select * from ###asn1id where oid = ?", array($id));
  634.                         while ($row_asn = $res_asn->fetch_object()) {
  635.                                 $appendix_asn1ids[] = $row_asn->name;
  636.                         }
  637.  
  638.                         $appendix = implode(', ', $appendix_asn1ids);
  639.                         if (!empty($appendix)) $appendix = " ($appendix)";
  640.                 } else {
  641.                         $appendix = '';
  642.                 }
  643.                 return $appendix;
  644.         }
  645.  
  646.         protected function is_root($id) {
  647.                 return empty(explode(':',$id,2)[1]);
  648.         }
  649.  
  650.         protected function authTokenAccepted($content, $authTokens) {
  651.                 foreach ($authTokens as $token) {
  652.                         if (OIDplusPagePublicWhois::genWhoisAuthToken($content) == $token) return true;
  653.                 }
  654.                 return false;
  655.         }
  656.  
  657.         protected function allowObjectView($obj, $authTokens) {
  658.                 // Master auth token (TODO: Have an object-master-token and a ra-master-token?)
  659.                 $authToken = trim(OIDplus::config()->getValue('whois_auth_token'));
  660.                 if (empty($authToken)) $authToken = false;
  661.                 if ($authToken && in_array($authToken, $authTokens)) return true;
  662.  
  663.                 // Per-OID auth tokens
  664.  
  665.                 $curid = $obj->nodeId();
  666.                 while ($test_obj = OIDplusObject::findFitting($curid)) {
  667.                         // Example: You have an auth Token for 2.999.1.2.3
  668.                         // This allows you to view 2.999.1.2.3 and all of its children,
  669.                         // as long as they are not confidential (then you need their auth token).
  670.                         // 2, 2.999, 2.999.1 and 2.999.1.2 are visible,
  671.                         // (because their existence is now obvious).
  672.                         if ($test_obj->isConfidential() && !$this->authTokenAccepted($curid, $authTokens)) return false;
  673.                         $objParentTest = $test_obj->getParent();
  674.                         if (!$objParentTest) break;
  675.                         $curid = $objParentTest->nodeId();
  676.                 }
  677.  
  678.                 // Allow
  679.                 return true;
  680.         }
  681.  
  682.         protected function allowRAView($row_ra, $authTokens) {
  683.                 // Master auth token (TODO: Have an object-master-token and a ra-master-token?)
  684.                 $authToken = trim(OIDplus::config()->getValue('whois_auth_token'));
  685.                 if (empty($authToken)) $authToken = false;
  686.                 if ($authToken && in_array($authToken, $authTokens)) return true;
  687.  
  688.                 // Per-RA auth tokens
  689.                 if ($row_ra->privacy && !$this->authTokenAccepted('ra:'.$row_ra->ra_name, $authTokens)) return false;
  690.  
  691.                 // Allow
  692.                 return true;
  693.         }
  694.  
  695.         protected function _oidip_attr($name, $value) {
  696.                 return array(
  697.                         'xmlns' => '',
  698.                         'xmlschema' => '',
  699.                         'xmlschemauri' => '',
  700.                         'name' => $name,
  701.                         'value' => $value
  702.                 );
  703.         }
  704.  
  705.         protected function _oidip_newout_format(&$out) {
  706.                 foreach ($out as &$line) {
  707.                         if (is_string($line)) {
  708.                                 $ary = explode(':', $line, 2);
  709.                                 $key = trim($ary[0]);
  710.                                 $value = isset($ary[1]) ? trim($ary[1]) : '';
  711.  
  712.                                 $line = array(
  713.                                         'xmlns' => '',
  714.                                         'xmlschema' => '',
  715.                                         'xmlschemauri' => '',
  716.                                         'name' => $key,
  717.                                         'value' => $value
  718.                                 );
  719.                         } else if (is_array($line)) {
  720.                                 if (!isset($line['xmlns']))        $line['xmlns'] = '';
  721.                                 if (!isset($line['xmlschema']))    $line['xmlschema'] = '';
  722.                                 if (!isset($line['xmlschemauri'])) $line['xmlschemauri'] = '';
  723.                                 if (!isset($line['name']))         $line['name'] = '';
  724.                                 if (!isset($line['value']))        $line['value'] = '';
  725.                         } else {
  726.                                 assert(false);
  727.                         }
  728.                 }
  729.         }
  730.  
  731. }
  732.