Subversion Repositories oidplus

Rev

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