Subversion Repositories oidplus

Rev

Rev 1454 | 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. abstract class OIDplusObject extends OIDplusBaseClass {
  27.  
  28.         /**
  29.          *
  30.          */
  31.         //const UUID_NAMEBASED_NS_OidPlusMisc = 'ad1654e6-7e15-11e4-9ef6-78e3b5fc7f22';
  32.  
  33.         /**
  34.          * Please overwrite this function!
  35.          * @param string $node_id
  36.          * @return OIDplusObject|null
  37.          */
  38.         public static function parse(string $node_id)/*: ?OIDplusObject*/ {
  39.                 foreach (OIDplus::getEnabledObjectTypes() as $ot) {
  40.                         try {
  41.                                 $good = false;
  42.                                 if (get_parent_class($ot) == OIDplusObject::class) {
  43.                                         $reflector = new \ReflectionMethod($ot, 'parse');
  44.                                         $isImplemented = ($reflector->getDeclaringClass()->getName() === $ot);
  45.                                         if ($isImplemented) { // avoid endless loop if parse is not overriden
  46.                                                 $good = true;
  47.                                         }
  48.                                 }
  49.                                 // We need to do the workaround with "$good", otherwise PHPstan shows
  50.                                 // "Call to an undefined static method object::parse()"
  51.                                 if ($good && $obj = $ot::parse($node_id)) return $obj;
  52.                         } catch (\Exception $e) {}
  53.                 }
  54.                 return null;
  55.         }
  56.  
  57.         /**
  58.          * @return OIDplusAltId[]
  59.          * @throws OIDplusException
  60.          */
  61.         public function getAltIds(): array {
  62.                 if ($this->isRoot()) return array();
  63.  
  64.                 $ids = array();
  65.  
  66.                 // Create Information Object OID/AID/UUID
  67.                 // ... but not for OIDs below oid:1.3.6.1.4.1.37476.30.9, because these are the definition of these Information Object AID/OID/UUID (which will be decoded in the OID object type plugin)
  68.                 if (!str_starts_with($this->nodeId(true), 'oid:1.3.6.1.4.1.37476.30.9.')) {
  69.                         // Creates an OIDplus-Hash-OID
  70.                         // ... exclude OIDs, because an OID is already an OID
  71.                         if ($this->ns() != 'oid') {
  72.                                 $sid = OIDplus::getSystemId(true);
  73.                                 if (!empty($sid)) {
  74.                                         $ns_oid = $this->getPlugin()->getManifest()->getOid();
  75.                                         $hash_payload = $ns_oid.':'.$this->nodeId(false);
  76.                                         $oid = $sid . '.' . smallhash($hash_payload);
  77.                                         $ids[] = new OIDplusAltId('oid', $oid, _L('OIDplus Information Object OID'));
  78.                                 }
  79.                         }
  80.  
  81.                         // Make a OIDplus-UUID, but...
  82.                         // ... exclude GUID, because a GUID is already a GUID
  83.                         // ... exclude OIDs which are 2.25, because these are basically GUIDs (but 2nd, 3rd, 4th, ... level is OK)
  84.                         // Previously, we excluded OID, because an OID already has a record UUID_NAMEBASED_NS_OID (defined by IETF) set by class OIDplusOid
  85.                         if (($this->ns() != 'guid') && ($this->ns() != 'oid' || $this->one_up()->nodeId(true) != 'oid:2.25') /*&& ($this->ns() != 'oid')*/) {
  86.                                 // Obsolete custom namespace for UUIDv3 and UUIDv5:
  87.                                 //$ids[] = new OIDplusAltId('guid', gen_uuid_md5_namebased(self::UUID_NAMEBASED_NS_OidPlusMisc, $this->nodeId()), _L('Name based version 3 / MD5 UUID with namespace %1','UUID_NAMEBASED_NS_OidPlusMisc'));
  88.                                 //$ids[] = new OIDplusAltId('guid', gen_uuid_sha1_namebased(self::UUID_NAMEBASED_NS_OidPlusMisc, $this->nodeId()), _L('Name based version 5 / SHA1 UUID with namespace %1','UUID_NAMEBASED_NS_OidPlusMisc'));
  89.                                 // New custom UUIDv8:
  90.                                 $sysid = OIDplus::getSystemId(false);
  91.                                 $sysid_int = $sysid ? $sysid : 0;
  92.                                 $unix_ts = $this->getCreatedTime() ? strtotime($this->getCreatedTime()) : 0;
  93.                                 $ns_oid = $this->getPlugin()->getManifest()->getOid();
  94.                                 $obj_name = $this->nodeId(false);
  95.                                 $ids[] = new OIDplusAltId('guid',
  96.                                         gen_uuid_v8(
  97.                                                 dechex($sysid_int),
  98.                                                 dechex((int)round($unix_ts/60/60/24)),
  99.                                                 dechex(0),
  100.                                                 sha1($ns_oid), // Note: No 14bit collission between 1.3.6.1.4.1.37476.2.5.2.4.8.[0-185]
  101.                                                 sha1($obj_name)
  102.                                         ),
  103.                                         _L('OIDplus Information Object Custom UUID (RFC4122bis)'),
  104.                                         '',
  105.                                         'https://github.com/danielmarschall/oidplus/blob/master/doc/oidplus_custom_guid.md'
  106.                                         );
  107.                         }
  108.  
  109.                         // Make a AID based on ViaThinkSoft schema
  110.                         // ... exclude AIDs, because an AID is already an AID
  111.                         if ($this->ns() != 'aid') {
  112.                                 $sid = OIDplus::getSystemId(false);
  113.                                 if ($sid !== false) {
  114.                                         $ns_oid = $this->getPlugin()->getManifest()->getOid();
  115.                                         $hash_payload = $ns_oid.':'.$this->nodeId(false);
  116.                                         $sid_hex = strtoupper(str_pad(dechex((int)$sid),8,'0',STR_PAD_LEFT));
  117.                                         $obj_hex = strtoupper(str_pad(dechex(smallhash($hash_payload)),8,'0',STR_PAD_LEFT));
  118.                                         $aid = 'D276000186B20005'.$sid_hex.$obj_hex;
  119.                                         $ids[] = new OIDplusAltId('aid', $aid,
  120.                                                 _L('OIDplus Information Object Application Identifier (ISO/IEC 7816)'),
  121.                                                 ' ('._L('No PIX allowed').')',
  122.                                                 'https://hosted.oidplus.com/viathinksoft/?goto=aid%3AD276000186B20005');
  123.                                 }
  124.                         }
  125.  
  126.                         // Make a MAC based on AAI (not 100% worldwide unique!)
  127.                         // ... exclude MACs, because an MAC is already a MAC
  128.                         if ($this->ns() != 'mac') {
  129.                                 $ns_oid = $this->getPlugin()->getManifest()->getOid();
  130.                                 $obj_name = $this->nodeId(false);
  131.                                 $mac = strtoupper(substr(sha1($ns_oid.':'.$obj_name),-12));
  132.                                 $mac = rtrim(chunk_split($mac, 2, '-'),'-');
  133.  
  134.                                 $mac[1] = '2'; // 2=AAI Unicast
  135.                                 $ids[] = new OIDplusAltId('mac', $mac, _L('OIDplus Information Object MAC address, Unicast (AAI)'));
  136.  
  137.                                 $mac[1] = '3'; // 3=AAI Multicast
  138.                                 $ids[] = new OIDplusAltId('mac', $mac, _L('OIDplus Information Object MAC address, Multicast (AAI)'));
  139.                         }
  140.  
  141.                         // Make a DN based on DN
  142.                         // ... exclude DN, because an DN is already a DN
  143.                         if ($this->ns() != 'x500dn') {
  144.                                 $sysid = OIDplus::getSystemId(false);
  145.                                 if ($sysid !== false) {
  146.                                         $ns_oid = $this->getPlugin()->getManifest()->getOid();
  147.                                         $hash_payload = $ns_oid.':'.$this->nodeId(false);
  148.                                         $objhash = smallhash($hash_payload);
  149.  
  150.                                         $oid_at_sysid = '1.3.6.1.4.1.37476.2.5.2.9.4.1';
  151.                                         $oid_at_objhash = '1.3.6.1.4.1.37476.2.5.2.9.4.2';
  152.                                         $dn = '/dc=com/dc=example/cn=oidplus/'.$oid_at_sysid.'='.$sysid.'/'.$oid_at_objhash.'='.$objhash;
  153.  
  154.                                         $ids[] = new OIDplusAltId('x500dn', $dn, _L('OIDplus Information Object X.500 DN'));
  155.                                 }
  156.                         }
  157.                 }
  158.  
  159.                 return $ids;
  160.         }
  161.  
  162.         /**
  163.          * @return string
  164.          */
  165.         public abstract static function objectTypeTitle(): string;
  166.  
  167.         /**
  168.          * @return string
  169.          */
  170.         public abstract static function objectTypeTitleShort(): string;
  171.  
  172.         /**
  173.          * @return OIDplusObjectTypePlugin|null
  174.          */
  175.         public function getPlugin()/*: ?OIDplusObjectTypePlugin */ {
  176.                 $plugins = OIDplus::getObjectTypePlugins();
  177.                 foreach ($plugins as $plugin) {
  178.                         if (get_class($this) == $plugin::getObjectTypeClassName()) {
  179.                                 return $plugin;
  180.                         }
  181.                 }
  182.                 return null;
  183.         }
  184.  
  185.         /**
  186.          * @return string
  187.          */
  188.         public abstract static function ns(): string;
  189.  
  190.         /**
  191.          * @return string
  192.          */
  193.         public abstract static function root(): string;
  194.  
  195.         /**
  196.          * @return bool
  197.          */
  198.         public abstract function isRoot(): bool;
  199.  
  200.         /**
  201.          * @param bool $with_ns
  202.          * @return string
  203.          */
  204.         public abstract function nodeId(bool $with_ns=true): string;
  205.  
  206.         /**
  207.          * @param string $str
  208.          * @return string mixed
  209.          * @throws OIDplusException
  210.          */
  211.         public abstract function addString(string $str): string;
  212.  
  213.         /**
  214.          * @param OIDplusObject $parent
  215.          * @return string
  216.          */
  217.         public abstract function crudShowId(OIDplusObject $parent): string;
  218.  
  219.         /**
  220.          * @return string
  221.          */
  222.         public function crudInsertPrefix(): string {
  223.                 return '';
  224.         }
  225.  
  226.         /**
  227.          * @return string
  228.          */
  229.         public function crudInsertSuffix(): string {
  230.                 return '';
  231.         }
  232.  
  233.         /**
  234.          * @param OIDplusObject|null $parent
  235.          * @return string
  236.          */
  237.         public abstract function jsTreeNodeName(OIDplusObject $parent = null): string;
  238.  
  239.         /**
  240.          * @return string
  241.          */
  242.         public abstract function defaultTitle(): string;
  243.  
  244.         /**
  245.          * @return bool
  246.          */
  247.         public abstract function isLeafNode(): bool;
  248.  
  249.         /**
  250.          * @param string $title
  251.          * @param string $content
  252.          * @param string $icon
  253.          * @return void
  254.          */
  255.         public abstract function getContentPage(string &$title, string &$content, string &$icon);
  256.  
  257.         /**
  258.          * @param OIDplusRA|string|null $ra
  259.          * @return array
  260.          * @throws OIDplusConfigInitializationException
  261.          * @throws OIDplusException
  262.          */
  263.         public static function getRaRoots($ra=null) : array{
  264.                 if ($ra instanceof OIDplusRA) $ra = $ra->raEmail();
  265.  
  266.                 $out = array();
  267.  
  268.                 if (!OIDplus::baseConfig()->getValue('OBJECT_CACHING', true)) {
  269.                         if (!$ra) {
  270.                                 $res = OIDplus::db()->query("select oChild.id as child_id, oChild.ra_email as child_mail, oParent.ra_email as parent_mail from ###objects as oChild ".
  271.                                                             "left join ###objects as oParent on oChild.parent = oParent.id");
  272.                                 $res->naturalSortByField('child_id');
  273.                                 while ($row = $res->fetch_array()) {
  274.                                         if (!OIDplus::authUtils()->isRaLoggedIn($row['parent_mail']) && OIDplus::authUtils()->isRaLoggedIn($row['child_mail'])) {
  275.                                                 $x = self::parse($row['child_id']); // can be NULL if namespace was disabled
  276.                                                 if ($x) $out[] = $x;
  277.                                         }
  278.                                 }
  279.                         } else {
  280.                                 $res = OIDplus::db()->query("select oChild.id as child_id from ###objects as oChild ".
  281.                                                             "left join ###objects as oParent on oChild.parent = oParent.id ".
  282.                                                             "where (".OIDplus::db()->getSlang()->isNullFunction('oParent.ra_email',"''")." <> ? and ".
  283.                                                             OIDplus::db()->getSlang()->isNullFunction('oChild.ra_email',"''")." = ?) or ".
  284.                                                             "      (oParent.ra_email is null and ".OIDplus::db()->getSlang()->isNullFunction('oChild.ra_email',"''")." = ?) ",
  285.                                                             array($ra, $ra, $ra));
  286.                                 $res->naturalSortByField('child_id');
  287.                                 while ($row = $res->fetch_array()) {
  288.                                         $x = self::parse($row['child_id']); // can be NULL if namespace was disabled
  289.                                         if ($x) $out[] = $x;
  290.                                 }
  291.                         }
  292.                 } else {
  293.                         if (!$ra) {
  294.                                 $ra_mails_to_check = OIDplus::authUtils()->loggedInRaList();
  295.                                 if (count($ra_mails_to_check) == 0) return $out;
  296.                         } else {
  297.                                 $ra_mails_to_check = array($ra);
  298.                         }
  299.  
  300.                         self::buildObjectInformationCache();
  301.  
  302.                         foreach ($ra_mails_to_check as $check_ra_mail) {
  303.                                 $out_part = array();
  304.  
  305.                                 foreach (self::$object_info_cache as $id => $cacheitem) {
  306.                                         if ($cacheitem[self::CACHE_RA_EMAIL] == $check_ra_mail) {
  307.                                                 $parent = $cacheitem[self::CACHE_PARENT];
  308.                                                 if (!isset(self::$object_info_cache[$parent]) || (self::$object_info_cache[$parent][self::CACHE_RA_EMAIL] != $check_ra_mail)) {
  309.                                                         $out_part[] = $id;
  310.                                                 }
  311.                                         }
  312.                                 }
  313.  
  314.                                 natsort($out_part);
  315.  
  316.                                 foreach ($out_part as $id) {
  317.                                         $obj = self::parse($id);
  318.                                         if ($obj) $out[] = $obj;
  319.                                 }
  320.                         }
  321.                 }
  322.  
  323.                 return $out;
  324.         }
  325.  
  326.         /**
  327.          * @return array
  328.          * @throws OIDplusException
  329.          */
  330.         public static function getAllNonConfidential(): array {
  331.                 $out = array();
  332.  
  333.                 if (!OIDplus::baseConfig()->getValue('OBJECT_CACHING', true)) {
  334.                         $res = OIDplus::db()->query("select id from ###objects where confidential = ?", array(false));
  335.                         $res->naturalSortByField('id');
  336.                         while ($row = $res->fetch_array()) {
  337.                                 $obj = self::parse($row['id']); // will be NULL if the object type is not registered
  338.                                 if ($obj && (!$obj->isConfidential())) {
  339.                                         $out[] = $row['id'];
  340.                                 }
  341.                         }
  342.                 } else {
  343.                         self::buildObjectInformationCache();
  344.  
  345.                         foreach (self::$object_info_cache as $id => $cacheitem) {
  346.                                 $confidential = $cacheitem[self::CACHE_CONFIDENTIAL];
  347.                                 if (!$confidential) {
  348.                                         $obj = self::parse($id); // will be NULL if the object type is not registered
  349.                                         if ($obj && (!$obj->isConfidential())) {
  350.                                                 $out[] = $id;
  351.                                         }
  352.                                 }
  353.                         }
  354.                 }
  355.  
  356.                 return $out;
  357.         }
  358.  
  359.         /**
  360.          * @return bool
  361.          * @throws OIDplusException
  362.          */
  363.         public function isConfidential(): bool {
  364.                 if (!OIDplus::baseConfig()->getValue('OBJECT_CACHING', true)) {
  365.                         //static $confidential_cache = array();
  366.                         $curid = $this->nodeId();
  367.                         //$orig_curid = $curid;
  368.                         //if (isset($confidential_cache[$curid])) return $confidential_cache[$curid];
  369.                         // Recursively search for the confidential flag in the parents
  370.                         while (($res = OIDplus::db()->query("select parent, confidential from ###objects where id = ?", array($curid)))->any()) {
  371.                                 $row = $res->fetch_array();
  372.                                 if ($row['confidential']) {
  373.                                         //$confidential_cache[$curid] = true;
  374.                                         //$confidential_cache[$orig_curid] = true;
  375.                                         return true;
  376.                                 } else {
  377.                                         //$confidential_cache[$curid] = false;
  378.                                 }
  379.                                 $curid = $row['parent'];
  380.                                 //if (isset($confidential_cache[$curid])) {
  381.                                         //$confidential_cache[$orig_curid] = $confidential_cache[$curid];
  382.                                         //return $confidential_cache[$curid];
  383.                                 //}
  384.                         }
  385.  
  386.                         //$confidential_cache[$orig_curid] = false;
  387.                         return false;
  388.                 } else {
  389.                         self::buildObjectInformationCache();
  390.  
  391.                         $curid = $this->nodeId();
  392.                         // Recursively search for the confidential flag in the parents
  393.                         while (isset(self::$object_info_cache[$curid])) {
  394.                                 if (self::$object_info_cache[$curid][self::CACHE_CONFIDENTIAL]) return true;
  395.                                 $curid = self::$object_info_cache[$curid][self::CACHE_PARENT];
  396.                         }
  397.                         return false;
  398.                 }
  399.         }
  400.  
  401.         /**
  402.          * @param OIDplusObject $obj
  403.          * @return bool
  404.          * @throws OIDplusException
  405.          */
  406.         public function isChildOf(OIDplusObject $obj): bool {
  407.                 if (!OIDplus::baseConfig()->getValue('OBJECT_CACHING', true)) {
  408.                         $curid = $this->nodeId();
  409.                         while (($res = OIDplus::db()->query("select parent from ###objects where id = ?", array($curid)))->any()) {
  410.                                 $row = $res->fetch_array();
  411.                                 if ($curid == $obj->nodeId()) return true;
  412.                                 $curid = $row['parent'];
  413.                         }
  414.                         return false;
  415.                 } else {
  416.                         self::buildObjectInformationCache();
  417.  
  418.                         $curid = $this->nodeId();
  419.                         while (isset(self::$object_info_cache[$curid])) {
  420.                                 if ($curid == $obj->nodeId()) return true;
  421.                                 $curid = self::$object_info_cache[$curid][self::CACHE_PARENT];
  422.                         }
  423.                         return false;
  424.                 }
  425.         }
  426.  
  427.         /**
  428.          * @return array
  429.          * @throws OIDplusException
  430.          */
  431.         public function getChildren(): array {
  432.                 $out = array();
  433.                 if (!OIDplus::baseConfig()->getValue('OBJECT_CACHING', true)) {
  434.                         $res = OIDplus::db()->query("select id from ###objects where parent = ?", array($this->nodeId()));
  435.                         while ($row = $res->fetch_array()) {
  436.                                 $obj = self::parse($row['id']);
  437.                                 if (!$obj) continue;
  438.                                 $out[] = $obj;
  439.                         }
  440.                 } else {
  441.                         self::buildObjectInformationCache();
  442.  
  443.                         foreach (self::$object_info_cache as $id => $cacheitem) {
  444.                                 $parent = $cacheitem[self::CACHE_PARENT];
  445.                                 if ($parent == $this->nodeId()) {
  446.                                         $obj = self::parse($id);
  447.                                         if (!$obj) continue;
  448.                                         $out[] = $obj;
  449.                                 }
  450.                         }
  451.                 }
  452.                 return $out;
  453.         }
  454.  
  455.         /**
  456.          * @return OIDplusRA|null
  457.          * @throws OIDplusException
  458.          */
  459.         public function getRa()/*: ?OIDplusRA*/ {
  460.                 $ra = $this->getRaMail();
  461.                 return $ra ? new OIDplusRA($ra) : null;
  462.         }
  463.  
  464.         /**
  465.          * @param OIDplusRA|string|null $ra
  466.          * @return bool
  467.          * @throws OIDplusConfigInitializationException
  468.          * @throws OIDplusException
  469.          */
  470.         public function userHasReadRights($ra=null): bool {
  471.                 if ($ra instanceof OIDplusRA) $ra = $ra->raEmail();
  472.  
  473.                 // If it is not confidential, everybody can read/see it.
  474.                 // Note: This also checks if superior OIDs are confidential.
  475.                 if (!$this->isConfidential()) return true;
  476.  
  477.                 if (!$ra) {
  478.                         // Admin may do everything
  479.                         if (OIDplus::authUtils()->isAdminLoggedIn()) return true;
  480.  
  481.                         // If the RA is logged in, then they can see the OID.
  482.                         $ownRa = $this->getRaMail();
  483.                         if ($ownRa && OIDplus::authUtils()->isRaLoggedIn($ownRa)) return true;
  484.                 } else {
  485.                         // If this OID belongs to the requested RA, then they may see it.
  486.                         if ($this->getRaMail() == $ra) return true;
  487.                 }
  488.  
  489.                 // If someone has rights to an object below our confidential node,
  490.                 // we let him see the confidential node,
  491.                 // Otherwise he could not browse through to his own node.
  492.                 $roots = $this->getRaRoots($ra);
  493.                 foreach ($roots as $root) {
  494.                         if ($root->isChildOf($this)) return true;
  495.                 }
  496.  
  497.                 return false;
  498.         }
  499.  
  500.         /**
  501.          * @param array|null $row
  502.          * @return string|null
  503.          * @throws OIDplusException
  504.          */
  505.         public function getIcon(array $row=null) {
  506.                 $namespace = $this->ns(); // must use $this, not self::, otherwise the virtual method will not be called
  507.  
  508.                 if (is_null($row)) {
  509.                         $ra_email = $this->getRaMail();
  510.                 } else {
  511.                         $ra_email = $row['ra_email'];
  512.                 }
  513.  
  514.                 // $dirs = glob(OIDplus::localpath().'plugins/'.'*'.'/objectTypes/'.$namespace.'/');
  515.                 // if (count($dirs) == 0) return null; // default icon (folder)
  516.                 // $dir = substr($dirs[0], strlen(OIDplus::localpath()));
  517.                 $reflection = new \ReflectionClass($this);
  518.                 $dir = dirname($reflection->getFilename());
  519.                 $dir = substr($dir, strlen(OIDplus::localpath()));
  520.                 $dir = str_replace('\\', '/', $dir);
  521.  
  522.                 if ($this->isRoot()) {
  523.                         $icon = $dir . '/' . $this::treeIconFilename('root');
  524.                 } else {
  525.                         // We use $this:: instead of self:: , because we want to call the overridden methods
  526.                         if ($ra_email && OIDplus::authUtils()->isRaLoggedIn($ra_email)) {
  527.                                 if ($this->isLeafNode()) {
  528.                                         $icon = $dir . '/' . $this::treeIconFilename('own_leaf');
  529.                                         if (!file_exists($icon)) $icon = $dir . '/' . $this::treeIconFilename('own');
  530.                                 } else {
  531.                                         $icon = $dir . '/' . $this::treeIconFilename('own');
  532.                                 }
  533.                         } else {
  534.                                 if ($this->isLeafNode()) {
  535.                                         $icon = $dir . '/' . $this::treeIconFilename('general_leaf');
  536.                                         if (!file_exists($icon)) $icon = $dir . '/' . $this::treeIconFilename('general');
  537.                                 } else {
  538.                                         $icon = $dir . '/' . $this::treeIconFilename('general');
  539.                                 }
  540.                         }
  541.                 }
  542.  
  543.                 if (!file_exists($icon)) return null; // default icon (folder)
  544.  
  545.                 return $icon;
  546.         }
  547.  
  548.         /**
  549.          * @param string $id
  550.          * @return bool
  551.          * @throws OIDplusException
  552.          */
  553.         public static function exists(string $id): bool {
  554.                 if (!OIDplus::baseConfig()->getValue('OBJECT_CACHING', true)) {
  555.                         $res = OIDplus::db()->query("select id from ###objects where id = ?", array($id));
  556.                         return $res->any();
  557.                 } else {
  558.                         self::buildObjectInformationCache();
  559.                         return isset(self::$object_info_cache[$id]);
  560.                 }
  561.         }
  562.  
  563.         /**
  564.          * Get parent gives the next possible parent which is EXISTING in OIDplus
  565.          * It does not give the immediate parent
  566.          * @return OIDplusObject|null
  567.          * @throws OIDplusException
  568.          */
  569.         public function getParent()/*: ?OIDplusObject*/ {
  570.                 if (!OIDplus::baseConfig()->getValue('OBJECT_CACHING', true)) {
  571.                         $res = OIDplus::db()->query("select parent from ###objects where id = ?", array($this->nodeId()));
  572.                         if ($res->any()) {
  573.                                 $row = $res->fetch_array();
  574.                                 $parent = $row['parent'];
  575.                                 $obj = OIDplusObject::parse($parent);
  576.                                 if ($obj) return $obj;
  577.                         }
  578.                 } else {
  579.                         self::buildObjectInformationCache();
  580.                         if (isset(self::$object_info_cache[$this->nodeId()])) {
  581.                                 $parent = self::$object_info_cache[$this->nodeId()][self::CACHE_PARENT];
  582.                                 $obj = OIDplusObject::parse($parent);
  583.                                 if ($obj) return $obj;
  584.                         }
  585.                 }
  586.  
  587.                 // If this OID does not exist, the SQL query "select parent from ..." does not work. So we try to find the next possible parent using one_up()
  588.                 $cur = $this->one_up();
  589.                 if (!$cur) return null;
  590.                 do {
  591.                         // findFitting() checks if that OID exists
  592.                         if ($fitting = self::findFitting($cur->nodeId())) return $fitting;
  593.  
  594.                         $prev = $cur;
  595.                         $cur = $cur->one_up();
  596.                         if (!$cur) return null;
  597.                 } while ($prev->nodeId() !== $cur->nodeId());
  598.  
  599.                 return null;
  600.         }
  601.  
  602.         /**
  603.          * @return string|null
  604.          * @throws OIDplusException
  605.          */
  606.         public function getRaMail() {
  607.                 if (!OIDplus::baseConfig()->getValue('OBJECT_CACHING', true)) {
  608.                         $res = OIDplus::db()->query("select ra_email from ###objects where id = ?", array($this->nodeId()));
  609.                         if (!$res->any()) return null;
  610.                         $row = $res->fetch_array();
  611.                         return $row['ra_email'];
  612.                 } else {
  613.                         self::buildObjectInformationCache();
  614.                         if (isset(self::$object_info_cache[$this->nodeId()])) {
  615.                                 return self::$object_info_cache[$this->nodeId()][self::CACHE_RA_EMAIL];
  616.                         }
  617.                         return null;
  618.                 }
  619.         }
  620.  
  621.         /**
  622.          * @return string|null
  623.          * @throws OIDplusException
  624.          */
  625.         public function getTitle() {
  626.                 if (!OIDplus::baseConfig()->getValue('OBJECT_CACHING', true)) {
  627.                         $res = OIDplus::db()->query("select title from ###objects where id = ?", array($this->nodeId()));
  628.                         if (!$res->any()) return null;
  629.                         $row = $res->fetch_array();
  630.                         return $row['title'];
  631.                 } else {
  632.                         self::buildObjectInformationCache();
  633.                         if (isset(self::$object_info_cache[$this->nodeId()])) {
  634.                                 return self::$object_info_cache[$this->nodeId()][self::CACHE_TITLE];
  635.                         }
  636.                         return null;
  637.                 }
  638.         }
  639.  
  640.         /**
  641.          * @return string|null
  642.          * @throws OIDplusException
  643.          */
  644.         public function getDescription() {
  645.                 if (!OIDplus::baseConfig()->getValue('OBJECT_CACHING', true)) {
  646.                         $res = OIDplus::db()->query("select description from ###objects where id = ?", array($this->nodeId()));
  647.                         if (!$res->any()) return null;
  648.                         $row = $res->fetch_array();
  649.                         return $row['description'];
  650.                 } else {
  651.                         self::buildObjectInformationCache();
  652.                         if (isset(self::$object_info_cache[$this->nodeId()])) {
  653.                                 return self::$object_info_cache[$this->nodeId()][self::CACHE_DESCRIPTION];
  654.                         }
  655.                         return null;
  656.                 }
  657.         }
  658.  
  659.         /**
  660.          * @return string|null
  661.          * @throws OIDplusException
  662.          */
  663.         public function getComment() {
  664.                 if (!OIDplus::baseConfig()->getValue('OBJECT_CACHING', true)) {
  665.                         $res = OIDplus::db()->query("select comment from ###objects where id = ?", array($this->nodeId()));
  666.                         if (!$res->any()) return null;
  667.                         $row = $res->fetch_array();
  668.                         return $row['comment'];
  669.                 } else {
  670.                         self::buildObjectInformationCache();
  671.                         if (isset(self::$object_info_cache[$this->nodeId()])) {
  672.                                 return self::$object_info_cache[$this->nodeId()][self::CACHE_COMMENT];
  673.                         }
  674.                         return null;
  675.                 }
  676.         }
  677.  
  678.         /**
  679.          * @return string|null
  680.          * @throws OIDplusException
  681.          */
  682.         public function getCreatedTime() {
  683.                 if (!OIDplus::baseConfig()->getValue('OBJECT_CACHING', true)) {
  684.                         $res = OIDplus::db()->query("select created from ###objects where id = ?", array($this->nodeId()));
  685.                         if (!$res->any()) return null;
  686.                         $row = $res->fetch_array();
  687.                         return $row['created'];
  688.                 } else {
  689.                         self::buildObjectInformationCache();
  690.                         if (isset(self::$object_info_cache[$this->nodeId()])) {
  691.                                 return self::$object_info_cache[$this->nodeId()][self::CACHE_CREATED];
  692.                         }
  693.                         return null;
  694.                 }
  695.         }
  696.  
  697.         /**
  698.          * @return string|null
  699.          * @throws OIDplusException
  700.          */
  701.         public function getUpdatedTime() {
  702.                 if (!OIDplus::baseConfig()->getValue('OBJECT_CACHING', true)) {
  703.                         $res = OIDplus::db()->query("select updated from ###objects where id = ?", array($this->nodeId()));
  704.                         if (!$res->any()) return null;
  705.                         $row = $res->fetch_array();
  706.                         return $row['updated'];
  707.                 } else {
  708.                         self::buildObjectInformationCache();
  709.                         if (isset(self::$object_info_cache[$this->nodeId()])) {
  710.                                 return self::$object_info_cache[$this->nodeId()][self::CACHE_UPDATED];
  711.                         }
  712.                         return null;
  713.                 }
  714.         }
  715.  
  716.         /**
  717.          * @param OIDplusRA|string|null $ra
  718.          * @return bool
  719.          * @throws OIDplusException
  720.          */
  721.         public function userHasParentalWriteRights($ra=null): bool {
  722.                 if ($ra instanceof OIDplusRA) $ra = $ra->raEmail();
  723.  
  724.                 if (!$ra) {
  725.                         if (OIDplus::authUtils()->isAdminLoggedIn()) return true;
  726.                 }
  727.  
  728.                 $objParent = $this->getParent();
  729.                 if (!$objParent) return false;
  730.                 return $objParent->userHasWriteRights($ra);
  731.         }
  732.  
  733.         /**
  734.          * @param OIDplusRA|string|null $ra
  735.          * @return bool
  736.          * @throws OIDplusException
  737.          */
  738.         public function userHasWriteRights($ra=null): bool {
  739.                 if ($ra instanceof OIDplusRA) $ra = $ra->raEmail();
  740.  
  741.                 if (!$ra) {
  742.                         if (OIDplus::authUtils()->isAdminLoggedIn()) return true;
  743.                         // TODO: should we allow that the parent RA also may update title/description about this OID (since they delegated it?)
  744.                         $ownRa = $this->getRaMail();
  745.                         return $ownRa && OIDplus::authUtils()->isRaLoggedIn($ownRa);
  746.                 } else {
  747.                         return $this->getRaMail() == $ra;
  748.                 }
  749.         }
  750.  
  751.         /**
  752.          * @param string|OIDplusObject $to
  753.          * @return int|null
  754.          */
  755.         public function distance($to)/*: ?int*/ {
  756.                 return null; // not implemented
  757.         }
  758.  
  759.         /**
  760.          * @param OIDplusObject|string $obj
  761.          * @return bool
  762.          */
  763.         public function equals($obj): bool {
  764.                 if (!$obj) return false;
  765.                 if (!is_object($obj)) {
  766.                         if ($this->nodeId(true) === $obj) return true; // simplest case
  767.                         $obj = OIDplusObject::parse($obj);
  768.                         if (!$obj) return false;
  769.                 } else {
  770.                         if ($this->nodeId(true) === $obj->nodeId(true)) return true; // simplest case
  771.                 }
  772.                 if (!($obj instanceof $this)) return false;
  773.  
  774.                 $distance = $this->distance($obj);
  775.                 if (is_numeric($distance)) return $distance === 0; // if the distance function is implemented, use it
  776.  
  777.                 return $this->nodeId() == $obj->nodeId(); // otherwise compare the node id case-sensitive
  778.         }
  779.  
  780.         /**
  781.          * @param string $search_id
  782.          * @return OIDplusObject|false
  783.          * @throws OIDplusException
  784.          */
  785.         public static function findFitting(string $search_id) {
  786.                 $obj = OIDplusObject::parse($search_id);
  787.                 if (!$obj) return false; // e.g. if ObjectType plugin is disabled
  788.  
  789.                 if ($obj->nodeId(false) == '') return false; // speed optimization. "oid:" is not equal to any object in the database
  790.  
  791.                 if (!OIDplus::baseConfig()->getValue('OBJECT_CACHING', true)) {
  792.                         $res = OIDplus::db()->query("select id from ###objects where id like ?", array($obj->ns().':%'));
  793.                         while ($row = $res->fetch_object()) {
  794.                                 $test = OIDplusObject::parse($row->id);
  795.                                 if ($test && $obj->equals($test)) return $test;
  796.                         }
  797.                         return false;
  798.                 } else {
  799.                         self::buildObjectInformationCache();
  800.                         foreach (self::$object_info_cache as $id => $cacheitem) {
  801.                                 if (strpos($id, $obj->ns().':') === 0) {
  802.                                         $test = OIDplusObject::parse($id);
  803.                                         if ($test && $obj->equals($test)) return $test;
  804.                                 }
  805.                         }
  806.                         return false;
  807.                 }
  808.         }
  809.  
  810.         /**
  811.          * @return OIDplusObject|null
  812.          */
  813.         public function one_up()/*: ?OIDplusObject*/ {
  814.                 return null; // not implemented
  815.         }
  816.  
  817.         // Caching stuff
  818.  
  819.         protected static $object_info_cache = null;
  820.  
  821.         /**
  822.          * @return void
  823.          */
  824.         public static function resetObjectInformationCache() {
  825.                 self::$object_info_cache = null;
  826.         }
  827.  
  828.         const CACHE_ID = 'id';
  829.         const CACHE_PARENT = 'parent';
  830.         const CACHE_TITLE = 'title';
  831.         const CACHE_DESCRIPTION = 'description';
  832.         const CACHE_RA_EMAIL = 'ra_email';
  833.         const CACHE_CONFIDENTIAL = 'confidential';
  834.         const CACHE_CREATED = 'created';
  835.         const CACHE_UPDATED = 'updated';
  836.         const CACHE_COMMENT = 'comment';
  837.  
  838.         /**
  839.          * @return void
  840.          * @throws OIDplusException
  841.          */
  842.         private static function buildObjectInformationCache() {
  843.                 if (is_null(self::$object_info_cache)) {
  844.                         self::$object_info_cache = array();
  845.                         $res = OIDplus::db()->query("select * from ###objects");
  846.                         while ($row = $res->fetch_array()) {
  847.                                 self::$object_info_cache[$row['id']] = $row;
  848.                         }
  849.                 }
  850.         }
  851.  
  852.         /**
  853.          * override this function if you want your object type to save
  854.          * attachments in directories with easy names.
  855.          * Take care that your custom directory name will not allow jailbreaks (../) !
  856.          * @return string
  857.          * @throws OIDplusException
  858.          */
  859.         public function getDirectoryName(): string {
  860.                 if ($this->isRoot()) return $this->ns();
  861.                 return $this->getLegacyDirectoryName();
  862.         }
  863.  
  864.         /**
  865.          * @return string
  866.          * @throws OIDplusException
  867.          */
  868.         public final function getLegacyDirectoryName(): string {
  869.                 if ($this::ns() == 'oid') {
  870.                         $oid = $this->nodeId(false);
  871.                 } else {
  872.                         $oid = null;
  873.                         $alt_ids = $this->getAltIds();
  874.                         foreach ($alt_ids as $alt_id) {
  875.                                 if ($alt_id->getNamespace() == 'oid') {
  876.                                         $oid = $alt_id->getId();
  877.                                         break; // we prefer the first OID (for GUIDs, the first OID is the OIDplus-OID, and the second OID is the UUID OID)
  878.                                 }
  879.                         }
  880.                 }
  881.  
  882.                 if (!is_null($oid) && ($oid != '')) {
  883.                         // For OIDs, it is the OID, for other identifiers
  884.                         // it it the OID alt ID (generated using the SystemID)
  885.                         return str_replace('.', '_', $oid);
  886.                 } else {
  887.                         // Can happen if you don't have a system ID (due to missing OpenSSL plugin)
  888.                         return md5($this->nodeId(true)); // we don't use $id, because $this->nodeId(true) is possibly more canonical than $id
  889.                 }
  890.         }
  891.  
  892.         /**
  893.          * @param string $mode
  894.          * @return string
  895.          */
  896.         public static function treeIconFilename(string $mode): string {
  897.                 // for backwards-compatibility with older plugins
  898.                 return 'img/treeicon_'.$mode.'.png';
  899.         }
  900.  
  901. }
  902.