Subversion Repositories oidplus

Rev

Rev 1086 | Rev 1121 | 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 OIDplusGs1 extends OIDplusObject {
  27.         private $number;
  28.  
  29.         /**
  30.          * @param $number
  31.          */
  32.         public function __construct($number) {
  33.                 // TODO: syntax checks
  34.                 $this->number = $number;
  35.         }
  36.  
  37.         /**
  38.          * @param string $node_id
  39.          * @return OIDplusGs1|null
  40.          */
  41.         public static function parse(string $node_id)/*: ?OIDplusGs1*/ {
  42.                 @list($namespace, $number) = explode(':', $node_id, 2);
  43.                 if ($namespace !== self::ns()) return null;
  44.                 return new self($number);
  45.         }
  46.  
  47.         /**
  48.          * @return string
  49.          */
  50.         public static function objectTypeTitle(): string {
  51.                 return _L('GS1 Based IDs (GLN/GTIN/SSCC/...)');
  52.         }
  53.  
  54.         /**
  55.          * @return string
  56.          */
  57.         public static function objectTypeTitleShort(): string {
  58.                 return _L('GS1');
  59.         }
  60.  
  61.         /**
  62.          * @return string
  63.          */
  64.         public static function ns(): string {
  65.                 return 'gs1';
  66.         }
  67.  
  68.         /**
  69.          * @return string
  70.          */
  71.         public static function root(): string {
  72.                 return self::ns().':';
  73.         }
  74.  
  75.         /**
  76.          * @return bool
  77.          */
  78.         public function isRoot(): bool {
  79.                 return $this->number == '';
  80.         }
  81.  
  82.         /**
  83.          * @param bool $with_ns
  84.          * @return string
  85.          */
  86.         public function nodeId(bool $with_ns=true): string {
  87.                 return $with_ns ? self::root().$this->number : $this->number;
  88.         }
  89.  
  90.         /**
  91.          * @param string $str
  92.          * @return string
  93.          * @throws OIDplusException
  94.          */
  95.         public function addString(string $str): string {
  96.                 $m = array();
  97.                 if (!preg_match('@^\\d+$@', $str, $m)) {
  98.                         throw new OIDplusException(_L('GS1 value needs to be numeric'));
  99.                 }
  100.  
  101.                 return $this->nodeId() . $str;
  102.         }
  103.  
  104.         /**
  105.          * @param OIDplusObject $parent
  106.          * @return string
  107.          */
  108.         public function crudShowId(OIDplusObject $parent): string {
  109.                 return $this->chunkedNotation(false);
  110.         }
  111.  
  112.         /**
  113.          * @return string
  114.          */
  115.         public function crudInsertPrefix(): string {
  116.                 return $this->isRoot() ? '' : $this->chunkedNotation(false);
  117.         }
  118.  
  119.         /**
  120.          * @param OIDplusObject|null $parent
  121.          * @return string
  122.          */
  123.         public function jsTreeNodeName(OIDplusObject $parent = null): string {
  124.                 if ($parent == null) return $this->objectTypeTitle();
  125.                 return substr($this->nodeId(), strlen($parent->nodeId()));
  126.         }
  127.  
  128.         /**
  129.          * @return string
  130.          */
  131.         public function defaultTitle(): string {
  132.                 return $this->number;
  133.         }
  134.  
  135.         /**
  136.          * @return bool
  137.          */
  138.         public function isLeafNode(): bool {
  139.                 return !$this->isBaseOnly();
  140.         }
  141.  
  142.         /**
  143.          * @param string $title
  144.          * @param string $content
  145.          * @param string $icon
  146.          * @return void
  147.          * @throws OIDplusException
  148.          */
  149.         public function getContentPage(string &$title, string &$content, string &$icon) {
  150.                 $icon = file_exists(__DIR__.'/img/main_icon.png') ? OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'img/main_icon.png' : '';
  151.  
  152.                 if ($this->isRoot()) {
  153.                         $title = OIDplusGs1::objectTypeTitle();
  154.  
  155.                         $res = OIDplus::db()->query("select * from ###objects where parent = ?", array(self::root()));
  156.                         if ($res->any()) {
  157.                                 $content  = '<p>'._L('Please select an item in the tree view at the left to show its contents.').'</p>';
  158.                         } else {
  159.                                 $content  = '<p>'._L('Currently, no GS1 based numbers are registered in the system.').'</p>';
  160.                         }
  161.  
  162.                         if (!$this->isLeafNode()) {
  163.                                 if (OIDplus::authUtils()->isAdminLoggedIn()) {
  164.                                         $content .= '<h2>'._L('Manage root objects').'</h2>';
  165.                                 } else {
  166.                                         $content .= '<h2>'._L('Available objects').'</h2>';
  167.                                 }
  168.                                 $content .= '%%CRUD%%';
  169.                         }
  170.                 } else {
  171.                         $title = $this->getTitle();
  172.  
  173.                         if ($this->isLeafNode()) {
  174.                                 $chunked = $this->chunkedNotation(true);
  175.                                 $checkDigit = $this->checkDigit();
  176.                                 $content = '<h2>'.$chunked.' - <abbr title="'._L('check digit').'">'.$checkDigit.'</abbr></h2>';
  177.                                 $content .= '<p><a target="_blank" href="https://www.ean-search.org/?q='.htmlentities($this->fullNumber()).'">'._L('Lookup at ean-search.org').'</a></p>';
  178.                                 $content .= '<img src="'.OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'barcode.php?number='.urlencode($this->fullNumber()).'">';
  179.                                 $content .= '<h2>'._L('Description').'</h2>%%DESC%%'; // TODO: add more meta information about the object type
  180.                         } else {
  181.                                 $chunked = $this->chunkedNotation(true);
  182.                                 $content = '<h2>'.$chunked.'</h2>';
  183.                                 $content .= '<h2>'._L('Description').'</h2>%%DESC%%'; // TODO: add more meta information about the object type
  184.                                 if ($this->userHasWriteRights()) {
  185.                                         $content .= '<h2>'._L('Create or change subordinate objects').'</h2>';
  186.                                 } else {
  187.                                         $content .= '<h2>'._L('Subordinate objects').'</h2>';
  188.                                 }
  189.                                 $content .= '%%CRUD%%';
  190.                         }
  191.                 }
  192.         }
  193.  
  194.         # ---
  195.  
  196.         /**
  197.          * @return bool
  198.          */
  199.         public function isBaseOnly(): bool {
  200.                 return strlen($this->number) <= 7;
  201.         }
  202.  
  203.         /**
  204.          * @param bool $withAbbr
  205.          * @return string
  206.          * @throws OIDplusException
  207.          */
  208.         public function chunkedNotation(bool $withAbbr=true): string {
  209.                 $curid = self::root().$this->number;
  210.  
  211.                 $obj = OIDplusObject::findFitting($curid);
  212.                 if (!$obj) return $this->number;
  213.  
  214.                 $hints = array();
  215.                 $lengths = array(strlen($curid));
  216.                 while ($obj = OIDplusObject::findFitting($curid)) {
  217.                         $objParent = $obj->getParent();
  218.                         if (!$objParent) break;
  219.                         $curid = $objParent->nodeId();
  220.                         $hints[] = $obj->getTitle();
  221.                         $lengths[] = strlen($curid);
  222.                 }
  223.  
  224.                 array_shift($lengths);
  225.                 $chunks = array();
  226.  
  227.                 $full = self::root().$this->number;
  228.                 foreach ($lengths as $len) {
  229.                         $chunks[] = substr($full, $len);
  230.                         $full = substr($full, 0, $len);
  231.                 }
  232.  
  233.                 $hints = array_reverse($hints);
  234.                 $chunks = array_reverse($chunks);
  235.  
  236.                 $full = array();
  237.                 foreach ($chunks as $c) {
  238.                         $hint = array_shift($hints);
  239.                         $full[] = $withAbbr && ($hint !== '') ? '<abbr title="'.htmlentities($hint).'">'.$c.'</abbr>' : $c;
  240.                 }
  241.                 return implode(' ', $full);
  242.         }
  243.  
  244.         /**
  245.          * @return string
  246.          */
  247.         public function fullNumber(): string {
  248.                 return $this->number . $this->checkDigit();
  249.         }
  250.  
  251.         /**
  252.          * @return int
  253.          */
  254.         public function checkDigit(): int {
  255.                 $mul = 3;
  256.                 $sum = 0;
  257.                 for ($i=strlen($this->number)-1; $i>=0; $i--) {
  258.                         $sum += $this->number[$i] * $mul;
  259.                         $mul = $mul == 3 ? 1 : 3;
  260.                 }
  261.                 return 10 - ($sum % 10);
  262.         }
  263.  
  264.         /**
  265.          * @return OIDplusGs1|null
  266.          */
  267.         public function one_up()/*: ?OIDplusGs1*/ {
  268.                 return self::parse($this->ns().':'.substr($this->number,0,strlen($this->number)-1));
  269.         }
  270.  
  271.         /**
  272.          * @param string $a
  273.          * @param string $b
  274.          * @return false|int
  275.          */
  276.         private static function distance_(string $a, string $b) {
  277.                 $min_len = min(strlen($a), strlen($b));
  278.  
  279.                 for ($i=0; $i<$min_len; $i++) {
  280.                         if ($a[$i] != $b[$i]) return false;
  281.                 }
  282.  
  283.                 return strlen($a) - strlen($b);
  284.         }
  285.  
  286.         /**
  287.          * @param $to
  288.          * @return int|null
  289.          */
  290.         public function distance($to) {
  291.                 if (!is_object($to)) $to = OIDplusObject::parse($to);
  292.                 if (!($to instanceof $this)) return null;
  293.  
  294.                 // This is pretty tricky, because the whois service should accept GS1 numbers with and without checksum
  295.                 if ($this->number == $to->number) return 0;
  296.                 if ($this->number.$this->checkDigit() == $to->number) return 0;
  297.                 if ($this->number == $to->number.$to->checkDigit()) return 0;
  298.  
  299.                 $b = $this->number;
  300.                 $a = $to->number;
  301.                 $tmp = self::distance_($a, $b);
  302.                 if ($tmp !== false) return $tmp;
  303.  
  304.                 $b = $this->number.$this->checkDigit();
  305.                 $a = $to->number;
  306.                 $tmp = self::distance_($a, $b);
  307.                 if ($tmp !== false) return $tmp;
  308.  
  309.                 $b = $this->number;
  310.                 $a = $to->number.$to->checkDigit();
  311.                 $tmp = self::distance_($a, $b);
  312.                 if ($tmp !== false) return $tmp;
  313.  
  314.                 return null;
  315.         }
  316.  
  317.         /**
  318.          * @return array|OIDplusAltId[]
  319.          * @throws OIDplusException
  320.          */
  321.         public function getAltIds(): array {
  322.                 if ($this->isRoot()) return array();
  323.                 $ids = parent::getAltIds();
  324.  
  325.                 // (VTS F5) GS1 to AID (PIX allowed)
  326.                 $gs1 = $this->nodeId(false);
  327.                 $aid = 'D276000186F5'.$gs1;
  328.                 if (strlen($aid)%2 == 1) $aid .= 'F';
  329.                 $aid_is_ok = aid_canonize($aid);
  330.                 if ($aid_is_ok) $ids[] = new OIDplusAltId('aid', $aid, _L('Application Identifier (ISO/IEC 7816)'), ' ('._L('Optional PIX allowed, with "FF" prefix').')');
  331.  
  332.                 return $ids;
  333.         }
  334.  
  335.         /**
  336.          * @return string
  337.          */
  338.         public function getDirectoryName(): string {
  339.                 if ($this->isRoot()) return $this->ns();
  340.                 return $this->ns().'_'.$this->nodeId(false); // safe, because there are only numbers
  341.         }
  342.  
  343.         /**
  344.          * @param string $mode
  345.          * @return string
  346.          */
  347.         public static function treeIconFilename(string $mode): string {
  348.                 return 'img/'.$mode.'_icon16.png';
  349.         }
  350. }
  351.