Subversion Repositories oidplus

Rev

Rev 1385 | 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 OIDplusX500DN extends OIDplusObject {
  27.         /**
  28.          * @var string
  29.          */
  30.         private $identifier;
  31.  
  32.         /**
  33.          * @param string $identifier
  34.          */
  35.         public function __construct(string $identifier) {
  36.                 // No syntax checks
  37.                 $this->identifier = $identifier;
  38.         }
  39.  
  40.         /**
  41.          * @param string $node_id
  42.          * @return OIDplusX500DN|null
  43.          */
  44.         public static function parse(string $node_id)/*: ?OIDplusX500DN*/ {
  45.                 @list($namespace, $identifier) = explode(':', $node_id, 2);
  46.                 if ($namespace !== self::ns()) return null;
  47.                 return new self($identifier);
  48.         }
  49.  
  50.         /**
  51.          * @return string
  52.          */
  53.         public static function objectTypeTitle(): string {
  54.                 return _L('X.500 Distinguished Name');
  55.         }
  56.  
  57.         /**
  58.          * @return string
  59.          */
  60.         public static function objectTypeTitleShort(): string {
  61.                 return _L('X.500 DN');
  62.         }
  63.  
  64.         /**
  65.          * @return string
  66.          */
  67.         public static function ns(): string {
  68.                 return 'x500dn';
  69.         }
  70.  
  71.         /**
  72.          * @return string
  73.          */
  74.         public static function root(): string {
  75.                 return self::ns().':';
  76.         }
  77.  
  78.         /**
  79.          * @return bool
  80.          */
  81.         public function isRoot(): bool {
  82.                 return $this->identifier == '';
  83.         }
  84.  
  85.         /**
  86.          * @param bool $with_ns
  87.          * @return string
  88.          */
  89.         public function nodeId(bool $with_ns=true): string {
  90.                 return $with_ns ? self::root().$this->identifier : $this->identifier;
  91.         }
  92.  
  93.         /**
  94.          * @return string[]
  95.          */
  96.         public static function getKnownAttributeNames(): array {
  97.                 return [
  98.  
  99.                         // Source: http://oid-info.com/get/2.5.4
  100.                         "objectClass" => ["2.5.4.0", "objectClass"],
  101.                         "aliasedEntryName" => ["2.5.4.1", "aliasedEntryName"],
  102.                         "knowledgeInformation" => ["2.5.4.2", "knowledgeInformation"],
  103.                         "commonName" => ["2.5.4.3", "commonName"],
  104.                         "surname" => ["2.5.4.4", "surname"],
  105.                         "serialNumber" => ["2.5.4.5", "serialNumber"],
  106.                         "countryName" => ["2.5.4.6", "countryName"],
  107.                         "localityName" => ["2.5.4.7", "localityName"],
  108.                         "stateOrProvinceName" => ["2.5.4.8", "stateOrProvinceName"],
  109.                         "streetAddress" => ["2.5.4.9", "streetAddress"],
  110.                         "organizationName" => ["2.5.4.10", "organizationName"],
  111.                         "organizationalUnitName" => ["2.5.4.11", "organizationalUnitName"],
  112.                         "title" => ["2.5.4.12", "title"],
  113.                         "description" => ["2.5.4.13", "description"],
  114.                         "searchGuide" => ["2.5.4.14", "searchGuide"],
  115.                         "businessCategory" => ["2.5.4.15", "businessCategory"],
  116.                         "postalAddress" => ["2.5.4.16", "postalAddress"],
  117.                         "postalCode" => ["2.5.4.17", "postalCode"],
  118.                         "postOfficeBox" => ["2.5.4.18", "postOfficeBox"],
  119.                         "physicalDeliveryOfficeName" => ["2.5.4.19", "physicalDeliveryOfficeName"],
  120.                         "telephoneNumber" => ["2.5.4.20", "telephoneNumber"],
  121.                         "telexNumber" => ["2.5.4.21", "telexNumber"],
  122.                         "teletexTerminalIdentifier" => ["2.5.4.22", "teletexTerminalIdentifier"],
  123.                         "facsimileTelephoneNumber" => ["2.5.4.23", "facsimileTelephoneNumber"],
  124.                         "x121Address" => ["2.5.4.24", "x121Address"],
  125.                         "internationalISDNNumber" => ["2.5.4.25", "internationalISDNNumber"],
  126.                         "registeredAddress" => ["2.5.4.26", "registeredAddress"],
  127.                         "destinationIndicator" => ["2.5.4.27", "destinationIndicator"],
  128.                         "preferredDeliveryMethod" => ["2.5.4.28", "preferredDeliveryMethod"],
  129.                         "presentationAddress" => ["2.5.4.29", "presentationAddress"],
  130.                         "supportedApplicationContext" => ["2.5.4.30", "supportedApplicationContext"],
  131.                         "member" => ["2.5.4.31", "member"],
  132.                         "owner" => ["2.5.4.32", "owner"],
  133.                         "roleOccupant" => ["2.5.4.33", "roleOccupant"],
  134.                         "seeAlso" => ["2.5.4.34", "seeAlso"],
  135.                         "userPassword" => ["2.5.4.35", "userPassword"],
  136.                         "userCertificate" => ["2.5.4.36", "userCertificate"],
  137.                         "cACertificate" => ["2.5.4.37", "cACertificate"],
  138.                         "authorityRevocationList" => ["2.5.4.38", "authorityRevocationList"],
  139.                         "certificateRevocationList" => ["2.5.4.39", "certificateRevocationList"],
  140.                         "crossCertificatePair" => ["2.5.4.40", "crossCertificatePair"],
  141.                         "name" => ["2.5.4.41", "name"],
  142.                         "givenName" => ["2.5.4.42", "givenName"],
  143.                         "initials" => ["2.5.4.43", "initials"],
  144.                         "generationQualifier" => ["2.5.4.44", "generationQualifier"],
  145.                         "uniqueIdentifier" => ["2.5.4.45", "uniqueIdentifier"],
  146.                         "dnQualifier" => ["2.5.4.46", "dnQualifier"],
  147.                         "enhancedSearchGuide" => ["2.5.4.47", "enhancedSearchGuide"],
  148.                         "protocolInformation" => ["2.5.4.48", "protocolInformation"],
  149.                         "distinguishedName" => ["2.5.4.49", "distinguishedName"],
  150.                         "uniqueMember" => ["2.5.4.50", "uniqueMember"],
  151.                         "houseIdentifier" => ["2.5.4.51", "houseIdentifier"],
  152.                         "supportedAlgorithms" => ["2.5.4.52", "supportedAlgorithms"],
  153.                         "deltaRevocationList" => ["2.5.4.53", "deltaRevocationList"],
  154.                         "dmdName" => ["2.5.4.54", "dmdName"],
  155.                         "clearance" => ["2.5.4.55", "clearance"],
  156.                         "defaultDirQop" => ["2.5.4.56", "defaultDirQop"],
  157.                         "attributeIntegrityInfo" => ["2.5.4.57", "attributeIntegrityInfo"],
  158.                         "attributeCertificate" => ["2.5.4.58", "attributeCertificate"],
  159.                         "attributeCertificateRevocationList" => ["2.5.4.59", "attributeCertificateRevocationList"],
  160.                         "confKeyInfo" => ["2.5.4.60", "confKeyInfo"],
  161.                         "aACertificate" => ["2.5.4.61", "aACertificate"],
  162.                         "attributeDescriptorCertificate" => ["2.5.4.62", "attributeDescriptorCertificate"],
  163.                         "attributeAuthorityRevocationList" => ["2.5.4.63", "attributeAuthorityRevocationList"],
  164.                         "family-information" => ["2.5.4.64", "family-information"],
  165.                         "pseudonym" => ["2.5.4.65", "pseudonym"],
  166.                         "communicationsService" => ["2.5.4.66", "communicationsService"],
  167.                         "communicationsNetwork" => ["2.5.4.67", "communicationsNetwork"],
  168.                         "certificationPracticeStmt" => ["2.5.4.68", "certificationPracticeStmt"],
  169.                         "certificatePolicy" => ["2.5.4.69", "certificatePolicy"],
  170.                         "pkiPath" => ["2.5.4.70", "pkiPath"],
  171.                         "privPolicy" => ["2.5.4.71", "privPolicy"],
  172.                         "role" => ["2.5.4.72", "role"],
  173.                         "delegationPath" => ["2.5.4.73", "delegationPath"],
  174.                         "protPrivPolicy" => ["2.5.4.74", "protPrivPolicy"],
  175.                         "xMLPrivilegeInfo" => ["2.5.4.75", "xMLPrivilegeInfo"],
  176.                         "xmlPrivPolicy" => ["2.5.4.76", "xmlPrivPolicy"],
  177.                         "uuidpair" => ["2.5.4.77", "uuidpair"],
  178.                         "tagOid" => ["2.5.4.78", "tagOid"],
  179.                         "uiiFormat" => ["2.5.4.79", "uiiFormat"],
  180.                         "uiiInUrh" => ["2.5.4.80", "uiiInUrh"],
  181.                         "contentUrl" => ["2.5.4.81", "contentUrl"],
  182.                         "permission" => ["2.5.4.82", "permission"],
  183.                         "uri" => ["2.5.4.83", "uri"],
  184.                         "pwdAttribute" => ["2.5.4.84", "pwdAttribute"],
  185.                         "userPwd" => ["2.5.4.85", "userPwd"],
  186.                         "urn" => ["2.5.4.86", "urn"],
  187.                         "url" => ["2.5.4.87", "url"],
  188.                         "utmCoordinates" => ["2.5.4.88", "utmCoordinates"],
  189.                         "urnC" => ["2.5.4.89", "urnC"],
  190.                         "uii" => ["2.5.4.90", "uii"],
  191.                         "epc" => ["2.5.4.91", "epc"],
  192.                         "tagAfi" => ["2.5.4.92", "tagAfi"],
  193.                         "epcFormat" => ["2.5.4.93", "epcFormat"],
  194.                         "epcInUrn" => ["2.5.4.94", "epcInUrn"],
  195.                         "ldapUrl" => ["2.5.4.95", "ldapUrl"],
  196.                         "id-at-tagLocation" => ["2.5.4.96", "id-at-tagLocation"],
  197.                         "organizationIdentifier" => ["2.5.4.97", "organizationIdentifier"],
  198.                         "id-at-countryCode3c" => ["2.5.4.98", "id-at-countryCode3c"],
  199.                         "id-at-countryCode3n" => ["2.5.4.99", "id-at-countryCode3n"],
  200.                         "id-at-dnsName" => ["2.5.4.100", "id-at-dnsName"],
  201.                         "id-at-eepkCertificateRevocationList" => ["2.5.4.101", "id-at-eepkCertificateRevocationList"],
  202.                         "id-at-eeAttrCertificateRevocationList" => ["2.5.4.102", "id-at-eeAttrCertificateRevocationList"],
  203.                         "id-at-supportedPublicKeyAlgorithms" => ["2.5.4.103", "id-at-supportedPublicKeyAlgorithms"],
  204.                         "id-at-intEmail" => ["2.5.4.104", "id-at-intEmail"],
  205.                         "id-at-jid" => ["2.5.4.105", "id-at-jid"],
  206.                         "id-at-objectIdentifier" => ["2.5.4.106", "id-at-objectIdentifier"],
  207.                         // Source: https://www.ibm.com/docs/en/zos/2.2.0?topic=SSLTBW_2.2.0/com.ibm.tcp.ipsec.ipsec.help.doc/com/ibm/tcp/ipsec/nss/NssImageServerPs.RB_X500.htm
  208.                         // TODO: Translate human-friendly names using _L()
  209.                         "C" => ["2.5.4.6", "Country"],
  210.                         "CN" => ["2.5.4.3", "Common name"],
  211.                         "DC" => ["0.9.2342.19200300.100.1.25", "Domain component"],
  212.                         "E" => ["1.2.840.113549.1.9.1", "E-mail address"],
  213.                         "EMAIL" => ["1.2.840.113549.1.9.1", "E-mail address"], //(preferred)
  214.                         "EMAILADDRESS" => ["1.2.840.113549.1.9.1", "E-mail address"],
  215.                         "L" => ["2.5.4.7", "Locality"],
  216.                         "O" => ["2.5.4.10", "Organization name"],
  217.                         "OU" => ["2.5.4.11", "Organizational unit name"],
  218.                         "PC" => ["2.5.4.17", "Postal code"],
  219.                         "S" => ["2.5.4.8", "State or province"],
  220.                         "SN" => ["2.5.4.4", "Family name"], // SN=Surname
  221.                         "SP" => ["2.5.4.8", "State or province"],
  222.                         "ST" => ["2.5.4.8", "State or province"], //(preferred)
  223.                         "STREET" => ["2.5.4.9", "Street"],
  224.                         "T" => ["2.5.4.12", "Title"],
  225.                         // Source: https://www.cryptosys.net/pki/manpki/pki_distnames.html
  226.                         "TITLE" => ["2.5.4.12", "Title"],
  227.                         "G" => ["2.5.4.42", "Given name"],
  228.                         "GN" => ["2.5.4.42", "Given name"],
  229.                         "UID" => ["0.9.2342.19200300.100.1.1", "User ID"],
  230.                         "SERIALNUMBER", ["2.5.4.5", "Serial number"]
  231.                 ];
  232.         }
  233.  
  234.         /**
  235.          * @param string $arc A RDN (Relative Distinguished Name), e.g. C=DE, CN=test, or 2.999=example
  236.          * @return bool
  237.          */
  238.         private static function isValidArc(string $arc): bool {
  239.                 $ary = explode('=', $arc);
  240.                 if (count($ary) !== 2) return false;
  241.                 if ($ary[0] == "") return false;
  242.                 if ($ary[1] == "") return false;
  243.  
  244.                 if (oid_valid_dotnotation($ary[0], false, false, 1)) return true;
  245.  
  246.                 $accepted_attribute_names = self::getKnownAttributeNames();
  247.                 foreach ($accepted_attribute_names as $abbr => list($oid, $human_friendly_name)) {
  248.                         if (strtolower($abbr) === strtolower($ary[0])) return true;
  249.                 }
  250.                 return false;
  251.         }
  252.  
  253.         /**
  254.          * @param string $str
  255.          * @return string
  256.          */
  257.         public function addString(string $str): string {
  258.                 if (substr($str,0,1) == '/') $str = substr($str, 1);
  259.  
  260.                 $new_arcs = explode('/', $str);
  261.                 foreach ($new_arcs as $n => $test_arc) {
  262.                         if (!self::isValidArc($test_arc)) {
  263.                                 throw new OIDplusException(_L("Arc %1 (%2) is not a valid Relative Distinguished Name (RDN).", $n+1, $test_arc));
  264.                         }
  265.                 }
  266.  
  267.                 if ($this->isRoot()) {
  268.                         if (substr($str,0,1) != '/') $str = '/'.$str;
  269.                         return self::root() . $str;
  270.                 } else {
  271.                         if (strpos($str,'/') !== false) throw new OIDplusException(_L('Please only submit one arc.'));
  272.                         return $this->nodeId() . '/' . $str;
  273.                 }
  274.         }
  275.  
  276.         /**
  277.          * @param OIDplusObject $parent
  278.          * @return string
  279.          */
  280.         public function crudShowId(OIDplusObject $parent): string {
  281.                 if ($parent->isRoot()) {
  282.                         return substr($this->nodeId(), strlen($parent->nodeId()));
  283.                 } else {
  284.                         return substr($this->nodeId(), strlen($parent->nodeId())+1);
  285.                 }
  286.         }
  287.  
  288.         /**
  289.          * @param OIDplusObject|null $parent
  290.          * @return string
  291.          */
  292.         public function jsTreeNodeName(OIDplusObject $parent = null): string {
  293.                 if ($parent == null) return $this->objectTypeTitle();
  294.                 if ($parent->isRoot()) {
  295.                         return substr($this->nodeId(), strlen($parent->nodeId()));
  296.                 } else {
  297.                         return substr($this->nodeId(), strlen($parent->nodeId())+1);
  298.                 }
  299.         }
  300.  
  301.         /**
  302.          * @return string
  303.          */
  304.         public function defaultTitle(): string {
  305.                 return $this->identifier;
  306.         }
  307.  
  308.         /**
  309.          * @return bool
  310.          */
  311.         public function isLeafNode(): bool {
  312.                 return false;
  313.         }
  314.  
  315.         /**
  316.          * @return string[]
  317.          * @throws OIDplusException
  318.          */
  319.         private function getTechInfo(): array {
  320.                 $tech_info = array();
  321.  
  322.                 $known_attr_names = self::getKnownAttributeNames();
  323.  
  324.                 // Note: There are some notation rules if names contain things like backslashes, see https://www.cryptosys.net/pki/manpki/pki_distnames.html
  325.                 // We currently do not implement these! (TODO)
  326.  
  327.                 $html_dce_ad_notation = '';
  328.                 $html_ldap_notation = '';
  329.                 $html_encoded_string_notation = '';
  330.  
  331.                 $arcs = explode('/', ltrim($this->identifier,'/'));
  332.                 foreach ($arcs as $arc) {
  333.                         $ary = explode('=', $arc);
  334.  
  335.                         $found_oid = '';
  336.                         $found_hf_name = '???';
  337.                         foreach ($known_attr_names as $name => list($oid, $human_friendly_name)) {
  338.                                 if (strtolower($name) == strtolower($ary[0])) {
  339.                                         $found_oid = $oid;
  340.                                         $found_hf_name = $human_friendly_name;
  341.                                         break;
  342.                                 }
  343.                         }
  344.  
  345.                         $html_dce_ad_notation .= '/<abbr title="'.htmlentities($found_hf_name).'">'.htmlentities(strtoupper($ary[0])).'</abbr>='.htmlentities($ary[1]);
  346.                         $html_ldap_notation = '<abbr title="'.htmlentities($found_hf_name).'">'.htmlentities(strtoupper($ary[0])).'</abbr>='.htmlentities(str_replace(',','\\,',$ary[1])) . ($html_ldap_notation == '' ? '' : ', ' . $html_ldap_notation);
  347.  
  348.                         $html_encoded_str = '#<abbr title="'._L('ASN.1: UTF8String').'">'.sprintf('%02s', strtoupper(dechex(0x0C/*UTF8String*/))).'</abbr>';
  349.                         $utf8 = vts_utf8_encode($ary[1]);
  350.                         $html_encoded_str .= '<abbr title="'._L('Length').'">'.sprintf('%02s', strtoupper(dechex(strlen($utf8)))).'</abbr>'; // TODO: This length does only work for length <= 0x7F! The correct implementation is described here: https://misc.daniel-marschall.de/asn.1/oid_facts.html#chap1_2
  351.                         $html_encoded_str .= '<abbr title="'.htmlentities($ary[1]).'">';
  352.                         for ($i=0; $i<strlen($utf8); $i++) {
  353.                                 $char = substr($utf8, $i, 1);
  354.                                 $html_encoded_str .= sprintf('%02s', strtoupper(dechex(ord($char))));
  355.                         }
  356.                         $html_encoded_str .= '</abbr>';
  357.                         $html_encoded_string_notation = '<abbr title="'.htmlentities(strtoupper($ary[0]) . ' = ' . $found_hf_name).'">'.htmlentities($found_oid).'</abbr>='.$html_encoded_str . ($html_encoded_string_notation == '' ? '' : ',' . $html_encoded_string_notation);
  358.                 }
  359.  
  360.                 $tmp = _L('DCE/MSAD notation');
  361.                 $tmp = str_replace('DCE', '<abbr title="'._L('Distributed Computing Environment').'">DCE</abbr>', $tmp);
  362.                 $tmp = str_replace('MSAD', '<abbr title="'._L('Microsoft ActiveDirectory').'">MSAD</abbr>', $tmp);
  363.                 $tech_info[$tmp] = $html_dce_ad_notation;
  364.  
  365.                 $tmp = _L('LDAP notation');
  366.                 $tmp = str_replace('LDAP', '<abbr title="'._L('Lightweight Directory Access Protocol').'">LDAP</abbr>', $tmp);
  367.                 $tech_info[$tmp] = $html_ldap_notation;
  368.  
  369.                 $tmp = _L('Encoded string notation');
  370.                 $tech_info[$tmp] = $html_encoded_string_notation;
  371.  
  372.                 return $tech_info;
  373.         }
  374.  
  375.         /**
  376.          * @param string $title
  377.          * @param string $content
  378.          * @param string $icon
  379.          * @return void
  380.          * @throws OIDplusException
  381.          */
  382.         public function getContentPage(string &$title, string &$content, string &$icon) {
  383.                 $icon = file_exists(__DIR__.'/img/main_icon.png') ? OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'img/main_icon.png' : '';
  384.  
  385.                 if ($this->isRoot()) {
  386.                         $title = OIDplusX500DN::objectTypeTitle();
  387.  
  388.                         $res = OIDplus::db()->query("select * from ###objects where parent = ?", array(self::root()));
  389.                         if ($res->any()) {
  390.                                 $content  = '<p>'._L('Please select an object in the tree view at the left to show its contents.').'</p>';
  391.                         } else {
  392.                                 $content  = '<p>'._L('Currently, no X.500 Distinguished Names are registered in the system.').'</p>';
  393.                         }
  394.  
  395.                         if (!$this->isLeafNode()) {
  396.                                 if (OIDplus::authUtils()->isAdminLoggedIn()) {
  397.                                         $content .= '<h2>'._L('Manage root objects').'</h2>';
  398.                                 } else {
  399.                                         $content .= '<h2>'._L('Available objects').'</h2>';
  400.                                 }
  401.                                 $content .= '%%CRUD%%';
  402.                         }
  403.                 } else {
  404.                         $title = $this->getTitle();
  405.  
  406.                         $tech_info = $this->getTechInfo();
  407.                         $tech_info_html = '';
  408.                         if (count($tech_info) > 0) {
  409.                                 $tech_info_html .= '<h2>'._L('Technical information').'</h2>';
  410.                                 $tech_info_html .= '<table border="0">';
  411.                                 foreach ($tech_info as $key => $value) {
  412.                                         $tech_info_html .= '<tr><td valign="top">'.$key.': </td><td><code>'.$value.'</code></td></tr>';
  413.                                 }
  414.                                 $tech_info_html .= '</table>';
  415.                         }
  416.  
  417.                         $content = $tech_info_html;
  418.  
  419.                         $content .= '<h2>'._L('Description').'</h2>%%DESC%%';
  420.  
  421.                         if (!$this->isLeafNode()) {
  422.                                 if ($this->userHasWriteRights()) {
  423.                                         $content .= '<h2>'._L('Create or change subordinate objects').'</h2>';
  424.                                 } else {
  425.                                         $content .= '<h2>'._L('Subordinate objects').'</h2>';
  426.                                 }
  427.                                 $content .= '%%CRUD%%';
  428.                         }
  429.                 }
  430.         }
  431.  
  432.         /**
  433.          * @return OIDplusX500DN|null
  434.          */
  435.         public function one_up()/*: ?OIDplusX500DN*/ {
  436.                 $oid = $this->identifier;
  437.  
  438.                 $p = strrpos($oid, '/');
  439.                 if ($p === false) return self::parse($oid);
  440.                 if ($p == 0) return self::parse('/');
  441.  
  442.                 $oid_up = substr($oid, 0, $p);
  443.  
  444.                 return self::parse(self::ns().':'.$oid_up);
  445.         }
  446.  
  447.         /**
  448.          * @param OIDplusObject|string $to
  449.          * @return int|null
  450.          */
  451.         public function distance($to) {
  452.                 if (!is_object($to)) $to = OIDplusObject::parse($to);
  453.                 if (!$to) return null;
  454.                 if (!($to instanceof $this)) return null;
  455.  
  456.                 $a = $to->identifier;
  457.                 $b = $this->identifier;
  458.  
  459.                 if (substr($a,0,1) == '/') $a = substr($a,1);
  460.                 if (substr($b,0,1) == '/') $b = substr($b,1);
  461.  
  462.                 $ary = explode('/', $a);
  463.                 $bry = explode('/', $b);
  464.  
  465.                 $min_len = min(count($ary), count($bry));
  466.  
  467.                 for ($i=0; $i<$min_len; $i++) {
  468.                         if ($ary[$i] != $bry[$i]) return null;
  469.                 }
  470.  
  471.                 return count($ary) - count($bry);
  472.         }
  473.  
  474.         /**
  475.          * @return string
  476.          */
  477.         public function getDirectoryName(): string {
  478.                 if ($this->isRoot()) return $this->ns();
  479.                 return $this->ns().'_'.md5($this->nodeId(false));
  480.         }
  481.  
  482.         /**
  483.          * @param string $mode
  484.          * @return string
  485.          */
  486.         public static function treeIconFilename(string $mode): string {
  487.                 return 'img/'.$mode.'_icon16.png';
  488.         }
  489. }
  490.