1C-Bitrix 25.700.0
Загрузка...
Поиск...
Не найдено
importprocess.php
См. документацию.
1<?php
8
10
11use Bitrix\Main;
16
20
22{
23 const DISTRIBUTOR_HOST = 'www.1c-bitrix.ru';
24 const DISTRIBUTOR_PORT = 80;
25 //const REMOTE_PATH = '/locations_data/compiled/';
26 const REMOTE_PATH = '/download/files/locations/pro/';
27 const REMOTE_SETS_PATH = 'bundles/';
28 const REMOTE_LAYOUT_FILE = 'bundles/layout.csv';
29 const REMOTE_TYPE_GROUP_FILE = 'typegroup.csv';
30 const REMOTE_TYPE_FILE = 'type.v2.csv';
31 const REMOTE_EXTERNAL_SERVICE_FILE = 'externalservice.csv';
32
33 const PACK_STANDARD = 'standard';
34 const PACK_EXTENDED = 'extended';
35
36 const LOCAL_SETS_PATH = 'bundles/';
37 const LOCAL_LOCATION_FILE = '%s.csv';
38 const LOCAL_LAYOUT_FILE = 'layout.csv';
39 const LOCAL_TYPE_GROUP_FILE = 'typegroup.csv';
40 const LOCAL_TYPE_FILE = 'type.csv';
41 const LOCAL_EXTERNAL_SERVICE_FILE = 'externalservice.csv';
42
43 const USER_FILE_DIRECTORY_SESSION_KEY = 'location_import_user_file';
44 const USER_FILE_TEMP_NAME = 'userfile.csv';
45
46 const SOURCE_REMOTE = 'remote';
47 const SOURCE_FILE = 'file';
48
50 const INSERTER_MTU = 99999;
51 const INSERTER_MTU_ORACLE = 9999;
52
53 const DB_TYPE_MYSQL = 'mysql';
54 const DB_TYPE_MSSQL = 'mssql';
55 const DB_TYPE_ORACLE = 'oracle';
56
59 const TREE_REBALANCE_TEMP_TABLE_NAME = 'b_sale_location_rebalance';
60
61 const DEBUG_MODE = false;
62
63 protected $sessionKey = 'location_import';
64 protected $rebalanceInserter = false;
65 protected $stat = array();
66 protected $hitData = array();
67 protected $useCache = true;
68
69 protected $dbConnection = null;
70 protected $dbConnType = null;
71 protected $dbHelper = null;
72
73 public function __construct($options)
74 {
76
77 if($options['ONLY_DELETE_ALL'])
78 {
79 $this->addStage(array(
80 'PERCENT' => 100,
81 'CODE' => 'DELETE_ALL',
82 'CALLBACK' => 'stageDeleteAll',
83 'SUBPERCENT_CALLBACK' => 'getSubpercentForstageDeleteAll'
84 ));
85 }
86 else
87 {
88 $this->addStage(array(
89 'PERCENT' => 5,
90 'CODE' => 'DOWNLOAD_FILES',
91 'CALLBACK' => 'stageDownloadFiles',
92 'SUBPERCENT_CALLBACK' => 'getSubpercentForStageDownloadFiles'
93 ));
94
95 if($options['REQUEST']['OPTIONS']['DROP_ALL'])
96 {
97 $this->addStage(array(
98 'PERCENT' => 7,
99 'CODE' => 'DELETE_ALL',
100 'CALLBACK' => 'stageDeleteAll',
101 'SUBPERCENT_CALLBACK' => 'getSubpercentForstageDeleteAll'
102 ));
103 }
104
105 $this->addStage(array(
106 'PERCENT' => 10,
107 'CODE' => 'DROP_INDEXES',
108 'CALLBACK' => 'stageDropIndexes',
109 'SUBPERCENT_CALLBACK' => 'getSubpercentForStageDropIndexes'
110 ));
111
112 $this->addStage(array(
113 'PERCENT' => 60,
114 'STEP_SIZE' => 6000,
115 'CODE' => 'PROCESS_FILES',
116 'CALLBACK' => 'stageProcessFiles',
117 'SUBPERCENT_CALLBACK' => 'getSubpercentForStageProcessFiles'
118 ));
119
120 if($options['REQUEST']['OPTIONS']['INTEGRITY_PRESERVE'])
121 {
122 $this->addStage(array(
123 'PERCENT' => 65,
124 'STEP_SIZE' => 1,
125 'CODE' => 'INTEGRITY_PRESERVE',
126 'CALLBACK' => 'stageIntegrityPreserve'
127 ));
128 }
129
130 $this->addStage(array(
131 'PERCENT' => 90,
132 'STEP_SIZE' => 1,
133 'CODE' => 'REBALANCE_WALK_TREE',
134 'CALLBACK' => 'stageRebalanceWalkTree',
135 'SUBPERCENT_CALLBACK' => 'getSubpercentForStageRebalanceWalkTree'
136 ));
137
138 $this->addStage(array(
139 'PERCENT' => 95,
140 'STEP_SIZE' => 1,
141 'CODE' => 'REBALANCE_CLEANUP_TEMP_TABLE',
142 'CALLBACK' => 'stageRebalanceCleanupTempTable'
143 ));
144
145 $this->addStage(array(
146 'PERCENT' => 100,
147 'STEP_SIZE' => 1,
148 'CODE' => 'RESTORE_INDEXES',
149 'CALLBACK' => 'stageRestoreIndexes',
150 'SUBPERCENT_CALLBACK' => 'getSubpercentForStageRestoreIndexes'
151 ));
152 }
153
154 $this->dbConnection = Main\HttpApplication::getConnection();
155 $this->dbConnType = $this->dbConnection->getType();
156 $this->dbHelper = $this->dbConnection->getSqlHelper();
157
158 parent::__construct($options);
159 }
160
162 {
163 if (!is_array($options))
164 {
165 $options = [];
166 }
167 $options['ONLY_DELETE_ALL'] = (bool)($options['ONLY_DELETE_ALL'] ?? false);
168 $options['LANGUAGE_ID'] = trim((string)($options['LANGUAGE_ID'] ?? LANGUAGE_ID));
169 if ($options['LANGUAGE_ID'] === '')
170 {
171 $options['LANGUAGE_ID'] = LANGUAGE_ID;
172 }
173
174 $options['REQUEST'] ??= [];
175 if (is_object($options['REQUEST']) && method_exists($options['REQUEST'], 'toArray'))
176 {
177 $options['REQUEST'] = $options['REQUEST']->toArray();
178 }
179 if (!is_array($options['REQUEST']))
180 {
181 $options['REQUEST'] = [];
182 }
183 $options['REQUEST']['OPTIONS'] ??= [];
184 if (!is_array($options['REQUEST']['OPTIONS']))
185 {
186 $options['REQUEST']['OPTIONS'] = [];
187 }
188
189 $requestOptions = &$options['REQUEST']['OPTIONS'];
190 $requestOptions['DROP_ALL'] ??= null;
191 $requestOptions['INTEGRITY_PRESERVE'] ??= null;
192 $requestOptions['EXCLUDE_COUNTRY_DISTRICT'] ??= null;
193 $requestOptions['SOURCE'] ??= null;
194 $requestOptions['TIME_LIMIT'] = (int)($requestOptions['TIME_LIMIT'] ?? 0);
195 unset($requestOptions);
196
197 $options['REQUEST']['ADDITIONAL'] ??= [];
198 if (!is_array($options['REQUEST']['ADDITIONAL']))
199 {
200 $options['REQUEST']['ADDITIONAL'] = [];
201 }
202
203 $options['LOCATION_SETS'] ??= [];
204 if (!is_array($options['LOCATION_SETS']))
205 {
206 $options['LOCATION_SETS'] = [];
207 }
208
209 return $options;
210 }
211
212 public function onBeforePerformIteration()
213 {
214 if ($this->options['ONLY_DELETE_ALL'])
215 {
216 return;
217 }
218
219 $this->data['inited'] ??= false;
220 if (!$this->data['inited'])
221 {
222 if((string)($this->data['LOCAL_PATH'] ?? '') === '')
223 {
224 [$this->data['LOCAL_PATH'], $this->data['LOCAL_PATH_CREATED']] = $this->getTemporalDirectory();
225 }
226
227 $opts = $this->options['REQUEST']['OPTIONS'];
228
229 if(!in_array($opts['SOURCE'], array(self::SOURCE_REMOTE, self::SOURCE_FILE)))
230 throw new Main\SystemException('Unknown import type');
231
232 $sets = array();
233 if($opts['SOURCE'] == self::SOURCE_REMOTE)
234 {
235 $sets = $this->normalizeQueryArray($this->options['REQUEST']['LOCATION_SETS']);
236 if(empty($sets))
237 throw new Main\SystemException('Nothing to do (no sets selected)');
238 }
239
240 $this->data['settings'] = array(
241 'sets' => $sets,
242 'options' => $opts
243 );
244
245 if($opts['SOURCE'] == self::SOURCE_REMOTE)
246 {
247 $this->data['settings']['additional'] = is_array($this->options['REQUEST']['ADDITIONAL']) ? array_flip(array_values($this->options['REQUEST']['ADDITIONAL'])) : array();
248
249 if(isset($this->data['settings']['additional']['ZIP']))
250 $this->data['settings']['additional']['ZIP_LOWER'] = $this->data['settings']['additional']['ZIP'];
251 }
252 elseif($this->checkSource(self::SOURCE_FILE))
253 {
254 $this->data['settings']['additional'] = false; // means ANY
255 }
256
257 $this->buildTypeTable();
259
260 $this->data['inited'] = true;
261 }
262
263 if ($this->data['inited'] && $this->data['LOCAL_PATH_CREATED'])
264 {
265 $this->touchImportTmpFiles((string)$this->data['LOCAL_PATH']);
266 }
267
268 $timeLimit = $this->data['settings']['options']['TIME_LIMIT'];
269 if ($timeLimit > 0)
270 {
271 $this->setTimeLimit($timeLimit);
272 }
273 }
274
278 private function touchImportTmpFiles(string $localPath): void
279 {
280 $iterator = new \RecursiveIteratorIterator(
281 new \RecursiveDirectoryIterator($localPath)
282 );
283 foreach ($iterator as $file) {
284
285 if ($file->isDir())
286 {
287 continue;
288 }
289
290 touch($file->getPathname());
291 }
292 }
293
295 // STAGE 1
296
297 protected function stageDownloadFiles()
298 {
299 if($this->checkSource(self::SOURCE_FILE)) // user uploaded file
300 {
301 if((string) $_SESSION[self::USER_FILE_DIRECTORY_SESSION_KEY] == '')
302 throw new Main\SystemException('User file was not uploaded properly');
303
304 $srcFilePath = $_SESSION[self::USER_FILE_DIRECTORY_SESSION_KEY].'/'.static::USER_FILE_TEMP_NAME;
305 $dstFilePath = $this->data['LOCAL_PATH'].self::getFileNameByIndex(0);
306
307 // ensure directory exists
308 $this->createDirectory($this->data['LOCAL_PATH'].'/'.self::LOCAL_SETS_PATH);
309
310 if(!@copy($srcFilePath, $dstFilePath))
311 {
312 $lastError = error_get_last();
313 throw new Main\SystemException($lastError['message']);
314 }
315
316 $this->data['files'] = array(
317 array(
318 'size' => filesize($dstFilePath),
319 'memgroup' => 'static'
320 )
321 );
322
323 $this->nextStage();
324 }
325 elseif($this->checkSource(self::SOURCE_REMOTE)) // get locations from remote server
326 {
327 if($this->getStep() == 0)
328 {
329 $this->data['files'] = array();
330
331 $this->cleanWorkDirectory();
332
333 // layout
335
336 // type groups
337 $typeGroups = $this->getRemoteTypeGroups();
338
339 // find out what groups we will include
340 $this->data['requiredGroups'] = array();
341 foreach($typeGroups as $code => $types)
342 {
343 if($code == 'LAYOUT') // layout is always included
344 continue;
345
346 foreach($types as $type)
347 {
348 if(isset($this->data['types']['allowed'][$type]))
349 {
350 $this->data['requiredGroups'][] = mb_strtolower($code);
351 break;
352 }
353 }
354 }
355 }
356 else
357 {
358 $packPath = self::REMOTE_SETS_PATH.($this->data['settings']['options']['PACK'] == self::PACK_EXTENDED ? self::PACK_EXTENDED : self::PACK_STANDARD).'/';
359 //$packPath = self::REMOTE_SETS_PATH.'/';
360
361 if($this->getStep() == 1) // get layout (root) file
362 {
363 $this->data['files'][0] = array(
364 'size' => static::downloadFile(self::REMOTE_LAYOUT_FILE, self::getFileNameByIndex(0), false, $this->data['LOCAL_PATH']),
365 'onlyThese' => array_flip($this->data['settings']['bundles']['allpoints']),
366 'memgroup' => 'static'
367 );
368
369 $this->data['fileDownload']['currentEndPoint'] = 0;
370 $this->data['fileDownload']['currentFileOffset'] = 1;
371 }
372
373 $i =& $this->data['fileDownload']['currentEndPoint'];
374 $j =& $this->data['fileDownload']['currentFileOffset'];
375
376 while($this->checkQuota() && isset($this->data['settings']['bundles']['endpoints'][$i])) // process as many bundles as possible
377 {
378 $ep = $this->data['settings']['bundles']['endpoints'][$i];
379
380 foreach($this->data['requiredGroups'] as $code)
381 {
383 $file = $packPath.$ep.'_'.$code.'.csv';
384 try
385 {
386 $this->data['files'][$j] = array(
387 'size' => static::downloadFile($file, $name, false, $this->data['LOCAL_PATH']),
388 'memgroup' => $ep
389 );
390 $j++;
391 }
392 catch(Main\SystemException $e) // 404 or smth - just skip for now
393 {
394 }
395 }
396 $i++;
397 }
398
399 if(!isset($this->data['settings']['bundles']['endpoints'][$i])) // no more bundles to process, all files downloaded
400 {
401 unset($this->data['requiredGroups']);
402 unset($this->data['settings']['bundles']['endpoints']);
403
404 $this->nextStage();
405 return;
406 }
407 }
408
409 $this->nextStep();
410 }
411 }
412
414 {
415 $pRange = $this->getCurrentPercentRange();
416
417 $currEp = (int)($this->data['fileDownload']['currentEndPoint'] ?? 0);
418
419 if (!$currEp)
420 {
421 return 0;
422 }
423
424 return round($pRange * ($currEp / count($this->data['settings']['bundles']['endpoints'])));
425 }
426
428 // STAGE 2
429
430 protected function stageDeleteAll()
431 {
432 switch($this->step)
433 {
434 case 0:
435 $this->dbConnection->query('truncate table '.Location\LocationTable::getTableName());
436 break;
437 case 1:
438 $this->dbConnection->query('truncate table '.Location\Name\LocationTable::getTableName());
439 break;
440 case 2:
441 $this->dbConnection->query('truncate table '.Location\ExternalTable::getTableName());
442 break;
443 case 3:
445 break;
446 case 4:
448 break;
449 }
450
451 $this->nextStep();
452
453 if($this->step >= 5)
454 $this->nextStage();
455 }
456
458 {
459 $pRange = $this->getCurrentPercentRange();
460 $step = $this->getStep();
461
462 $stepsCount = 5;
463
464 if($step >= $stepsCount)
465 return $pRange;
466 else
467 {
468 return round($pRange * ($step / $stepsCount));
469 }
470 }
471
473 // STAGE 2.5
474
475 protected function stageDropIndexes()
476 {
477 $indexes = array(
478 'IX_B_SALE_LOC_MARGINS',
479 'IX_B_SALE_LOC_MARGINS_REV',
480 'IX_B_SALE_LOC_PARENT',
481 'IX_B_SALE_LOC_DL',
482 'IX_B_SALE_LOC_TYPE',
483 'IX_B_SALE_LOC_NAME_NAME_U',
484 'IX_B_SALE_LOC_NAME_LI_LI',
485 'IX_B_SALE_LOC_EXT_LID_SID',
486
487 // old
488 'IXS_LOCATION_COUNTRY_ID',
489 'IXS_LOCATION_REGION_ID',
490 'IXS_LOCATION_CITY_ID',
491 'IX_B_SALE_LOCATION_1',
492 'IX_B_SALE_LOCATION_2',
493 'IX_B_SALE_LOCATION_3'
494 );
495
496 if(!isset($indexes[$this->getStep()]))
497 $this->nextStage();
498 else
499 {
500 $this->dropIndexes($indexes[$this->getStep()]);
501 $this->logMessage('Index dropped: '.$indexes[$this->getStep()]);
502 $this->nextStep();
503 }
504 }
505
507 {
508 $pRange = $this->getCurrentPercentRange();
509 $step = $this->getStep();
510
511 $indexCount = 14;
512
513 if($step >= $indexCount)
514 return $pRange;
515 else
516 {
517 return round($pRange * ($step / $indexCount));
518 }
519 }
520
522 // STAGE 3
523
524 protected function readBlockFromCurrentFile2()
525 {
526 $fIndex = $this->data['current']['fIndex'];
527 $fName = self::getFileNameByIndex($fIndex);
528 $onlyThese =& $this->data['files'][$fIndex]['onlyThese'];
529
530 //$this->logMessage('READ FROM File: '.$fName.' seek to '.$this->data['current']['bytesRead']);
531
532 if(!isset($this->hitData['csv']))
533 {
534 $file = $this->data['LOCAL_PATH'].$fName;
535
536 if(!file_exists($file) || !is_readable($file))
537 throw new Main\SystemException('Cannot open file '.$file.' for reading');
538
539 $this->logMessage('Chargeing File: '.$fName);
540
541 $this->hitData['csv'] = new CSVReader();
542 $this->hitData['csv']->LoadFile($file);
543 $this->hitData['csv']->AddEventCallback('AFTER_ASSOC_LINE_READ', array($this, 'provideEnFromRu'));
544 }
545
546 $block = $this->hitData['csv']->ReadBlockLowLevel($this->data['current']['bytesRead'], 100);
547
548 $this->data['current']['linesRead'] += count($block);
549
550 if(empty($block))
551 {
552 return array();
553 }
554
555 if($this->hitData['csv']->CheckFileIsLegacy())
556 {
557 $block = self::convertBlock($block);
558 }
559
560 if(is_array($onlyThese))
561 {
562 foreach($block as $i => $line)
563 {
564 if(is_array($onlyThese) && !isset($onlyThese[$line['CODE']]))
565 unset($block[$i]);
566 }
567 }
568
569 //$this->logMessage('Bytes read: '.$this->data['current']['bytesRead']);
570
571 return $block;
572 }
573
574 protected static function checkLocationCodeExists($code)
575 {
576 if ($code == '')
577 return false;
578
580
581 $code = $dbConnection->getSqlHelper()->forSql($code);
582 $res = $dbConnection->query("select ID from ".Location\LocationTable::getTableName()." where CODE = '".$code."'")->fetch();
583
584 return $res['ID'] ?? false;
585 }
586
587 protected function importBlock(&$block)
588 {
589 if(empty($block))
590 return;
591
592 $gid = $this->getCurrentGid();
593
594 // here must decide, which languages to import
595 $langs = array_flip(
596 array_map(
597 function ($value)
598 {
599 return mb_strtoupper($value);
600 },
601 array_keys(Location\Admin\NameHelper::getLanguageList()))
602 );
603
604 foreach($block as $i => $data)
605 {
606 $code = $data['CODE'];
607
608 // this spike is only for cutting off COUNTRY_DISTRICT
609 // strongly need for the more generalized mechanism for excluding certain types
610 if (!!($this->options['REQUEST']['OPTIONS']['EXCLUDE_COUNTRY_DISTRICT']))
611 {
612 if (!isset($this->data['COUNTRY_2_DISTRICT']))
613 {
614 $this->data['COUNTRY_2_DISTRICT'] = [];
615 }
616
617 if ($data['TYPE_CODE'] == 'COUNTRY')
618 {
619 $this->data['LAST_COUNTRY'] = $data['CODE'];
620 }
621 elseif ($data['TYPE_CODE'] == 'COUNTRY_DISTRICT')
622 {
623 $this->data['COUNTRY_2_DISTRICT'][$code] = $this->data['LAST_COUNTRY'];
624 continue;
625 }
626 else
627 {
628 if (isset($this->data['COUNTRY_2_DISTRICT'][$data['PARENT_CODE']]))
629 {
630 $data['PARENT_CODE'] = $this->data['COUNTRY_2_DISTRICT'][$data['PARENT_CODE']];
631 }
632 }
633 }
634
635 // this spike is only for cutting off COUNTRY_DISTRICT
636 // strongly need for the more generalized mechanism for excluding certain types
637 if (!!($this->options['REQUEST']['OPTIONS']['EXCLUDE_COUNTRY_DISTRICT']))
638 {
639 if (!isset($this->data['COUNTRY_2_DISTRICT']))
640 {
641 $this->data['COUNTRY_2_DISTRICT'] = [];
642 }
643
644 if ($data['TYPE_CODE'] == 'COUNTRY')
645 {
646 $this->data['LAST_COUNTRY'] = $data['CODE'];
647 }
648 elseif ($data['TYPE_CODE'] == 'COUNTRY_DISTRICT')
649 {
650 $this->data['COUNTRY_2_DISTRICT'][$code] = $this->data['LAST_COUNTRY'];
651 continue;
652 }
653 else
654 {
655 if (isset($this->data['COUNTRY_2_DISTRICT'][$data['PARENT_CODE']]))
656 {
657 $data['PARENT_CODE'] = $this->data['COUNTRY_2_DISTRICT'][$data['PARENT_CODE']];
658 }
659 }
660 }
661
662 if(isset($this->data['existedlocs']['static'][$code]) || isset($this->data['existedlocs'][$gid][$code])) // already exists
663 continue;
664
665 if(!isset($this->data['types']['allowed'][$data['TYPE_CODE']])) // disallowed
666 continue;
667
668 // have to check existence first
669 if(!$this->data['TABLE_WERE_EMPTY'])
670 {
671 $existedId = $this->checkLocationCodeExists($code);
672
673 if(intval($existedId))
674 {
675 $this->data['existedlocs'][$gid][$code] = $existedId;
676 continue;
677 }
678 }
679
681 // transform parent
682 if($data['PARENT_CODE'] <> '')
683 {
684 if(isset($this->data['existedlocs']['static'][$data['PARENT_CODE']]))
685 {
686 $data['PARENT_ID'] = $this->data['existedlocs']['static'][$data['PARENT_CODE']];
687 }
688 elseif(isset($this->data['existedlocs'][$gid][$data['PARENT_CODE']]))
689 {
690 $data['PARENT_ID'] = $this->data['existedlocs'][$gid][$data['PARENT_CODE']];
691 }
692 else
693 {
694 $data['PARENT_ID'] = 0;
695 }
696 }
697 else
698 {
699 $data['PARENT_ID'] = 0;
700 }
701
702 unset($data['PARENT_CODE']);
703
705 // transform type
706 $data['TYPE_ID'] = $this->data['types']['code2id'][$data['TYPE_CODE']];
707 unset($data['TYPE_CODE']);
708
710 // add
711 $names = $data['NAME'];
712 unset($data['NAME']);
713
714 $external = $data['EXT'];
715 unset($data['EXT']);
716
717 $data['LONGITUDE'] = (float)($data['LONGITUDE'] ?? 0);
718 $data['LATITUDE'] = (float)($data['LATITUDE'] ?? 0);
719 if(!$this->checkExternalServiceAllowed('GEODATA'))
720 {
721 $data['LONGITUDE'] = 0;
722 $data['LATITUDE'] = 0;
723 }
724
725 $locationId = $this->hitData['HANDLES']['LOCATION']->insert($data);
726
727 // store for further PARENT_CODE to PARENT_ID mapping
728 //if(!strlen($this->data['types']['last']) || $this->data['types']['last'] != $data['TYPE_CODE'])
729 $this->data['existedlocs'][$gid][$data['CODE']] = $locationId;
730
732 // add names
733 if(is_array($names) && !empty($names))
734 {
735 if(is_array($langs))
736 {
737 foreach($langs as $lid => $f)
738 {
739 $lid = mb_strtolower($lid);
740 $toAdd = static::getTranslatedName($names, $lid);
741
742 $this->hitData['HANDLES']['NAME']->insert(array(
743 'NAME' => $toAdd['NAME'],
744 'NAME_UPPER' => mb_strtoupper($toAdd['NAME']),
745 'LANGUAGE_ID' => $lid,
746 'LOCATION_ID' => $locationId
747 ));
748 }
749 }
750 }
751
753 // add external
754 if(is_array($external) && !empty($external))
755 {
756 foreach($external as $sCode => $values)
757 {
758 if($this->checkExternalServiceAllowed($sCode))
759 {
760 $serviceId = $this->data['externalService']['code2id'][$sCode];
761
762 if(!$serviceId)
763 throw new Main\SystemException('Location import failed: external service doesnt exist');
764
765 if($sCode == 'ZIP_LOWER')
766 {
767 if($values == '')
768 continue;
769
770 $values = explode(',', $values);
771
772 if(!is_array($values))
773 continue;
774
775 $values = array_unique($values);
776 }
777
778 if(is_array($values))
779 {
780 foreach($values as $val)
781 {
782 if($val == '')
783 continue;
784
785 $this->hitData['HANDLES']['EXTERNAL']->insert(array(
786 'SERVICE_ID' => $serviceId,
787 'XML_ID' => $val,
788 'LOCATION_ID' => $locationId
789 ));
790 }
791 }
792 }
793 }
794 }
795 }
796 }
797
798 protected function getCurrentGid()
799 {
800 return $this->data['files'][$this->data['current']['fIndex']]['memgroup'];
801 }
802
803 protected function stageProcessFiles()
804 {
805 if($this->dbConnType == self::DB_TYPE_ORACLE)
806 $mtu = self::INSERTER_MTU_ORACLE;
807 else
808 $mtu = self::INSERTER_MTU;
809
810 $this->hitData['HANDLES']['LOCATION'] = new BlockInserter(array(
811 'entityName' => '\Bitrix\Sale\Location\LocationTable',
812 'exactFields' => array('CODE', 'TYPE_ID', 'PARENT_ID', 'LATITUDE', 'LONGITUDE'),
813 'parameters' => array(
814 'autoIncrementFld' => 'ID',
815 'mtu' => $mtu
816 )
817 ));
818
819 $this->hitData['HANDLES']['NAME'] = new BlockInserter(array(
820 'entityName' => '\Bitrix\Sale\Location\Name\LocationTable',
821 'exactFields' => array('NAME', 'NAME_UPPER', 'LANGUAGE_ID', 'LOCATION_ID'),
822 'parameters' => array(
823 'autoIncrementFld' => 'ID',
824 'mtu' => $mtu
825 )
826 ));
827
828 $this->hitData['HANDLES']['EXTERNAL'] = new BlockInserter(array(
829 'entityName' => '\Bitrix\Sale\Location\ExternalTable',
830 'exactFields' => array('SERVICE_ID', 'XML_ID', 'LOCATION_ID'),
831 'parameters' => array(
832 'autoIncrementFld' => 'ID',
833 'mtu' => $mtu
834 )
835 ));
836
837 if($this->getStep() == 0)
838 {
839 // set initial values
840 $this->data['current'] = array(
841 'fIndex' => 0,
842 'bytesRead' => 0, // current file bytes read
843 'linesRead' => 0
844 );
845
846 $this->hitData['HANDLES']['LOCATION']->resetAutoIncrementFromIndex(); // synchronize sequences, etc...
847
848 // check if we are empty
849 $this->data['TABLE_WERE_EMPTY'] = Location\LocationTable::getCountByFilter() == 0;
850
852 }
853
854 while($this->checkQuota())
855 {
856 $block = $this->readBlockFromCurrentFile2();
857 $this->importBlock($block);
858
859 // clean memory
861
862 // or the current file is completely exhausted
863 if($this->checkFileCompletelyRead())
864 {
865 //$this->logMessage('Lines read: '.$this->data['current']['linesRead']);
866
867 // charge next file
868 unset($this->hitData['csv']);
869 $this->data['current']['fIndex']++; // next file to go
870 $this->data['current']['bytesRead'] = 0; // read counter from the beginning
871 $this->data['current']['linesRead'] = 0;
872 $this->data['current']['legacy'] = array(); // drop legacy data of the file, if were any. bye-bye
873
874 // may be that is all?
875 if($this->checkAllFilesRead())
876 {
877 unset($this->data['existedlocs']); // uff, remove that huge array at last
878
879 $this->nextStage();
880 break;
881 }
882 }
883
884 $this->nextStep();
885 }
886
887 $this->hitData['HANDLES']['LOCATION']->flush();
888 $this->hitData['HANDLES']['NAME']->flush();
889 $this->hitData['HANDLES']['EXTERNAL']->flush();
890
891 $this->logMessage('Inserted, go next: '.$this->getHitTimeString());
892
893 $this->logMemoryUsage();
894 }
895
897 {
898 $pRange = $this->getStagePercent($this->stage) - $this->getStagePercent($this->stage - 1);
899
900 $totalSize = 0;
901 $fileBytesRead = 0;
902
903 if(!isset($this->data['current']['fIndex']))
904 return 0;
905
906 $fIndex = $this->data['current']['fIndex'];
907
908 $i = -1;
909 foreach($this->data['files'] as $file)
910 {
911 $i++;
912
913 if($i < $fIndex)
914 $fileBytesRead += $file['size'];
915
916 $totalSize += $file['size'];
917 }
918
919 if(!$totalSize)
920 return 0;
921
922 return round($pRange * (intval($fileBytesRead + $this->data['current']['bytesRead']) / $totalSize));
923 }
924
926 // STAGE 4
927
928 protected function stageIntegrityPreserve()
929 {
930 $lay = $this->getRemoteLayout(true);
931
932 $this->restoreIndexes('IX_B_SALE_LOC_PARENT');
933
934 $res = Location\LocationTable::getList(array(
935 'select' => array(
936 'ID', 'CODE'
937 ),
938 'filter' => array(
939 '=PARENT_ID' => 0
940 )
941 ));
942 $relations = array();
943 $code2id = array();
944 while($item = $res->fetch())
945 {
946 if(isset($lay[$item['CODE']]) && ((string) $lay[$item['CODE']]['PARENT_CODE'] != '')/*except root*/)
947 $relations[$item['CODE']] = $lay[$item['CODE']]['PARENT_CODE'];
948 // relations is a match between codes from the layout file
949
950 $code2id[$item['CODE']] = $item['ID'];
951 }
952
953 $parentCode2id = $this->getLocationCodeToIdMap($relations);
954
955 foreach($code2id as $code => $id)
956 {
957 if (!isset($relations[$code]))
958 {
959 continue;
960 }
961 if ((string)($parentCode2id[$relations[$code]] ?? '') !== '') // parent really exists
962 {
964 $id,
965 ['PARENT_ID' => $parentCode2id[$relations[$code]]]
966 );
967 if (!$res->isSuccess())
968 {
969 throw new Main\SystemException('Cannot make element become a child of its legal parent');
970 }
971 }
972 }
973
974 $this->nextStage();
975 }
976
978 // STAGE 5
979
980 protected function stageRebalanceWalkTree()
981 {
982 if(!isset($this->data['rebalance']['queue']))
983 {
984 $this->restoreIndexes('IX_B_SALE_LOC_PARENT');
985
986 $this->logMessage('initialize Queue');
987
988 $this->data['rebalance']['margin'] = -1;
989 $this->data['processed'] = 0;
990 $this->data['rebalance']['queue'] = array(array('I' => 'root', 'D' => 0));
991
993 $res = Main\HttpApplication::getConnection()->query("select count(*) as CNT from {$tableName}")->fetch();
994
995 $this->data['rebalance']['cnt'] = intval($res['CNT']);
996 }
997
998 $i = -1;
999 while(!empty($this->data['rebalance']['queue']) && $this->checkQuota())
1000 {
1001 $i++;
1002
1003 $node =& $this->data['rebalance']['queue'][0];
1004
1005 if(isset($node['L']))
1006 {
1007 // we have already been here
1008 array_shift($this->data['rebalance']['queue']);
1009 if($node['I'] != 'root') // we dont need for ROOT item in outgoing
1010 {
1011 $this->acceptRebalancedNode(array(
1012 'I' => $node['I'],
1013 'D' => $node['D'],
1014 'L' => $node['L'],
1015 'R' => ++$this->data['rebalance']['margin']
1016 ));
1017 }
1018 else
1019 $this->data['rebalance']['margin']++;
1020 }
1021 else
1022 {
1023 $a = $this->getCachedBundle($node['I']);
1024
1025 if(!empty($a))
1026 {
1027 // go deeper
1028 $node['L'] = ++$this->data['rebalance']['margin'];
1029
1030 foreach($a as $id)
1031 {
1032 if($this->checkNodeIsParent($id))
1033 {
1034 array_unshift($this->data['rebalance']['queue'], array('I' => $id, 'D' => $node['D'] + 1));
1035 }
1036 else // we dont need to put it to the query
1037 {
1038 $this->acceptRebalancedNode(array(
1039 'I' => $id,
1040 'D' => $node['D'] + 1,
1041 'L' => ++$this->data['rebalance']['margin'],
1042 'R' => ++$this->data['rebalance']['margin']
1043 ));
1044 }
1045 }
1046 }
1047 else
1048 {
1049 array_shift($this->data['rebalance']['queue']);
1050 $this->acceptRebalancedNode(array(
1051 'I' => $node['I'],
1052 'D' => $node['D'],
1053 'L' => ++$this->data['rebalance']['margin'],
1054 'R' => ++$this->data['rebalance']['margin']
1055 ));
1056 }
1057 }
1058 }
1059
1060 $this->logMessage('Q size is '.count($this->data['rebalance']['queue']).' already processed: '.$this->data['processed'].'/'.$this->data['rebalance']['cnt']);
1061 $this->logMemoryUsage();
1062
1063 if(empty($this->data['rebalance']['queue']))
1064 {
1065 // last flush & then merge
1066 $this->mergeRebalancedNodes();
1067
1068 $this->nextStage();
1069 return;
1070 }
1071
1072 if($this->rebalanceInserter)
1073 $this->rebalanceInserter->flush();
1074
1075 $this->nextStep();
1076 }
1077
1079 {
1080 $processed = $this->data['processed'] ?? 0;
1081 $cnt = $this->data['rebalance']['cnt'] ?? 0;
1082 if (!$processed || !$cnt)
1083 return 0;
1084
1085 $pRange = $this->getCurrentPercentRange();
1086 $part = round($pRange * ($processed / $cnt));
1087
1088 return min($part, $pRange);
1089 }
1090
1092 // STAGE 6
1093
1095 {
1096 $this->dropTempTable();
1097 $this->nextStage();
1098 }
1099
1101 // STAGE 7
1102
1103 protected function stageRestoreIndexes()
1104 {
1105 $indexes = array(
1106 'IX_B_SALE_LOC_MARGINS',
1107 'IX_B_SALE_LOC_MARGINS_REV',
1108 //'IX_B_SALE_LOC_PARENT', // already restored at REBALANCE_WALK_TREE stage
1109 'IX_B_SALE_LOC_DL',
1110 'IX_B_SALE_LOC_TYPE',
1111 'IX_B_SALE_LOC_NAME_NAME_U',
1112 'IX_B_SALE_LOC_NAME_LI_LI',
1113 'IX_B_SALE_LOC_EXT_LID_SID',
1114
1115 // legacy
1116 'IXS_LOCATION_COUNTRY_ID',
1117 'IXS_LOCATION_REGION_ID',
1118 'IXS_LOCATION_CITY_ID',
1119 );
1120
1121 if(isset($indexes[$this->getStep()]))
1122 {
1123 $this->restoreIndexes($indexes[$this->getStep()]);
1124 $this->logMessage('Index restored: '.$indexes[$this->getStep()]);
1125 $this->nextStep();
1126 }
1127 else
1128 {
1129 Location\LocationTable::resetLegacyPath(); // for backward compatibility
1130 $this->nextStage();
1131 }
1132 }
1133
1135 {
1136 $pRange = $this->getCurrentPercentRange();
1137 $step = $this->getStep();
1138
1139 $stepCount = 12;
1140
1141 if($step >= $stepCount)
1142 {
1143 return $pRange;
1144 }
1145 else
1146 {
1147 return round($pRange * ($step / $stepCount));
1148 }
1149 }
1150
1152 // about stage util functions
1153
1154 protected function getLanguageId(): string
1155 {
1156 return $this->options['LANGUAGE_ID'];
1157 }
1158
1162 public function getTypes()
1163 {
1164 $result = array();
1165 $res = Location\TypeTable::getList(array(
1166 'select' => array(
1167 'CODE', 'TNAME' => 'NAME.NAME'
1168 ),
1169 'filter' => array(
1170 'NAME.LANGUAGE_ID' => $this->getLanguageId()
1171 ),
1172 'order' => array(
1173 'SORT' => 'asc',
1174 'NAME.NAME' => 'asc'
1175 )
1176 ));
1177 while($item = $res->fetch())
1178 $result[$item['CODE']] = $item['TNAME'];
1179
1180 return $result;
1181 }
1182
1183 public function getStatisticsAll()
1184 {
1185 $this->getStatistics();
1186
1187 return $this->stat;
1188 }
1189
1190 public function getStatistics($type = 'TOTAL')
1191 {
1192 if(empty($this->stat))
1193 {
1194 $types = \Bitrix\Sale\Location\Admin\TypeHelper::getTypes(array('LANGUAGE_ID' => $this->getLanguageId()));
1195
1196 $res = Location\LocationTable::getList(array(
1197 'select' => array(
1198 'CNT',
1199 'TCODE' => 'TYPE.CODE'
1200 ),
1201 'group' => array(
1202 'TYPE_ID'
1203 )
1204 ));
1205 $total = 0;
1206 $stat = array();
1207 while($item = $res->fetch())
1208 {
1209 $total += intval($item['CNT']);
1210 $stat[$item['TCODE']] = $item['CNT'];
1211 }
1212
1213 foreach($types as $code => $data)
1214 {
1215 $this->stat[$code] = array(
1216 'NAME' => $data['NAME_CURRENT'],
1217 'CODE' => $code,
1218 'CNT' => isset($stat[$code]) ? intval($stat[$code]) : 0,
1219 );
1220 }
1221
1222 $this->stat['TOTAL'] = array('CNT' => $total, 'CODE' => 'TOTAL');
1223
1224 $res = Location\GroupTable::getList(array(
1225 'runtime' => array(
1226 'CNT' => array(
1227 'data_type' => 'integer',
1228 'expression' => array(
1229 'COUNT(*)'
1230 )
1231 )
1232 ),
1233 'select' => array(
1234 'CNT'
1235 )
1236 ))->fetch();
1237
1238 $this->stat['GROUPS'] = array('CNT' => intval($res['CNT']), 'CODE' => 'GROUPS');
1239 }
1240
1241 return intval($this->stat[$type]['CNT']);
1242 }
1243
1244 public function determineLayoutToImport()
1245 {
1246 $lay = $this->getRemoteLayout(true);
1247
1248 $parentness = array();
1249 foreach($lay as $data)
1250 {
1251 $parentCode = (string)($data['PARENT_CODE'] ?? '');
1252 if ($parentCode === '')
1253 {
1254 continue;
1255 }
1256 $parentness[$parentCode] ??= 0;
1257 $parentness[$parentCode]++;
1258 }
1259
1260 $bundles = array_flip($this->data['settings']['sets']);
1261
1262 $selectedLayoutParts = array();
1263 foreach($bundles as $bundle => $void)
1264 {
1265 if(!isset($lay[$bundle]))
1266 throw new Main\SystemException('Unknown bundle passed in request');
1267
1268 // obtaining intermediate chain parts
1269 $chain = array();
1270
1271 $currentBundle = $bundle;
1272 $i = -1;
1273 while($currentBundle)
1274 {
1275 $i++;
1276
1277 if($i > 50) // smth is really bad
1278 throw new Main\SystemException('Too deep recursion got when building chains. Layout file is broken');
1279
1280 if(isset($lay[$currentBundle]))
1281 {
1282 $chain[] = $currentBundle;
1283 if($lay[$currentBundle]['PARENT_CODE'] <> '')
1284 {
1285 $currentBundle = $lay[$currentBundle]['PARENT_CODE'];
1286
1287 if(!isset($lay[$currentBundle]))
1288 {
1289 throw new Main\SystemException('Unknown parent bundle found ('.$currentBundle.'). Layout file is broken');
1290 }
1291 }
1292 else
1293 {
1294 $currentBundle = false;
1295 }
1296 }
1297 }
1298
1299 if(is_array($chain) && !empty($chain))
1300 {
1301 $chain = array_reverse($chain);
1302
1303 // find first occurance of selected bundle in the chain
1304 $subChain = array();
1305 foreach($chain as $i => $node)
1306 {
1307 if(isset($bundles[$node]))
1308 {
1309 $subChain = array_slice($chain, $i);
1310 break;
1311 }
1312 }
1313
1314 if(!empty($subChain))
1315 $selectedLayoutParts = array_merge($selectedLayoutParts, $subChain);
1316 }
1317 }
1318
1319 //$this->data['settings']['layout'] = $lay;
1320 $selectedLayoutParts = array_unique($selectedLayoutParts);
1321
1322 $this->data['settings']['bundles'] = array('endpoints' => array(), 'allpoints' => $selectedLayoutParts);
1323
1324 foreach($selectedLayoutParts as $bCode)
1325 {
1326 if(!isset($parentness[$bCode]))
1327 $this->data['settings']['bundles']['endpoints'][] = $bCode;
1328 //else
1329 // $this->data['settings']['bundles']['middlepoints'][] = $bCode;
1330 }
1331 unset($this->data['settings']['sets']);
1332 }
1333
1334 public function convertBlock($block)
1335 {
1336 $converted = array();
1337
1338 foreach($block as $line)
1339 {
1340 if($line[0] == 'S')
1341 $typeCode = 'COUNTRY';
1342 elseif($line[0] == 'R')
1343 $typeCode = 'REGION';
1344 elseif($line[0] == 'T')
1345 $typeCode = 'CITY';
1346 else
1347 throw new Main\SystemException('Unknown type found in legacy file');
1348
1349 $code = md5(implode(':', $line));
1350
1351 if($typeCode == 'REGION')
1352 $parentCode = $this->data['current']['legacy']['lastCOUNTRY'];
1353 elseif($typeCode == 'CITY')
1354 $parentCode = $this->data['current']['legacy']['lastParent'];
1355 else
1356 $parentCode = '';
1357
1358 if($typeCode != 'CITY')
1359 {
1360 $this->data['current']['legacy']['last'.$typeCode] = $code;
1361 $this->data['current']['legacy']['lastParent'] = $code;
1362 }
1363
1364 $cLine = array(
1365 'CODE' => $code,
1366 'TYPE_CODE' => $typeCode,
1367 'PARENT_CODE' => $parentCode
1368 );
1369
1370 $lang = false;
1371 $expectLang = true;
1372 $lineLen = count($line);
1373 for($k = 1; $k < $lineLen; $k++)
1374 {
1375 if($expectLang)
1376 {
1377 $lang = $line[$k];
1378 }
1379 else
1380 {
1381 $cLine['NAME'][mb_strtoupper($lang)]['NAME'] = $line[$k];
1382 }
1383
1384 $expectLang = !$expectLang;
1385 }
1386
1387 $converted[] = $cLine;
1388 }
1389
1390 return $converted;
1391 }
1392
1393 public function checkSource($sType)
1394 {
1395 return $this->data['settings']['options']['SOURCE'] == $sType;
1396 }
1397
1398 // download layout from server
1399 public function getRemoteLayout($getFlat = false)
1400 {
1401 [$localPath, $tmpDirCreated] = $this->getTemporalDirectory();
1402
1403 static::downloadFile(self::REMOTE_LAYOUT_FILE, self::LOCAL_LAYOUT_FILE, false, $localPath);
1404
1405 $csv = new CSVReader();
1406 $csv->AddEventCallback('AFTER_ASSOC_LINE_READ', array($this, 'provideEnFromRu'));
1407 $res = $csv->ReadBlock($localPath.self::LOCAL_LAYOUT_FILE);
1408
1409 $result = array();
1410 if($getFlat)
1411 {
1412 foreach($res as $line)
1413 $result[$line['CODE']] = $line;
1414 $csv->CloseFile();
1415 return $result;
1416 }
1417
1418 $lang = $this->getLanguageId();
1419
1420 foreach($res as $line)
1421 {
1422 $line['NAME'][mb_strtoupper($lang)] = static::getTranslatedName($line['NAME'], $lang);
1423 $result[$line['PARENT_CODE']][$line['CODE']] = $line;
1424 }
1425 $csv->CloseFile();
1426
1427 if($tmpDirCreated)
1428 {
1429 $this->deleteDirectory($localPath);
1430 }
1431
1432 return $result;
1433 }
1434
1435 // download types from server
1436 public function getRemoteTypes()
1437 {
1438 if(!$this->useCache || !isset($this->data['settings']['remote']['types']))
1439 {
1440 [$localPath, $tmpDirCreated] = $this->getTemporalDirectory();
1441
1442 static::downloadFile(self::REMOTE_TYPE_FILE, self::LOCAL_TYPE_FILE, false, $localPath);
1443
1444 $csv = new CSVReader();
1445 $csv->AddEventCallback('AFTER_ASSOC_LINE_READ', array($this, 'provideEnFromRu'));
1446 $res = $csv->ReadBlock($localPath.self::LOCAL_TYPE_FILE);
1447
1448 $result = array();
1449 foreach($res as $line)
1450 $result[$line['CODE']] = $line;
1451
1452 $this->data['settings']['remote']['types'] = $result;
1453 $csv->CloseFile();
1454 if($tmpDirCreated)
1455 {
1456 $this->deleteDirectory($localPath);
1457 }
1458 }
1459
1460 return $this->data['settings']['remote']['types'];
1461 }
1462
1463 // download external services from server
1465 {
1466 if(!$this->useCache || !isset($this->data['settings']['remote']['external_services']))
1467 {
1468 [$localPath, $tmpDirCreated] = $this->getTemporalDirectory();
1469
1470 static::downloadFile(self::REMOTE_EXTERNAL_SERVICE_FILE, self::LOCAL_EXTERNAL_SERVICE_FILE, false, $localPath);
1471
1472 $csv = new CSVReader();
1473 $res = $csv->ReadBlock($localPath.self::LOCAL_EXTERNAL_SERVICE_FILE);
1474
1475 $result = array();
1476 foreach($res as $line)
1477 $result[$line['CODE']] = $line;
1478
1479 $this->data['settings']['remote']['external_services'] = $result;
1480 $csv->CloseFile();
1481 if($tmpDirCreated)
1482 {
1483 $this->deleteDirectory($localPath);
1484 }
1485 }
1486
1487 return $this->data['settings']['remote']['external_services'];
1488 }
1489
1490 // download type groups from server
1491 public function getRemoteTypeGroups()
1492 {
1493 if(!$this->useCache || !isset($this->data['settings']['remote']['typeGroups']))
1494 {
1495 [$localPath, $tmpDirCreated] = $this->getTemporalDirectory();
1496
1497 static::downloadFile(self::REMOTE_TYPE_GROUP_FILE, self::LOCAL_TYPE_GROUP_FILE, false, $localPath);
1498
1499 $csv = new CSVReader();
1500 $res = $csv->ReadBlock($localPath.self::LOCAL_TYPE_GROUP_FILE);
1501
1502 $result = array();
1503 foreach($res as $line)
1504 {
1505 $result[$line['CODE']] = explode(':', $line['TYPES']);
1506 }
1507
1508 $this->data['settings']['remote']['typeGroups'] = $result;
1509 $csv->CloseFile();
1510 if($tmpDirCreated)
1511 {
1512 $this->deleteDirectory($localPath);
1513 }
1514 }
1515
1516 return $this->data['settings']['remote']['typeGroups'];
1517 }
1518
1519 public function getTypeLevels($langId = LANGUAGE_ID): array
1520 {
1521 $types = $this->getRemoteTypes();
1522 $levels = array();
1523
1524 if(!isset($langId))
1525 {
1526 $langId = $this->getLanguageId();
1527 }
1528
1529 foreach($types as $type)
1530 {
1531 if($type['SELECTORLEVEL'] = intval($type['SELECTORLEVEL']))
1532 {
1533 $name = static::getTranslatedName($type['NAME'], $langId);
1534 $levels[$type['SELECTORLEVEL']]['NAMES'][] = $name['NAME'];
1535 $levels[$type['SELECTORLEVEL']]['TYPES'][] = $type['CODE'];
1536
1537 $levels[$type['SELECTORLEVEL']]['DEFAULT'] = ($type['DEFAULTSELECT'] == '1');
1538 }
1539 }
1540
1541 foreach($levels as &$group)
1542 $group['NAMES'] = implode(', ', $group['NAMES']);
1543
1544 ksort($levels, SORT_NUMERIC);
1545
1546 return $levels;
1547 }
1548
1549 public static function getSiteLanguages()
1550 {
1551 static $langs;
1552
1553 if($langs == null)
1554 {
1555 $langs = array();
1556
1557 $res = \Bitrix\Main\SiteTable::getList(array('filter' => array('ACTIVE' => 'Y'), 'select' => array('LANGUAGE_ID'), 'group' => array('LANGUAGE_ID')));
1558 while($item = $res->fetch())
1559 {
1560 $langs[mb_strtoupper($item['LANGUAGE_ID'])] = true;
1561 }
1562
1563 $langs = array_unique(array_keys($langs)); // all active sites languages
1564 }
1565
1566 return $langs;
1567 }
1568
1569 public function getRequiredLanguages()
1570 {
1571 $required = array(mb_strtoupper($this->getLanguageId()));
1572
1573 $langs = Location\Admin\NameHelper::getLanguageList();
1574 if(isset($langs['en']))
1575 $required[] = 'EN';
1576
1577 return array_unique(array_merge($required, static::getSiteLanguages())); // current language plus for all active sites
1578 }
1579
1580 // read type.csv and build type table
1581 protected function buildTypeTable()
1582 {
1583 $this->data['types_processed'] ??= false;
1584 if ($this->data['types_processed'])
1585 {
1586 return;
1587 }
1588
1589 // read existed
1590 $existed = static::getExistedTypes();
1591
1592 if($this->checkSource(self::SOURCE_REMOTE))
1593 {
1594 $rTypes = $this->getRemoteTypes();
1595 $this->getRemoteTypeGroups();
1596
1597 $existed = static::createTypes($rTypes, $existed);
1598
1599 if(intval($dl = $this->data['settings']['options']['DEPTH_LIMIT']))
1600 {
1601 // here we must find out what types we are allowed to read
1602
1603 $typesGroupped = $this->getTypeLevels();
1604
1605 if(!isset($typesGroupped[$dl]))
1606 throw new Main\SystemException('Unknow type level to limit');
1607
1608 $allowed = [];
1609 foreach($typesGroupped as $gId => $group)
1610 {
1611 if($gId > $dl)
1612 break;
1613
1614 foreach($group['TYPES'] as $type)
1615 $allowed[] = $type;
1616 }
1617
1618 $this->data['types']['allowed'] = $allowed;
1619 }
1620 else
1621 {
1622 $allowed = [];
1623 foreach($rTypes as $type)
1624 {
1625 $allowed[] = $type['CODE'];
1626 }
1627 $this->data['types']['allowed'] = $allowed;
1628 }
1629 }
1630 elseif($this->checkSource(self::SOURCE_FILE))
1631 {
1632 $codes = [];
1633 if(!empty($existed) && is_array($existed))
1634 {
1635 $codes = array_keys($existed);
1636 }
1637
1638 $this->data['types']['allowed'] = $codes;
1639 }
1640
1641 if (empty($this->data['types']['allowed']))
1642 {
1643 $this->data['types']['last'] = null;
1644 }
1645 else
1646 {
1647 $this->data['types']['last'] = $this->data['types']['allowed'][count($this->data['types']['allowed']) - 1];
1648 }
1649 $this->data['types']['allowed'] = array_flip($this->data['types']['allowed']);
1650
1651 $this->data['types']['code2id'] = $existed;
1652 $this->data['types_processed'] = true;
1653 }
1654
1656 {
1657 if($this->data['settings']['additional'] === false)
1658 return true; // ANY
1659
1660 if($code == 'ZIP_LOWER')
1661 $code = 'ZIP';
1662
1663 return isset($this->data['settings']['additional'][$code]);
1664 }
1665
1666 protected function buildExternalSerivceTable()
1667 {
1668 $this->data['external_processed'] ??= false;
1669 if ($this->data['external_processed'])
1670 {
1671 return;
1672 }
1673
1674 // read existed
1675 $existed = static::getExistedServices();
1676
1677 if($this->checkSource(self::SOURCE_REMOTE))
1678 {
1679 $external = $this->getRemoteExternalServices();
1680 foreach($external as $line)
1681 {
1682 if(!isset($existed[$line['CODE']]) && $this->checkExternalServiceAllowed($line['CODE']))
1683 {
1684 $existed[$line['CODE']] = static::createService($line);
1685 }
1686 }
1687 unset($this->data['settings']['remote']['external_services']);
1688 }
1689
1690 $this->data['externalService']['code2id'] = $existed;
1691 $this->data['external_processed'] = true;
1692 }
1693
1694 protected function buildStaticLocationIndex()
1695 {
1696 $parameters = array(
1697 'select' => array('ID', 'CODE')
1698 );
1699
1700 // get static index, it will be always in memory
1701 $parameters['filter'] = array('TYPE.CODE' => array('COUNTRY', 'COUNTRY_DISTRICT', 'REGION')); // todo: from typegroup later
1702
1703 $this->data['existedlocs'] = array('static' => array());
1704 $res = Location\LocationTable::getList($parameters);
1705 while($item = $res->fetch())
1706 $this->data['existedlocs']['static'][$item['CODE']] = $item['ID']; // get existed, "static" index
1707 }
1708
1710 {
1711 $res = Location\LocationTable::getList(array('filter' => array('CODE' => $buffer), 'select' => array('ID', 'CODE')));
1712 while($item = $res->fetch())
1713 $result[$item['CODE']] = $item['ID'];
1714 }
1715
1716 protected function getLocationCodeToIdMap($codes)
1717 {
1718 if(empty($codes))
1719 return array();
1720
1721 $i = -1;
1722 $buffer = array();
1723 $result = array();
1724 foreach($codes as $code)
1725 {
1726 $i++;
1727
1728 if($i == self::MAX_CODE_FETCH_BLOCK_LEN)
1729 {
1731
1732 $buffer = array();
1733 $i = -1;
1734 }
1735
1736 if($code <> '')
1737 {
1738 $buffer[] = $code;
1739 }
1740 }
1741
1742 // last iteration
1744
1745 return $result;
1746 }
1747
1748 protected function manageExistedLocationIndex($memGroups)
1749 {
1750 $before = implode(', ', array_keys($this->data['existedlocs']));
1751
1752 $cleaned = false;
1753 foreach($this->data['existedlocs'] as $gid => $bundles)
1754 {
1755 if($gid == 'static' || in_array($gid, $memGroups))
1756 continue;
1757
1758 $cleaned = true;
1759
1760 $this->logMessage('Memory clean: REMOVING Group '.$gid);
1761 unset($this->data['existedlocs'][$gid]);
1762 }
1763
1764 if($cleaned)
1765 {
1766 $this->logMessage('BEFORE memgroups: '.$before);
1767 $this->logMessage('Clear all but '.$memGroups[0]);
1768
1769 $this->logMessage('AFTER memgroups: '.implode(', ', array_keys($this->data['existedlocs'])));
1770 }
1771 }
1772
1774 // about file and network I/O
1775
1776 protected function checkIndexExistsByName($indexName, $tableName)
1777 {
1778 $indexName = $this->dbHelper->forSql(trim($indexName));
1779 $tableName = $this->dbHelper->forSql(trim($tableName));
1780
1781 if(!mb_strlen($indexName) || !mb_strlen($tableName))
1782 return false;
1783
1784 if($this->dbConnType == self::DB_TYPE_MYSQL)
1785 $res = $this->dbConnection->query("show index from ".$tableName);
1786 elseif($this->dbConnType == self::DB_TYPE_ORACLE)
1787 $res = $this->dbConnection->query("SELECT INDEX_NAME as Key_name FROM USER_IND_COLUMNS WHERE TABLE_NAME = '".mb_strtoupper($tableName)."'");
1788 elseif($this->dbConnType == self::DB_TYPE_MSSQL)
1789 {
1790 $res = $this->dbConnection->query("SELECT si.name Key_name
1791 FROM sysindexkeys s
1792 INNER JOIN syscolumns c ON s.id = c.id AND s.colid = c.colid
1793 INNER JOIN sysobjects o ON s.id = o.Id AND o.xtype = 'U'
1794 LEFT JOIN sysindexes si ON si.indid = s.indid AND si.id = s.id
1795 WHERE o.name = '".mb_strtoupper($tableName)."'");
1796 }
1797
1798 while($item = $res->fetch())
1799 {
1800 if (isset($item['Key_name']) && $item['Key_name'] === $indexName)
1801 {
1802 return true;
1803 }
1804 if (isset($item['KEY_NAME']) && $item['KEY_NAME'] === $indexName)
1805 {
1806 return true;
1807 }
1808 }
1809
1810 return false;
1811 }
1812
1813 protected function dropIndexByName($indexName, $tableName)
1814 {
1815 $indexName = $this->dbHelper->forSql(trim($indexName));
1816 $tableName = $this->dbHelper->forSql(trim($tableName));
1817
1818 if(!mb_strlen($indexName) || !mb_strlen($tableName))
1819 return false;
1820
1821 if(!$this->checkIndexExistsByName($indexName, $tableName))
1822 return false;
1823
1824 if($this->dbConnType == self::DB_TYPE_MYSQL)
1825 $this->dbConnection->query("alter table {$tableName} drop index {$indexName}");
1826 elseif($this->dbConnType == self::DB_TYPE_ORACLE)
1827 $this->dbConnection->query("drop index {$indexName}");
1828 elseif($this->dbConnType == self::DB_TYPE_MSSQL)
1829 $this->dbConnection->query("drop index {$indexName} on {$tableName}");
1830
1831 return true;
1832 }
1833
1834 public static function getIndexMap(): array
1835 {
1836 $locationTable = Location\LocationTable::getTableName();
1837 $locationNameTable = Location\Name\LocationTable::getTableName();
1838 $locationExternalTable = Location\ExternalTable::getTableName();
1839
1840 if (Main\HttpApplication::getConnection()->getType() === 'pgsql')
1841 {
1842 return [
1843 'ix_b_sale_location_left_margin_right_margin' => ['TABLE' => $locationTable, 'COLUMNS' => ['LEFT_MARGIN', 'RIGHT_MARGIN']],
1844 'ix_b_sale_location_right_margin_left_margin' => ['TABLE' => $locationTable, 'COLUMNS' => ['RIGHT_MARGIN', 'LEFT_MARGIN']],
1845 'ix_b_sale_location_parent_id' => ['TABLE' => $locationTable, 'COLUMNS' => ['PARENT_ID']],
1846 'ix_b_sale_location_depth_level' => ['TABLE' => $locationTable, 'COLUMNS' => ['DEPTH_LEVEL']],
1847 'ix_b_sale_location_type_id' => ['TABLE' => $locationTable, 'COLUMNS' => ['TYPE_ID']],
1848 'ix_b_sale_location_type_id_left_margin_right_margin' => ['TABLE' => $locationTable, 'COLUMNS' => ['TYPE_ID', 'LEFT_MARGIN', 'RIGHT_MARGIN']],
1849
1850 'ix_b_sale_loc_name_name_upper' => ['TABLE' => $locationNameTable, 'COLUMNS' => ['NAME_UPPER']],
1851 'ix_b_sale_loc_name_location_id_language_id' => ['TABLE' => $locationNameTable, 'COLUMNS' => ['LOCATION_ID', 'LANGUAGE_ID']],
1852
1853 'ix_b_sale_loc_ext_location_id_service_id' => ['TABLE' => $locationExternalTable, 'COLUMNS' => ['LOCATION_ID', 'SERVICE_ID']],
1854
1855 // legacy
1856 'ix_b_sale_location_country_id' => ['TABLE' => $locationTable, 'COLUMNS' => ['COUNTRY_ID']],
1857 'ix_b_sale_location_region_id' => ['TABLE' => $locationTable, 'COLUMNS' => ['REGION_ID']],
1858 'ix_b_sale_location_city_id' => ['TABLE' => $locationTable, 'COLUMNS' => ['CITY_ID']],
1859
1860 // obsolete
1861 'ix_b_sale_location_1' => ['TABLE' => $locationTable, 'COLUMNS' => ['COUNTRY_ID'], 'DROP_ONLY' => true],
1862 'ix_b_sale_location_2' => ['TABLE' => $locationTable, 'COLUMNS' => ['REGION_ID'], 'DROP_ONLY' => true],
1863 'ix_b_sale_location_3' => ['TABLE' => $locationTable, 'COLUMNS' => ['CITY_ID'], 'DROP_ONLY' => true],
1864 ];
1865 }
1866
1867 return [
1868 'IX_SALE_LOCATION_MARGINS' => ['TABLE' => $locationTable, 'COLUMNS' => ['LEFT_MARGIN', 'RIGHT_MARGIN']],
1869 'IX_SALE_LOCATION_MARGINS_REV' => ['TABLE' => $locationTable, 'COLUMNS' => ['RIGHT_MARGIN', 'LEFT_MARGIN']],
1870 'IX_SALE_LOCATION_PARENT' => ['TABLE' => $locationTable, 'COLUMNS' => ['PARENT_ID']],
1871 'IX_SALE_LOCATION_DL' => ['TABLE' => $locationTable, 'COLUMNS' => ['DEPTH_LEVEL']],
1872 'IX_SALE_LOCATION_TYPE' => ['TABLE' => $locationTable, 'COLUMNS' => ['TYPE_ID']],
1873 'IX_SALE_L_NAME_NAME_UPPER' => ['TABLE' => $locationNameTable, 'COLUMNS' => ['NAME_UPPER']],
1874 'IX_SALE_L_NAME_LID_LID' => ['TABLE' => $locationNameTable, 'COLUMNS' => ['LOCATION_ID', 'LANGUAGE_ID']],
1875 'IX_B_SALE_LOC_EXT_LID_SID' => ['TABLE' => $locationExternalTable, 'COLUMNS' => ['LOCATION_ID', 'SERVICE_ID']],
1876 'IX_SALE_LOCATION_TYPE_MARGIN' => ['TABLE' => $locationTable, 'COLUMNS' => ['TYPE_ID', 'LEFT_MARGIN', 'RIGHT_MARGIN']],
1877
1878 // legacy
1879 'IXS_LOCATION_COUNTRY_ID' => ['TABLE' => $locationTable, 'COLUMNS' => ['COUNTRY_ID']],
1880 'IXS_LOCATION_REGION_ID' => ['TABLE' => $locationTable, 'COLUMNS' => ['REGION_ID']],
1881 'IXS_LOCATION_CITY_ID' => ['TABLE' => $locationTable, 'COLUMNS' => ['CITY_ID']],
1882
1883 // obsolete
1884 'IX_B_SALE_LOCATION_1' => ['TABLE' => $locationTable, 'COLUMNS' => ['COUNTRY_ID'], 'DROP_ONLY' => true],
1885 'IX_B_SALE_LOCATION_2' => ['TABLE' => $locationTable, 'COLUMNS' => ['REGION_ID'], 'DROP_ONLY' => true],
1886 'IX_B_SALE_LOCATION_3' => ['TABLE' => $locationTable, 'COLUMNS' => ['CITY_ID'], 'DROP_ONLY' => true],
1887 ];
1888 }
1889
1890 protected function dropIndexes($certainIndex = false)
1891 {
1892 $map = static::getIndexMap();
1893
1894 foreach($map as $index => $ixData)
1895 {
1896 if($certainIndex !== false && $certainIndex != $index)
1897 continue;
1898
1899 $this->dropIndexByName($index, $ixData['TABLE']);
1900 }
1901 }
1902
1903 public function restoreIndexes($certainIndex = false)
1904 {
1905 $map = $this->getIndexMap();
1906
1907 foreach($map as $ixName => $ixData)
1908 {
1909 if (($ixData['DROP_ONLY'] ?? null) === true)
1910 {
1911 continue;
1912 }
1913
1914 if($certainIndex !== false && $certainIndex != $ixName)
1915 continue;
1916
1917 if($this->checkIndexExistsByName($ixName, $ixData['TABLE']))
1918 return false;
1919
1920 $this->dbConnection->query('CREATE INDEX '.$ixName.' ON '.$ixData['TABLE'].' ('.implode(', ', $ixData['COLUMNS']).')');
1921 }
1922
1923 return true;
1924 }
1925
1926 private function getCachedBundle($id)
1927 {
1928 $locationTable = Location\LocationTable::getTableName();
1929
1930 $bundle = array();
1931 $res = $this->dbConnection->query("select ID from {$locationTable} where PARENT_ID = ".($id == 'root' ? '0' : intval($id)));
1932 while($item = $res->fetch())
1933 $bundle[] = $item['ID'];
1934
1935 return $bundle;
1936 }
1937
1938 private function checkNodeIsParent($id)
1939 {
1940 $locationTable = Location\LocationTable::getTableName();
1941
1942 $res = $this->dbConnection->query("select count(*) as CNT from {$locationTable} where PARENT_ID = ".($id == 'root' ? '0' : intval($id)))->fetch();
1943
1944 return intval($res['CNT']);
1945 }
1946
1947 private function mergeRebalancedNodes()
1948 {
1949 if($this->rebalanceInserter)
1950 {
1951 $this->logMessage('Finally, MERGE is in progress');
1952
1953 $this->rebalanceInserter->flush();
1954
1955 // merge temp table with location table
1956 // there should be more generalized method
1957 Location\LocationTable::mergeRelationsFromTemporalTable(self::TREE_REBALANCE_TEMP_TABLE_NAME, false, array('LEFT_MARGIN' => 'L', 'RIGHT_MARGIN' => 'R', 'DEPTH_LEVEL' => 'D', 'ID' => 'I'));
1958 }
1959 }
1960
1961 private function acceptRebalancedNode($node)
1962 {
1963 $this->createTempTable();
1964
1965 if(!$this->rebalanceInserter)
1966 {
1967 if($this->dbConnType == self::DB_TYPE_ORACLE)
1968 $mtu = self::TREE_REBALANCE_TEMP_BLOCK_LEN_O;
1969 else
1970 $mtu = self::TREE_REBALANCE_TEMP_BLOCK_LEN;
1971
1972 $this->rebalanceInserter = new BlockInserter(array(
1973 'tableName' => self::TREE_REBALANCE_TEMP_TABLE_NAME,
1974 'exactFields' => array(
1975 'I' => array('data_type' => 'integer'),
1976 'L' => array('data_type' => 'integer'),
1977 'R' => array('data_type' => 'integer'),
1978 'D' => array('data_type' => 'integer'),
1979 ),
1980 'parameters' => array(
1981 'mtu' => $mtu
1982 )
1983 ));
1984 }
1985
1986 $this->data['processed']++;
1987
1988 $this->rebalanceInserter->insert($node);
1989 }
1990
1991 private function dropTempTable()
1992 {
1993 if($this->dbConnection->isTableExists(self::TREE_REBALANCE_TEMP_TABLE_NAME))
1994 $this->dbConnection->query("drop table ".self::TREE_REBALANCE_TEMP_TABLE_NAME);
1995 }
1996
1997 private function createTempTable()
1998 {
1999 $this->data['rebalance']['tableCreated'] ??= false;
2000 if ($this->data['rebalance']['tableCreated'])
2001 {
2002 return;
2003 }
2004
2005 $tableName = self::TREE_REBALANCE_TEMP_TABLE_NAME;
2006
2007 if ($this->dbConnection->isTableExists($tableName))
2008 {
2009 $this->dbConnection->query("truncate table {$tableName}");
2010 }
2011 else
2012 {
2013
2014 if($this->dbConnType == self::DB_TYPE_ORACLE)
2015 {
2016 $this->dbConnection->query("create table {$tableName} (
2017 I NUMBER(18),
2018 L NUMBER(18),
2019 R NUMBER(18),
2020 D NUMBER(18)
2021 )");
2022 }
2023 else
2024 {
2025 $this->dbConnection->query("create table {$tableName} (
2026 I int,
2027 L int,
2028 R int,
2029 D int
2030 )");
2031 }
2032
2033 }
2034
2035 $this->data['rebalance']['tableCreated'] = true;
2036 }
2037
2038 protected function checkFileCompletelyRead()
2039 {
2040 return $this->data['current']['bytesRead'] >= $this->data['files'][$this->data['current']['fIndex']]['size'];
2041 }
2042
2043 protected function checkAllFilesRead()
2044 {
2045 return $this->data['current']['fIndex'] >= count($this->data['files']);
2046 }
2047
2048 protected function checkBufferIsFull($bufferSize)
2049 {
2050 return $bufferSize >= $this->getCurrStageStepSize();
2051 }
2052
2053 protected static function downloadFile($fileName, $storeAs, $skip404 = false, $storeTo = false)
2054 {
2055 // useless thing
2056 if(!$storeTo)
2057 $storeTo = \CTempFile::GetDirectoryName(1);
2058
2059 $storeTo .= $storeAs;
2060
2061 if(file_exists($storeTo))
2062 {
2063 if(!is_writable($storeTo))
2064 throw new Main\SystemException('Cannot remove previous '.$storeAs.' file');
2065
2066 unlink($storeTo);
2067 }
2068
2069 $query = '';
2070 if (!defined('SALE_LOCATIONS_IMPORT_SOURCE_URL'))
2071 {
2072 if (self::checkRegion())
2073 {
2074 $query = 'https://' . self::DISTRIBUTOR_HOST . self::REMOTE_PATH . $fileName;
2075 }
2076 }
2077 else
2078 {
2079 $query = 'http://' . SALE_LOCATIONS_IMPORT_SOURCE_URL . '/' . $fileName;
2080 }
2081
2082 if ($query === '')
2083 {
2084 throw new Main\SystemException('Region is not allowed');
2085 }
2086
2087 $client = new HttpClient();
2088
2089 if(!$client->download($query, $storeTo))
2090 {
2091 $eFormatted = array();
2092 foreach($client->getError() as $code => $desc)
2093 $eFormatted[] = trim($desc.' ('.$code.')');
2094
2095 throw new Main\SystemException('File download failed: '.implode(', ', $eFormatted).' ('.$query.')');
2096 }
2097
2098 $status = intval($client->getStatus());
2099
2100 if($status != 200 && file_exists($storeTo))
2101 unlink($storeTo);
2102
2103 $okay = $status == 200 || ($status == 404 && $skip404);
2104
2105 // honestly we should check for all 2xx codes, but for now this is enough
2106 if(!$okay)
2107 throw new Main\SystemException('File download failed: http error '.$status.' ('.$query.')');
2108
2109 return filesize($storeTo);
2110 }
2111
2115 protected static function cleanWorkDirectory()
2116 {
2117 }
2118
2119 protected function getFileNameByIndex($i)
2120 {
2121 return self::LOCAL_SETS_PATH.sprintf(self::LOCAL_LOCATION_FILE, $i);
2122 }
2123
2124 public function saveUserFile($inputName)
2125 {
2126 if(is_array($_FILES[$inputName]))
2127 {
2128 if($_FILES[$inputName]['error'] > 0)
2129 throw new Main\SystemException(self::explainFileUploadError($_FILES[$inputName]['error']));
2130
2131 if(!in_array($_FILES[$inputName]['type'], array(
2132 'text/plain',
2133 'text/csv',
2134 'application/vnd.ms-excel',
2135 'application/octet-stream'
2136 )))
2137 {
2138 throw new Main\SystemException('Unsupported file type');
2139 }
2140
2142
2143 [$localPath, $tmpDirCreated] = $this->getTemporalDirectory();
2144 $fileName = $localPath.'/'.static::USER_FILE_TEMP_NAME;
2145
2146 if(!@copy($_FILES[$inputName]['tmp_name'], $fileName))
2147 {
2148 $lastError = error_get_last();
2149 throw new Main\SystemException($lastError['message']);
2150 }
2151
2152 $_SESSION[static::USER_FILE_DIRECTORY_SESSION_KEY] = $localPath;
2153 }
2154 else
2155 throw new Main\SystemException('No file were uploaded');
2156 }
2157
2158 protected static function explainFileUploadError($error)
2159 {
2160 switch ($error)
2161 {
2162 case UPLOAD_ERR_INI_SIZE:
2163 $message = 'The uploaded file exceeds the upload_max_filesize directive in php.ini';
2164 break;
2165 case UPLOAD_ERR_FORM_SIZE:
2166 $message = 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form';
2167 break;
2168 case UPLOAD_ERR_PARTIAL:
2169 $message = 'The uploaded file was only partially uploaded';
2170 break;
2171 case UPLOAD_ERR_NO_FILE:
2172 $message = 'No file were uploaded';
2173 break;
2174 case UPLOAD_ERR_NO_TMP_DIR:
2175 $message = 'Missing a temporary folder';
2176 break;
2177 case UPLOAD_ERR_CANT_WRITE:
2178 $message = 'Failed to write file to disk';
2179 break;
2180 case UPLOAD_ERR_EXTENSION:
2181 $message = 'File upload stopped by extension';
2182 break;
2183
2184 default:
2185 $message = 'Unknown upload error';
2186 break;
2187 }
2188 return $message;
2189 }
2190
2191 // all this mess is only to get import work on Bitrix24 (which does not provide any temporal directory in a typical meaning)
2192 protected function getTemporalDirectory(): array
2193 {
2194 $dataLocalPath = trim((string)($this->data['LOCAL_PATH'] ?? ''));
2195 $wasCreated = false;
2196 if ($dataLocalPath !== '' && \Bitrix\Main\IO\Directory::isDirectoryExists($dataLocalPath))
2197 {
2198 $localPath = $dataLocalPath;
2199 }
2200 else
2201 {
2202 $wasCreated = true;
2203 $localPath = \CTempFile::GetDirectoryName(10);
2204 if (!\Bitrix\Main\IO\Directory::isDirectoryExists($localPath))
2205 {
2207 }
2208 }
2209
2210 return [
2211 $localPath,
2212 $wasCreated,
2213 ];
2214 }
2215
2216 protected static function createDirectory($path)
2217 {
2218 if(!\Bitrix\Main\IO\Directory::isDirectoryExists($path))
2219 {
2221 }
2222 }
2223
2224 protected static function deleteDirectory($path)
2225 {
2226 if(\Bitrix\Main\IO\Directory::isDirectoryExists($path))
2227 {
2229 }
2230 }
2231
2232 protected function normalizeQueryArray($value)
2233 {
2234 $result = array();
2235 if(is_array($value))
2236 {
2237 foreach($value as $v)
2238 {
2239 if($v <> '')
2240 {
2241 $result[] = $this->parseQueryCode($v);
2242 }
2243 }
2244 }
2245
2246 $result = array_unique($result);
2247 sort($result, SORT_STRING);
2248
2249 return $result;
2250 }
2251
2252 protected static function parseQueryCode($value)
2253 {
2254 $value = mb_strtolower(trim($value));
2255
2256 if(!preg_match('#^[a-z0-9]+$#i', $value))
2257 throw new Main\SystemException('Bad request parameter');
2258
2259 return $value;
2260 }
2261
2262 public function turnOffCache()
2263 {
2264 $this->useCache = false;
2265 }
2266
2267 ########################################################
2268 ## static part is used in places like wizards, etc
2269
2270 public static function getExistedTypes()
2271 {
2272 $existed = array();
2273 $res = Location\TypeTable::getList(array('select' => array('ID', 'CODE', 'SORT')));
2274 while($item = $res->fetch())
2275 $existed[$item['CODE']] = $item['ID'];
2276
2277 return $existed;
2278 }
2279
2280 public static function getExistedServices()
2281 {
2282 $existed = array();
2283 $res = Location\ExternalServiceTable::getList(array('select' => array('ID', 'CODE')));
2284 while($item = $res->fetch())
2285 $existed[$item['CODE']] = $item['ID'];
2286
2287 return $existed;
2288 }
2289
2290 public static function createTypes($types, $existed = false)
2291 {
2292 // read existed
2293 if($existed === false)
2294 $existed = static::getExistedTypes();
2295
2296 // here we try to add type names for ALL languages
2297 $langs = Location\Admin\NameHelper::getLanguageList();
2298
2299 foreach($types as $line)
2300 {
2301 // for sure
2302 unset($line['SELECTORLEVEL']);
2303 unset($line['DEFAULTSELECT']);
2304
2305 $names = array();
2306
2307 if(!is_array($line['NAME']))
2308 $line['NAME'] = array();
2309
2310 if(is_array($langs))
2311 {
2312 foreach($langs as $lid => $f)
2313 {
2314 $names[mb_strtoupper($lid)] = static::getTranslatedName($line['NAME'], $lid);
2315 }
2316 $line['NAME'] = $names;
2317 }
2318
2319 if(!isset($existed[$line['CODE']]))
2320 {
2321 $existed[$line['CODE']] = static::createType($line);
2322 }
2323 else
2324 {
2325 // ensure it has all appropriate translations
2326 // we can not use ::updateMultipleForOwner() here, because user may rename his types manually
2327 Location\Name\TypeTable::addAbsentForOwner($existed[$line['CODE']], $names);
2328 }
2329 }
2330
2331 return $existed;
2332 }
2333
2334 protected static function getTranslatedName($names, $languageId)
2335 {
2336 $languageIdMapped = mb_strtoupper(Location\Admin\NameHelper::mapLanguage($languageId));
2337 $languageId = mb_strtoupper($languageId);
2338
2339 if ((string)($names[$languageId]['NAME'] ?? null) !== '')
2340 {
2341 return $names[$languageId];
2342 }
2343
2344 if ((string)($names[$languageIdMapped]['NAME'] ?? null) !== '')
2345 {
2346 return $names[$languageIdMapped];
2347 }
2348
2349 return $names['EN'];
2350 }
2351
2352 public static function createType($type)
2353 {
2355
2356 if(is_array($type))
2357 {
2358 foreach($type as $fld => $val)
2359 {
2360 if(!isset($map[$fld]))
2361 {
2362 unset($type[$fld]);
2363 }
2364 }
2365 }
2366
2368 if(!$res->isSuccess())
2369 throw new Main\SystemException('Type creation failed: '.implode(', ', $res->getErrorMessages()));
2370
2371 return $res->getId();
2372 }
2373
2374 public static function createService($service)
2375 {
2376 $res = Location\ExternalServiceTable::add($service);
2377 if(!$res->isSuccess())
2378 throw new Main\SystemException('External service creation failed: '.implode(', ', $res->getErrorMessages()));
2379
2380 return $res->getId();
2381 }
2382
2383 public static function getTypeMap($file)
2384 {
2385 $csvReader = new CSVReader();
2386 $csvReader->LoadFile($file);
2387
2388 $types = array();
2389 $i = 0;
2390 while($type = $csvReader->FetchAssoc())
2391 {
2392 if($i) // fix for CSVReader parent class bug
2393 {
2394 unset($type['SELECTORLEVEL']);
2395 unset($type['DEFAULTSELECT']);
2396
2397 $types[$type['CODE']] = $type;
2398 }
2399
2400 $i++;
2401 }
2402
2403 return $types;
2404 }
2405
2406 public static function getServiceMap($file)
2407 {
2408 $csvReader = new CSVReader();
2409 $csvReader->LoadFile($file);
2410
2411 $services = array();
2412 while($service = $csvReader->FetchAssoc())
2413 $services[$service['CODE']] = $service;
2414
2415 return $services;
2416 }
2417
2418 // this is generally for e-shop installer
2419 public static function importFile(&$descriptior)
2420 {
2421 $timeLimit = ini_get('max_execution_time');
2422 if ($timeLimit < $descriptior['TIME_LIMIT']) set_time_limit($descriptior['TIME_LIMIT'] + 5);
2423
2424 $endTime = time() + $descriptior['TIME_LIMIT'];
2425
2426 if($descriptior['STEP'] == 'rebalance')
2427 {
2430 $descriptior['STEP'] = 'done';
2431 }
2432
2433 if($descriptior['STEP'] == 'import')
2434 {
2435 if(!isset($descriptior['DO_SYNC']))
2436 {
2437 $res = \Bitrix\Sale\Location\LocationTable::getList(array('select' => array('CNT')))->fetch();
2438 $descriptior['DO_SYNC'] = intval($res['CNT'] > 0);
2439 }
2440
2441 if(!isset($descriptior['TYPES']))
2442 {
2443 $descriptior['TYPE_MAP'] = static::getTypeMap($descriptior['TYPE_FILE']);
2444 $descriptior['TYPES'] = static::createTypes($descriptior['TYPE_MAP']);
2445
2446 $descriptior['SERVICE_MAP'] = static::getServiceMap($descriptior['SERVICE_FILE']);
2447 $descriptior['SERVICES'] = static::getExistedServices();
2448 }
2449
2450 $csvReader = new CSVReader();
2451 $csvReader->LoadFile($descriptior['FILE']);
2452
2453 while(time() < $endTime)
2454 {
2455 $block = $csvReader->ReadBlockLowLevel($descriptior['POS']/*changed inside*/, 10);
2456
2457 if(!count($block))
2458 break;
2459
2460 foreach($block as $item)
2461 {
2462 if($descriptior['DO_SYNC'])
2463 {
2464 $id = static::checkLocationCodeExists($item['CODE']);
2465 if($id)
2466 {
2467 $descriptior['CODES'][$item['CODE']] = $id;
2468 continue;
2469 }
2470 }
2471
2472 // type
2473 $item['TYPE_ID'] = $descriptior['TYPES'][$item['TYPE_CODE']];
2474 unset($item['TYPE_CODE']);
2475
2476 // parent id
2477 if($item['PARENT_CODE'] <> '')
2478 {
2479 if(!isset($descriptior['CODES'][$item['PARENT_CODE']]))
2480 {
2481 $descriptior['CODES'][$item['PARENT_CODE']] = static::checkLocationCodeExists($item['PARENT_CODE']);
2482 }
2483
2484 $item['PARENT_ID'] = $descriptior['CODES'][$item['PARENT_CODE']];
2485 }
2486 unset($item['PARENT_CODE']);
2487
2488 // ext
2489 if(is_array($item['EXT']))
2490 {
2491 foreach($item['EXT'] as $code => $values)
2492 {
2493 if(!empty($values))
2494 {
2495 if(!isset($descriptior['SERVICES'][$code]))
2496 {
2497 $descriptior['SERVICES'][$code] = static::createService(array(
2498 'CODE' => $code
2499 ));
2500 }
2501
2502 if($code == 'ZIP_LOWER')
2503 {
2504 if($values[0] == '')
2505 continue;
2506
2507 $values = explode(',', $values[0]);
2508
2509 if(!is_array($values))
2510 continue;
2511
2512 $values = array_unique($values);
2513 }
2514
2515 if(is_array($values))
2516 {
2517 foreach($values as $value)
2518 {
2519 if($value == '')
2520 continue;
2521
2522 $item['EXTERNAL'][] = array(
2523 'SERVICE_ID' => $descriptior['SERVICES'][$code],
2524 'XML_ID' => $value
2525 );
2526 }
2527 }
2528 }
2529 }
2530 }
2531 unset($item['EXT'], $item['ZIP_LOWER']);
2532
2534 $item,
2535 array(
2536 'RESET_LEGACY' => false,
2537 'REBALANCE' => false
2538 )
2539 );
2540
2541 if(!$res->isSuccess())
2542 throw new Main\SystemException('Cannot create location');
2543
2544 $descriptior['CODES'][$item['CODE']] = $res->getId();
2545 }
2546 }
2547
2548 if(!count($block))
2549 {
2550 unset($descriptior['CODES']);
2551 $descriptior['STEP'] = 'rebalance';
2552 }
2553 }
2554
2555 return $descriptior['STEP'] == 'done';
2556 }
2557
2558 public function provideEnFromRu(&$data)
2559 {
2560 // restore at least "EN" translation
2561 if (!is_array($data))
2562 {
2563 return;
2564 }
2565 if (!isset($data['NAME']['RU']))
2566 {
2567 return;
2568 }
2569 if (!is_array($data['NAME']['RU']))
2570 {
2571 return;
2572 }
2573 $data['NAME']['EN'] ??= [];
2574
2575 foreach ($data['NAME']['RU'] as $k => $v)
2576 {
2577 if ((string)($data['NAME']['EN'][$k] ?? '') === '')
2578 {
2579 $data['NAME']['EN'][$k] = Location\Admin\NameHelper::translitFromUTF8($data['NAME']['RU'][$k]);
2580 }
2581 }
2582 }
2583
2584 private static function checkRegion(): bool
2585 {
2586 $region = Application::getInstance()->getLicense()->getRegion();
2587 $isBitrixSiteManagementOnly = !Loader::includeModule('bitrix24') && !Loader::includeModule('intranet');
2588
2589 return $region === 'ru' || $region === 'by' || $region === 'kz' || $isBitrixSiteManagementOnly;
2590 }
2591}
$path
Определения access_edit.php:21
$type
Определения options.php:106
static getConnection($name="")
Определения application.php:638
static createDirectory($path)
Определения directory.php:142
static deleteDirectory($path)
Определения directory.php:150
Определения loader.php:13
static getList(array $parameters=array())
Определения datamanager.php:431
static deleteAll()
Определения connector.php:317
static getTableName()
Определения external.php:41
static downloadFile($fileName, $storeAs, $skip404=false, $storeTo=false)
Определения importprocess.php:2053
restoreIndexes($certainIndex=false)
Определения importprocess.php:1903
prepareImportProcessOptions($options)
Определения importprocess.php:161
static checkLocationCodeExists($code)
Определения importprocess.php:574
static createTypes($types, $existed=false)
Определения importprocess.php:2290
static getTranslatedName($names, $languageId)
Определения importprocess.php:2334
getRemoteLayout($getFlat=false)
Определения importprocess.php:1399
dropIndexByName($indexName, $tableName)
Определения importprocess.php:1813
checkIndexExistsByName($indexName, $tableName)
Определения importprocess.php:1776
static explainFileUploadError($error)
Определения importprocess.php:2158
manageExistedLocationIndex($memGroups)
Определения importprocess.php:1748
getLocationCodeToIdMapQuery($buffer, &$result)
Определения importprocess.php:1709
static importFile(&$descriptior)
Определения importprocess.php:2419
static parseQueryCode($value)
Определения importprocess.php:2252
static createService($service)
Определения importprocess.php:2374
dropIndexes($certainIndex=false)
Определения importprocess.php:1890
getTypeLevels($langId=LANGUAGE_ID)
Определения importprocess.php:1519
static resetLegacyPath()
Определения location.php:394
static addExtended(array $data, array $additional=array())
Определения location.php:162
static update($primary, array $data)
Определения location.php:218
static getTableName()
Определения location.php:45
static addAbsentForOwner($primaryOwner, $names, $behaviour=array('TREAT_EMPTY_AS_ABSENT'=> true))
Определения nameentity.php:191
static resort($dontCareEvents=false)
Определения tree.php:296
static mergeRelationsFromTemporalTable($temporalTabName, $additinalFlds=array(), $fldMap=array())
Определения tree.php:1019
static getCountByFilter($filter=array())
Определения tree.php:1089
static getMap()
Определения type.php:142
static add(array $data)
Определения type.php:48
addStage($params)
Определения process.php:67
getStagePercent($sNum=false)
Percentage.
Определения process.php:327
checkQuota()
Quotas info.
Определения process.php:398
logMessage($message='', $addTimeStamp=true)
Определения process.php:468
setTimeLimit($timeLimit)
Определения process.php:403
$f
Определения component_props.php:52
if(!defined("ADMIN_AJAX_MODE") &&(($_REQUEST["mode"] ?? '') !='excel')) $buffer
Определения epilog_admin_after.php:40
</td ></tr ></table ></td ></tr >< tr >< td class="bx-popup-label bx-width30"><?=GetMessage("PAGE_NEW_TAGS")?> array( $site)
Определения file_new.php:804
$res
Определения filter_act.php:7
$result
Определения get_property_values.php:14
$query
Определения get_search.php:11
$region
Определения .description.php:13
$isBitrixSiteManagementOnly
Определения .description.php:12
$inputName
Определения options.php:197
if(!is_null($config))($config as $configItem)(! $configItem->isVisible()) $code
Определения options.php:195
if(!defined('SITE_ID')) $lang
Определения include.php:91
$status
Определения session.php:10
$name
Определения menu_edit.php:35
$map
Определения config.php:5
Определения directory.php:3
$service
Определения payment.php:18
if(mb_strlen($order)< 6) $desc
Определения payment.php:44
$message
Определения payment.php:8
if( $daysToExpire >=0 &&$daysToExpire< 60 elseif)( $daysToExpire< 0)
Определения prolog_main_admin.php:393
$fileName
Определения quickway.php:305
$i
Определения factura.php:643
</p ></td >< td valign=top style='border-top:none;border-left:none;border-bottom:solid windowtext 1.0pt;border-right:solid windowtext 1.0pt;padding:0cm 2.0pt 0cm 2.0pt;height:9.0pt'>< p class=Normal align=center style='margin:0cm;margin-bottom:.0001pt;text-align:center;line-height:normal'>< a name=ТекстовоеПоле54 ></a ><?=($taxRate > count( $arTaxList) > 0) ? $taxRate."%"
Определения waybill.php:936
else $a
Определения template.php:137
$val
Определения options.php:1793
$langs
Определения options.php:141
$error
Определения subscription_card_product.php:20
$k
Определения template_pdf.php:567
$localPath
Определения template_copy.php:184
$iterator
Определения yandex_run.php:610