Subversion Repositories oidplus

Rev

Rev 1213 | Rev 1329 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
635 daniel-mar 1
<?php
2
 
3
/*
4
 * OIDplus 2.0
1086 daniel-mar 5
 * Copyright 2019 - 2023 Daniel Marschall, ViaThinkSoft
635 daniel-mar 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
 
1050 daniel-mar 20
namespace ViaThinkSoft\OIDplus;
635 daniel-mar 21
 
1086 daniel-mar 22
// phpcs:disable PSR1.Files.SideEffects
23
\defined('INSIDE_OIDPLUS') or die;
24
// phpcs:enable PSR1.Files.SideEffects
25
 
635 daniel-mar 26
class OIDplusGs1 extends OIDplusObject {
1130 daniel-mar 27
        /**
28
         * @var int|string
29
         */
635 daniel-mar 30
        private $number;
31
 
1116 daniel-mar 32
        /**
1130 daniel-mar 33
         * @param string|int $number
1116 daniel-mar 34
         */
635 daniel-mar 35
        public function __construct($number) {
36
                // TODO: syntax checks
37
                $this->number = $number;
38
        }
39
 
1116 daniel-mar 40
        /**
41
         * @param string $node_id
42
         * @return OIDplusGs1|null
43
         */
44
        public static function parse(string $node_id)/*: ?OIDplusGs1*/ {
635 daniel-mar 45
                @list($namespace, $number) = explode(':', $node_id, 2);
1116 daniel-mar 46
                if ($namespace !== self::ns()) return null;
635 daniel-mar 47
                return new self($number);
48
        }
49
 
1116 daniel-mar 50
        /**
51
         * @return string
52
         */
53
        public static function objectTypeTitle(): string {
635 daniel-mar 54
                return _L('GS1 Based IDs (GLN/GTIN/SSCC/...)');
55
        }
56
 
1116 daniel-mar 57
        /**
58
         * @return string
59
         */
60
        public static function objectTypeTitleShort(): string {
635 daniel-mar 61
                return _L('GS1');
62
        }
63
 
1116 daniel-mar 64
        /**
65
         * @return string
66
         */
67
        public static function ns(): string {
635 daniel-mar 68
                return 'gs1';
69
        }
70
 
1116 daniel-mar 71
        /**
72
         * @return string
73
         */
74
        public static function root(): string {
860 daniel-mar 75
                return self::ns().':';
635 daniel-mar 76
        }
77
 
1116 daniel-mar 78
        /**
79
         * @return bool
80
         */
81
        public function isRoot(): bool {
635 daniel-mar 82
                return $this->number == '';
83
        }
84
 
1116 daniel-mar 85
        /**
86
         * @param bool $with_ns
87
         * @return string
88
         */
89
        public function nodeId(bool $with_ns=true): string {
859 daniel-mar 90
                return $with_ns ? self::root().$this->number : $this->number;
635 daniel-mar 91
        }
92
 
1116 daniel-mar 93
        /**
94
         * @param string $str
95
         * @return string
96
         * @throws OIDplusException
97
         */
