PageRenderTime 46ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/modules/translate/lib/controller/import/importcsv.php

https://gitlab.com/alexprowars/bitrix
PHP | 557 lines | 423 code | 88 blank | 46 comment | 72 complexity | ca43696a5800c75a87af150921786e6e MD5 | raw file
  1. <?php
  2. namespace Bitrix\Translate\Controller\Import;
  3. use Bitrix\Main;
  4. use Bitrix\Main\Localization\Loc;
  5. use Bitrix\Translate;
  6. /**
  7. * Harvester of the lang folder disposition.
  8. */
  9. class ImportCsv
  10. extends Translate\Controller\Action
  11. implements Translate\Controller\ITimeLimit, Translate\Controller\IProcessParameters
  12. {
  13. use Translate\Controller\Stepper;
  14. use Translate\Controller\ProcessParams;
  15. /** @var int */
  16. private $seekLine;
  17. /** @var string */
  18. protected static $documentRoot;
  19. /** @var string[] */
  20. protected static $enabledLanguages;
  21. /** @var string[] */
  22. protected static $sourceEncoding;
  23. /** @var boolean */
  24. protected static $isUtfMode;
  25. /** @var int Session tab counter. */
  26. private $tabId = 0;
  27. /** @var string */
  28. private $encodingIn;
  29. /** @var string */
  30. private $updateMethod;
  31. /** @var string */
  32. private $csvFilePath;
  33. /** @var Translate\IO\CsvFile */
  34. private $csvFile;
  35. /** @var string[] */
  36. private $languageList;
  37. /** @var string[] */
  38. private $columnList;
  39. /** @var int */
  40. private $importedPhraseCount = 0;
  41. /**
  42. * \Bitrix\Main\Engine\Action constructor.
  43. *
  44. * @param string $name Action name.
  45. * @param Main\Engine\Controller $controller Parent controller object.
  46. * @param array $config Additional configuration.
  47. */
  48. public function __construct($name, Main\Engine\Controller $controller, $config = array())
  49. {
  50. $fields = ['tabId', 'encodingIn', 'updateMethod', 'csvFilePath', 'seekLine', 'importedPhrasesCount'];
  51. $this->keepField($fields);
  52. foreach ($fields as $key)
  53. {
  54. if (!empty($config[$key]))
  55. {
  56. $this->{$key} = $config[$key];
  57. }
  58. }
  59. self::$documentRoot = rtrim(Translate\IO\Path::tidy(Main\Application::getDocumentRoot()), '/');
  60. self::$enabledLanguages = Translate\Config::getEnabledLanguages();
  61. foreach (self::$enabledLanguages as $languageId)
  62. {
  63. self::$sourceEncoding[$languageId] = mb_strtolower(Main\Localization\Translation::getSourceEncoding($languageId));
  64. }
  65. parent::__construct($name, $controller, $config);
  66. }
  67. /**
  68. * Runs controller action.
  69. *
  70. * @param boolean $runBefore Flag to run onBeforeRun event handler.
  71. *
  72. * @return array
  73. */
  74. public function run($runBefore = false)
  75. {
  76. if ($runBefore)
  77. {
  78. $this->onBeforeRun();
  79. }
  80. $this->csvFile = new Translate\IO\CsvFile($this->csvFilePath);
  81. $this->csvFile
  82. ->setFieldsType(Translate\IO\CsvFile::FIELDS_TYPE_WITH_DELIMITER)
  83. ->setFirstHeader(false)
  84. ;
  85. if (!$this->csvFile->openLoad())
  86. {
  87. $this->addError(new Main\Error(Loc::getMessage('TR_IMPORT_EMPTY_FILE_ERROR')));
  88. return array(
  89. 'STATUS' => Translate\Controller\STATUS_COMPLETED
  90. );
  91. }
  92. if (!$this->verifyCsvFile())
  93. {
  94. return array(
  95. 'STATUS' => Translate\Controller\STATUS_COMPLETED
  96. );
  97. }
  98. if ($this->csvFile->hasUtf8Bom())
  99. {
  100. $this->encodingIn = 'utf-8';
  101. }
  102. if ($this->isNewProcess)
  103. {
  104. $this->clearProgressParameters();
  105. $this->totalItems = 0;
  106. while ($csvRow = $this->csvFile->fetch())
  107. {
  108. $this->totalItems ++;
  109. }
  110. $this->processedItems = 0;
  111. $this->seekLine = 0;
  112. $this->saveProgressParameters();
  113. return array(
  114. 'STATUS' => Translate\Controller\STATUS_PROGRESS,
  115. 'PROCESSED_ITEMS' => 0,
  116. 'TOTAL_ITEMS' => $this->totalItems,
  117. );
  118. }
  119. else
  120. {
  121. $progressParams = $this->getProgressParameters();
  122. if (isset($progressParams['totalItems']))
  123. {
  124. $this->totalItems = $progressParams['totalItems'];
  125. }
  126. if (isset($progressParams['seekLine']))
  127. {
  128. $this->seekLine = $progressParams['seekLine'];
  129. }
  130. if (isset($progressParams['importedPhrasesCount']))
  131. {
  132. $this->importedPhraseCount = $progressParams['importedPhrasesCount'];
  133. }
  134. }
  135. return $this->performStep('runImporting');
  136. }
  137. /**
  138. * Imports data from csv file.
  139. *
  140. * @return array
  141. */
  142. private function runImporting()
  143. {
  144. $fileIndex = $this->columnList['file'];
  145. $keyIndex = $this->columnList['key'];
  146. $currentLine = 0;
  147. $maxLinePortion = 500;
  148. $hasFinishedReading = false;
  149. while (true)
  150. {
  151. $linePortion = 0;
  152. $phraseList = array();
  153. while ($csvRow = $this->csvFile->fetch())
  154. {
  155. $currentLine ++;
  156. if ($this->seekLine > 0)
  157. {
  158. if ($currentLine <= $this->seekLine)
  159. {
  160. continue;
  161. }
  162. }
  163. if (
  164. !is_array($csvRow) ||
  165. empty($csvRow) ||
  166. (count($csvRow) == 1 && ($csvRow[0] === null || $csvRow[0] === ''))
  167. )
  168. {
  169. continue;
  170. }
  171. $rowErrors = array();
  172. $filePath = (isset($csvRow[$fileIndex]) ? $csvRow[$fileIndex] : '');
  173. $key = (isset($csvRow[$keyIndex]) ? $csvRow[$keyIndex] : '');
  174. if ($filePath == '' || $key == '')
  175. {
  176. if ($filePath == '')
  177. {
  178. $rowErrors[] = Loc::getMessage('TR_IMPORT_ERROR_DESTINATION_FILEPATH_ABSENT');
  179. }
  180. if ($key == '')
  181. {
  182. $rowErrors[] = Loc::getMessage('TR_IMPORT_ERROR_PHRASE_CODE_ABSENT');
  183. }
  184. $this->addError(new Main\Error(Loc::getMessage(
  185. 'TR_IMPORT_ERROR_LINE_FILE_EXT',
  186. [
  187. '#LINE#' => ($currentLine + 1),
  188. '#ERROR#' => implode('; ', $rowErrors)
  189. ]
  190. )));
  191. continue;
  192. }
  193. $linePortion ++;
  194. if (!isset($phraseList[$filePath]))
  195. {
  196. $phraseList[$filePath] = [];
  197. }
  198. foreach ($this->languageList as $languageId)
  199. {
  200. if (!isset($phraseList[$filePath][$languageId]))
  201. {
  202. $phraseList[$filePath][$languageId] = [];
  203. }
  204. $langIndex = $this->columnList[$languageId];
  205. if (!isset($csvRow[$langIndex]) || (empty($csvRow[$langIndex]) && $csvRow[$langIndex] !== '0'))
  206. {
  207. continue;
  208. }
  209. $phrase = str_replace("\\\\", "\\", $csvRow[$langIndex]);
  210. $encodingOut = self::$sourceEncoding[$languageId];
  211. if (!empty($this->encodingIn) && $this->encodingIn !== $encodingOut)
  212. {
  213. $phrase = Main\Text\Encoding::convertEncoding($phrase, $this->encodingIn, $encodingOut);
  214. }
  215. $checked = true;
  216. if ($encodingOut === 'utf-8')
  217. {
  218. $validPhrase = preg_replace("/[^\x01-\x7F]/", '', $phrase);// remove ASCII characters
  219. if ($validPhrase !== $phrase)
  220. {
  221. $checked = Translate\Text\StringHelper::validateUtf8OctetSequences($phrase);
  222. }
  223. unset($validPhrase);
  224. }
  225. if ($checked)
  226. {
  227. $phraseList[$filePath][$languageId][$key] = $phrase;
  228. }
  229. else
  230. {
  231. $rowErrors[] = Loc::getMessage('TR_IMPORT_ERROR_NO_VALID_UTF8_PHRASE', ['#LANG#' => $languageId]);
  232. }
  233. unset($checked, $phrase);
  234. }
  235. if (!empty($rowErrors))
  236. {
  237. $this->addError(new Main\Error(Loc::getMessage(
  238. 'TR_IMPORT_ERROR_LINE_FILE_BIG',
  239. [
  240. '#LINE#' => ($currentLine + 1),
  241. '#FILENAME#' => $filePath,
  242. '#PHRASE#' => $key,
  243. '#ERROR#' => implode('; ', $rowErrors),
  244. ]
  245. )));
  246. }
  247. unset($rowErrors);
  248. if ($linePortion >= $maxLinePortion)
  249. {
  250. break;
  251. }
  252. }
  253. if ($csvRow === null)
  254. {
  255. $hasFinishedReading = true;
  256. }
  257. unset($csvRow);
  258. $this->processedItems += $linePortion;
  259. foreach ($phraseList as $filePath => $translationList)
  260. {
  261. if (Translate\IO\Path::isLangDir($filePath, true) !== true)
  262. {
  263. $this->addError(new Main\Error(Loc::getMessage('TR_IMPORT_ERROR_FILE_NOT_LANG', array('#FILE#' => $filePath))));
  264. continue;
  265. }
  266. $filePath = Translate\IO\Path::normalize('/'.$filePath);
  267. foreach ($translationList as $languageId => $fileMessages)
  268. {
  269. if (empty($fileMessages))
  270. {
  271. continue;
  272. }
  273. $langFilePath = Translate\IO\Path::replaceLangId($filePath, $languageId);
  274. if (\Rel2Abs('/', $langFilePath) !== $langFilePath)
  275. {
  276. $this->addError(new Main\Error(Loc::getMessage('TR_IMPORT_ERROR_BAD_FILEPATH', ['#FILE#' => $filePath])));
  277. break;
  278. }
  279. $fullPath = self::$documentRoot. $langFilePath;
  280. $fullPath = Main\Localization\Translation::convertLangPath($fullPath, $languageId);
  281. $langFile = new Translate\File($fullPath);
  282. $langFile->setLangId($languageId);
  283. $langFile->setOperatingEncoding(self::$sourceEncoding[$languageId]);
  284. if (!$langFile->loadTokens())
  285. {
  286. if (!$langFile->load() && $langFile->hasErrors())
  287. {
  288. foreach ($langFile->getErrors() as $error)
  289. {
  290. if ($error->getCode() !== 'EMPTY_CONTENT')
  291. {
  292. $this->addError($error);
  293. }
  294. }
  295. }
  296. }
  297. if (count($this->getErrors()) > 0)
  298. {
  299. continue;
  300. }
  301. $hasDataToUpdate = false;
  302. /** @var \ArrayAccess $langFile */
  303. foreach ($fileMessages as $key => $phrase)
  304. {
  305. switch ($this->updateMethod)
  306. {
  307. // import only new messages
  308. case Translate\Controller\Import\Csv::METHOD_ADD_ONLY:
  309. if (!isset($langFile[$key]) || (empty($langFile[$key]) && $langFile[$key] !== '0'))
  310. {
  311. $langFile[$key] = $phrase;
  312. $hasDataToUpdate = true;
  313. $this->importedPhraseCount ++;
  314. }
  315. break;
  316. // update only existing messages
  317. case Translate\Controller\Import\Csv::METHOD_UPDATE_ONLY:
  318. if (isset($langFile[$key]) && $langFile[$key] !== $phrase)
  319. {
  320. $langFile[$key] = $phrase;
  321. $hasDataToUpdate = true;
  322. $this->importedPhraseCount ++;
  323. }
  324. break;
  325. // import new messages and replace all existing with new ones
  326. case Translate\Controller\Import\Csv::METHOD_ADD_UPDATE:
  327. if ($langFile[$key] !== $phrase)
  328. {
  329. $langFile[$key] = $phrase;
  330. $hasDataToUpdate = true;
  331. $this->importedPhraseCount ++;
  332. }
  333. break;
  334. }
  335. }
  336. if ($hasDataToUpdate)
  337. {
  338. // backup
  339. if ($langFile->isExists() && Translate\Config::needToBackUpFiles())
  340. {
  341. if (!$langFile->backup())
  342. {
  343. $this->addError(new Main\Error(
  344. Loc::getMessage('TR_IMPORT_ERROR_CREATE_BACKUP', ['#FILE#' => $langFilePath])
  345. ));
  346. }
  347. }
  348. // sort phrases by key
  349. if (Translate\Config::needToSortPhrases())
  350. {
  351. if (in_array($languageId, Translate\Config::getNonSortPhraseLanguages()) === false)
  352. {
  353. $langFile->sortPhrases();
  354. }
  355. }
  356. try
  357. {
  358. if (!$langFile->save())
  359. {
  360. if ($langFile->hasErrors())
  361. {
  362. $this->addErrors($langFile->getErrors());
  363. }
  364. }
  365. }
  366. catch (Main\IO\IoException $exception)
  367. {
  368. if (!$langFile->isExists())
  369. {
  370. $this->addError(new Main\Error(
  371. Loc::getMessage('TR_IMPORT_ERROR_WRITE_CREATE', ['#FILE#' => $langFilePath])
  372. ));
  373. }
  374. else
  375. {
  376. $this->addError(new Main\Error(
  377. Loc::getMessage('TR_IMPORT_ERROR_WRITE_UPDATE', ['#FILE#' => $langFilePath])
  378. ));
  379. }
  380. }
  381. }
  382. }
  383. }
  384. if ($this->instanceTimer()->hasTimeLimitReached())
  385. {
  386. $this->seekLine = $currentLine;
  387. break;
  388. }
  389. if ($hasFinishedReading)
  390. {
  391. $this->declareAccomplishment();
  392. $this->clearProgressParameters();
  393. break;
  394. }
  395. }
  396. $this->csvFile->close();
  397. if ($this->instanceTimer()->hasTimeLimitReached() !== true)
  398. {
  399. $this->declareAccomplishment();
  400. $this->clearProgressParameters();
  401. }
  402. return array(
  403. 'PROCESSED_ITEMS' => $this->processedItems,
  404. 'TOTAL_ITEMS' => $this->totalItems,
  405. 'TOTAL_PHRASES' => $this->importedPhraseCount,
  406. );
  407. }
  408. /**
  409. * Validates uploaded csv file.
  410. *
  411. * @return boolean
  412. */
  413. private function verifyCsvFile()
  414. {
  415. $testDelimiters = array(
  416. Translate\IO\CsvFile::DELIMITER_TZP,
  417. Translate\IO\CsvFile::DELIMITER_TAB,
  418. Translate\IO\CsvFile::DELIMITER_ZPT,
  419. );
  420. foreach ($testDelimiters as $delimiter)
  421. {
  422. $this->csvFile->setFieldDelimiter($delimiter);
  423. $this->csvFile->moveFirst();
  424. $rowHead = $this->csvFile->fetch();
  425. if (
  426. !is_array($rowHead) ||
  427. empty($rowHead) ||
  428. empty($rowHead[0]) ||
  429. (count($rowHead) < 3)
  430. )
  431. {
  432. continue;
  433. }
  434. break;
  435. }
  436. if (
  437. !is_array($rowHead) ||
  438. empty($rowHead) ||
  439. empty($rowHead[0]) ||
  440. (count($rowHead) < 3)
  441. )
  442. {
  443. $this->addError(new Main\Error(Loc::getMessage('TR_IMPORT_ERR_EMPTY_FIRST_ROW')));
  444. return false;
  445. }
  446. $this->languageList = self::$enabledLanguages;
  447. $this->columnList = array_flip($rowHead);
  448. foreach ($this->languageList as $keyLang => $langID)
  449. {
  450. if (!isset($this->columnList[$langID]))
  451. {
  452. unset($this->languageList[$keyLang]);
  453. }
  454. }
  455. if (!isset($this->columnList['file']))
  456. {
  457. $this->addError(new Main\Error(Loc::getMessage('TR_IMPORT_ERR_DESTINATION_FIELD_ABSENT')));
  458. }
  459. if (!isset($this->columnList['key']))
  460. {
  461. $this->addError(new Main\Error(Loc::getMessage('TR_IMPORT_ERR_PHRASE_CODE_FIELD_ABSENT')));
  462. }
  463. if (empty($this->languageList))
  464. {
  465. $this->addError(new Main\Error(Loc::getMessage('TR_IMPORT_ERR_LANGUAGE_LIST_ABSENT')));
  466. }
  467. return count($this->getErrors()) === 0;
  468. }
  469. }