98
        public function addString(string $str): string {
635 daniel-mar 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
 
1116 daniel-mar 107
        /**
108
         * @param OIDplusObject $parent
109
         * @return string
1130 daniel-mar 110
         * @throws OIDplusException
1116 daniel-mar 111
         */
112
        public function crudShowId(OIDplusObject $parent): string {
635 daniel-mar 113
                return $this->chunkedNotation(false);
114
        }
115
 
1116 daniel-mar 116
        /**
117
         * @return string
1130 daniel-mar 118
         * @throws OIDplusException
1116 daniel-mar 119
         */
120
        public function crudInsertPrefix(): string {
635 daniel-mar 121
                return $this->isRoot() ? '' : $this->chunkedNotation(false);
122
        }
123
 
1116 daniel-mar 124
        /**
125
         * @param OIDplusObject|null $parent
126
         * @return string
127
         */
128
        public function jsTreeNodeName(OIDplusObject $parent = null): string {
635 daniel-mar 129
                if ($parent == null) return $this->objectTypeTitle();
130
                return substr($this->nodeId(), strlen($parent->nodeId()));
131
        }
132
 
1116 daniel-mar 133
        /**
134
         * @return string
135
         */
136
        public function defaultTitle(): string {
635 daniel-mar 137
                return $this->number;
138
        }
139
 
1116 daniel-mar 140
        /**
141
         * @return bool
142
         */
143
        public function isLeafNode(): bool {
635 daniel-mar 144
                return !$this->isBaseOnly();
145
        }
146
 
1116 daniel-mar 147
        /**
1139 daniel-mar 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
        /**
1116 daniel-mar 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) {
801 daniel-mar 167
                $icon = file_exists(__DIR__.'/img/main_icon.png') ? OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'img/main_icon.png' : '';
635 daniel-mar 168
 
169
                if ($this->isRoot()) {
170
                        $title = OIDplusGs1::objectTypeTitle();
171
 
172
                        $res = OIDplus::db()->query("select * from ###objects where parent = ?", array(self::root()));
790 daniel-mar 173
                        if ($res->any()) {
962 daniel-mar 174
                                $content  = '<p>'._L('Please select an item in the tree view at the left to show its contents.').'</p>';
635 daniel-mar 175
                        } else {
962 daniel-mar 176
                                $content  = '<p>'._L('Currently, no GS1 based numbers are registered in the system.').'</p>';
635 daniel-mar 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
 
1139 daniel-mar 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 .= '<table border="0">';
195
                                foreach ($tech_info as $key => $value) {
1324 daniel-mar 196
                                        $tech_info_html .= '<tr><td valign="top">'.$key.': </td><td><code>'.str_replace(' ','&nbsp;',$value).'</code></td></tr>';
1139 daniel-mar 197
                                }
198
                                $tech_info_html .= '</table>';
199
                        }
635 daniel-mar 200
                        if ($this->isLeafNode()) {
201
                                $chunked = $this->chunkedNotation(true);
202
                                $checkDigit = $this->checkDigit();
1139 daniel-mar 203
                                $content  = '<h2>'._L('Barcode').' '.$chunked.' - <abbr title="'._L('check digit').'">'.$checkDigit.'</abbr></h2>';
635 daniel-mar 204
                                $content .= '<p><a target="_blank" href="https://www.ean-search.org/?q='.htmlentities($this->fullNumber()).'">'._L('Lookup at ean-search.org').'</a></p>';
1213 daniel-mar 205
                                if (url_get_contents_available(true, $reason)) {
206
                                        $content .= '<p><img alt="'._L('Barcode').'" src="' . OIDplus::webpath(__DIR__, OIDplus::PATH_RELATIVE) . 'barcode.php?number=' . urlencode($this->fullNumber()) . '"></p>';
207
                                }
1139 daniel-mar 208
                                $content .= $tech_info_html;
635 daniel-mar 209
                                $content .= '<h2>'._L('Description').'</h2>%%DESC%%'; // TODO: add more meta information about the object type
210
                        } else {
211
                                $chunked = $this->chunkedNotation(true);
1139 daniel-mar 212
                                $content  = '<h2>'._L('Barcode').' '.$chunked.'</h2>';
213
                                $content .= $tech_info_html;
635 daniel-mar 214
                                $content .= '<h2>'._L('Description').'</h2>%%DESC%%'; // TODO: add more meta information about the object type
215
                                if ($this->userHasWriteRights()) {
928 daniel-mar 216
                                        $content .= '<h2>'._L('Create or change subordinate objects').'</h2>';
635 daniel-mar 217
                                } else {
928 daniel-mar 218
                                        $content .= '<h2>'._L('Subordinate objects').'</h2>';
635 daniel-mar 219
                                }
220
                                $content .= '%%CRUD%%';
221
                        }
222
                }
223
        }
224
 
225
        # ---
226
 
1116 daniel-mar 227
        /**
228
         * @return bool
229
         */
230
        public function isBaseOnly(): bool {
1139 daniel-mar 231
                // TODO: This is actually not correct, since there are many GS1 Application Identifiers which can have less than 7 digits
635 daniel-mar 232
                return strlen($this->number) <= 7;
233
        }
234
 
1116 daniel-mar 235
        /**
236
         * @param bool $withAbbr
237
         * @return string
238
         * @throws OIDplusException
239
         */
240
        public function chunkedNotation(bool $withAbbr=true): string {
859 daniel-mar 241
                $curid = self::root().$this->number;
635 daniel-mar 242
 
977 daniel-mar 243
                $obj = OIDplusObject::findFitting($curid);
244
                if (!$obj) return $this->number;
635 daniel-mar 245
 
246
                $hints = array();
247
                $lengths = array(strlen($curid));
977 daniel-mar 248
                while ($obj = OIDplusObject::findFitting($curid)) {
249
                        $objParent = $obj->getParent();
250
                        if (!$objParent) break;
251
                        $curid = $objParent->nodeId();
252
                        $hints[] = $obj->getTitle();
635 daniel-mar 253
                        $lengths[] = strlen($curid);
254
                }
255
 
256
                array_shift($lengths);
257
                $chunks = array();
258
 
859 daniel-mar 259
                $full = self::root().$this->number;
635 daniel-mar 260
                foreach ($lengths as $len) {
261
                        $chunks[] = substr($full, $len);
262
                        $full = substr($full, 0, $len);
263
                }
264
 
265
                $hints = array_reverse($hints);
266
                $chunks = array_reverse($chunks);
267
 
268
                $full = array();
269
                foreach ($chunks as $c) {
934 daniel-mar 270
                        $hint = array_shift($hints);
271
                        $full[] = $withAbbr && ($hint !== '') ? '<abbr title="'.htmlentities($hint).'">'.$c.'</abbr>' : $c;
635 daniel-mar 272
                }
273
                return implode(' ', $full);
274
        }
275
 
1116 daniel-mar 276
        /**
277
         * @return string
278
         */
279
        public function fullNumber(): string {
635 daniel-mar 280
                return $this->number . $this->checkDigit();
281
        }
282
 
1116 daniel-mar 283
        /**
284
         * @return int
285
         */
286
        public function checkDigit(): int {
635 daniel-mar 287
                $mul = 3;
288
                $sum = 0;
289
                for ($i=strlen($this->number)-1; $i>=0; $i--) {
1130 daniel-mar 290
                        $str = "".$this->number;
291
                        $sum += (int)$str[$i] * $mul;
635 daniel-mar 292
                        $mul = $mul == 3 ? 1 : 3;
293
                }
294
                return 10 - ($sum % 10);
295
        }
296
 
1116 daniel-mar 297
        /**
298
         * @return OIDplusGs1|null
299
         */
300
        public function one_up()/*: ?OIDplusGs1*/ {
301
                return self::parse($this->ns().':'.substr($this->number,0,strlen($this->number)-1));
635 daniel-mar 302
        }
303
 
1116 daniel-mar 304
        /**
305
         * @param string $a
306
         * @param string $b
307
         * @return false|int
308
         */
309
        private static function distance_(string $a, string $b) {
635 daniel-mar 310
                $min_len = min(strlen($a), strlen($b));
311
 
312
                for ($i=0; $i<$min_len; $i++) {
313
                        if ($a[$i] != $b[$i]) return false;
314
                }
315
 
316
                return strlen($a) - strlen($b);
317
        }
318
 
1116 daniel-mar 319
        /**
1130 daniel-mar 320
         * @param OIDplusObject|string $to
1116 daniel-mar 321
         * @return int|null
322
         */
635 daniel-mar 323
        public function distance($to) {
324
                if (!is_object($to)) $to = OIDplusObject::parse($to);
1121 daniel-mar 325
                if (!$to) return null;
1116 daniel-mar 326
                if (!($to instanceof $this)) return null;
635 daniel-mar 327
 
328
                // This is pretty tricky, because the whois service should accept GS1 numbers with and without checksum
329
                if ($this->number == $to->number) return 0;
330
                if ($this->number.$this->checkDigit() == $to->number) return 0;
331
                if ($this->number == $to->number.$to->checkDigit()) return 0;
332
 
333
                $b = $this->number;
334
                $a = $to->number;
335
                $tmp = self::distance_($a, $b);
1116 daniel-mar 336
                if ($tmp !== false) return $tmp;
635 daniel-mar 337
 
338
                $b = $this->number.$this->checkDigit();
339
                $a = $to->number;
340
                $tmp = self::distance_($a, $b);
1116 daniel-mar 341
                if ($tmp !== false) return $tmp;
635 daniel-mar 342
 
343
                $b = $this->number;
344
                $a = $to->number.$to->checkDigit();
345
                $tmp = self::distance_($a, $b);
1116 daniel-mar 346
                if ($tmp !== false) return $tmp;
635 daniel-mar 347
 
348
                return null;
349
        }
350
 
1116 daniel-mar 351
        /**
352
         * @return array|OIDplusAltId[]
353
         * @throws OIDplusException
354
         */
355
        public function getAltIds(): array {
945 daniel-mar 356
                if ($this->isRoot()) return array();
357
                $ids = parent::getAltIds();
358
 
359
                // (VTS F5) GS1 to AID (PIX allowed)
360
                $gs1 = $this->nodeId(false);
361
                $aid = 'D276000186F5'.$gs1;
362
                if (strlen($aid)%2 == 1) $aid .= 'F';
363
                $aid_is_ok = aid_canonize($aid);
959 daniel-mar 364
                if ($aid_is_ok) $ids[] = new OIDplusAltId('aid', $aid, _L('Application Identifier (ISO/IEC 7816)'), ' ('._L('Optional PIX allowed, with "FF" prefix').')');
945 daniel-mar 365
 
366
                return $ids;
367
        }
368
 
1116 daniel-mar 369
        /**
370
         * @return string
371
         */
372
        public function getDirectoryName(): string {
635 daniel-mar 373
                if ($this->isRoot()) return $this->ns();
374
                return $this->ns().'_'.$this->nodeId(false); // safe, because there are only numbers
375
        }
800 daniel-mar 376
 
1116 daniel-mar 377
        /**
378
         * @param string $mode
379
         * @return string
380
         */
381
        public static function treeIconFilename(string $mode): string {
800 daniel-mar 382
                return 'img/'.$mode.'_icon16.png';
383
        }
635 daniel-mar 384
}