PageRenderTime 36ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/modules/landing/lib/update/block/nodeattributes.php

https://gitlab.com/alexprowars/bitrix
PHP | 660 lines | 492 code | 84 blank | 84 comment | 69 complexity | 629956265ce24deef695286a355b2b50 MD5 | raw file
  1. <?php
  2. namespace Bitrix\Landing\Update\Block;
  3. use Bitrix\Landing\Manager;
  4. use Bitrix\Landing\Subtype\Form;
  5. use Bitrix\Main\Config\Option;
  6. use Bitrix\Main\Update\Stepper;
  7. use Bitrix\Landing\Block;
  8. use Bitrix\Landing\Internals\BlockTable;
  9. use Bitrix\Main\Entity;
  10. use Bitrix\Main\Localization\Loc;
  11. use Bitrix\Main\Loader;
  12. use Bitrix\Main\Web\DOM\Element;
  13. final class NodeAttributes extends Stepper
  14. {
  15. const CONTINUE_EXECUTING = true;
  16. const STOP_EXECUTING = false;
  17. const OPTION_NAME = 'blocks_attrs_update';
  18. const OPTION_STATUS_NAME = 'blocks_attrs_update_status';
  19. const STEP_PORTION = 1; //count of block CODES to step
  20. protected static $moduleId = 'landing';
  21. protected $dataToUpdate = array();
  22. protected $blocksToUpdate = array();
  23. protected $sitesToUpdate = array();
  24. protected $status = array();
  25. protected $codesToStep = array();
  26. /**
  27. * get progress from option or set default
  28. */
  29. private function loadCurrentStatus()
  30. {
  31. // saved in option
  32. $this->status = Option::get('landing', self::OPTION_STATUS_NAME, '');
  33. $this->status = ($this->status !== '' ? @unserialize($this->status, ['allowed_classes' => false]) : array());
  34. $this->status = (is_array($this->status) ? $this->status : array());
  35. // or default
  36. if (empty($this->status))
  37. {
  38. // get codes from all updaters options
  39. $count = 0;
  40. $params = array();
  41. foreach (Option::getForModule('landing') as $key => $option)
  42. {
  43. if (mb_strpos($key, self::OPTION_NAME) === 0 && $key != self::OPTION_STATUS_NAME)
  44. {
  45. $option = ($option !== '' ? @unserialize($option, ['allowed_classes' => false]) : array());
  46. if(!isset($option['BLOCKS']))
  47. {
  48. Option::delete('landing', array('name' => $key));
  49. continue;
  50. }
  51. // save params
  52. $params[$key] = $option['PARAMS'];
  53. // count of all blocks - to progress-bar
  54. $filter = array(
  55. 'CODE' => array_keys($option['BLOCKS']),
  56. 'DELETED' => 'N',
  57. );
  58. if (
  59. isset($option['PARAMS']['UPDATE_PUBLISHED_SITES']) &&
  60. $option['PARAMS']['UPDATE_PUBLISHED_SITES'] != 'Y'
  61. )
  62. {
  63. $filter['PUBLIC'] = 'N';
  64. }
  65. $res = BlockTable::getList(array(
  66. 'select' => array(
  67. new \Bitrix\Main\Entity\ExpressionField(
  68. 'CNT', 'COUNT(*)'
  69. ),
  70. ),
  71. 'filter' => $filter,
  72. ));
  73. if ($row = $res->fetch())
  74. {
  75. $count += $row['CNT'];
  76. }
  77. }
  78. }
  79. $this->status['COUNT'] = $count;
  80. $this->status['STEPS'] = 0;
  81. $this->status['SITES_TO_UPDATE'] = array();
  82. $this->status['UPDATER_ID'] = '';
  83. $this->status['PARAMS'] = $params;
  84. Option::set('landing', self::OPTION_STATUS_NAME, serialize($this->status));
  85. }
  86. }
  87. /**
  88. * May be several Options for update data. They have uniqueId. Find ID of first option
  89. *
  90. * @return string
  91. */
  92. private function getUpdaterUniqueId()
  93. {
  94. // continue processing current updater
  95. if ($this->status['UPDATER_ID'] !== '')
  96. {
  97. return $this->status['UPDATER_ID'];
  98. }
  99. $updaterOptions = Option::getForModule('landing');
  100. $allOptions = preg_grep('/' . self::OPTION_NAME . '.+/', array_keys($updaterOptions));
  101. $allOptions = array_diff($allOptions, array(self::OPTION_STATUS_NAME)); // remove status option from list
  102. sort($allOptions);
  103. if (!empty($allOptions))
  104. {
  105. return str_replace(self::OPTION_NAME, '', $allOptions[0]);
  106. }
  107. else
  108. {
  109. return '';
  110. }
  111. }
  112. public function execute(array &$result)
  113. {
  114. // nothing to update
  115. $this->loadCurrentStatus();
  116. if (!$this->status['COUNT'])
  117. {
  118. self::finish();
  119. return self::STOP_EXECUTING;
  120. }
  121. // find option. If nothing - we update all
  122. $this->status['UPDATER_ID'] = $this->getUpdaterUniqueId();
  123. if (!$this->status['UPDATER_ID'])
  124. {
  125. self::finish();
  126. return self::STOP_EXECUTING;
  127. }
  128. $this->processBlocks();
  129. // was processing all data for current option
  130. if (!is_array($this->dataToUpdate['BLOCKS']) || empty($this->dataToUpdate['BLOCKS']))
  131. {
  132. $this->finishOption();
  133. }
  134. $result['count'] = $this->status['COUNT'];
  135. $result['steps'] = $this->status['STEPS'];
  136. return self::CONTINUE_EXECUTING;
  137. }
  138. /**
  139. * Additional operations before stop executing
  140. */
  141. private static function finish()
  142. {
  143. self::clearOptions();
  144. self::removeCustomEvents();
  145. }
  146. /**
  147. * If no more blocks to update - remove all data options and status
  148. *
  149. * @throws \Bitrix\Main\ArgumentNullException
  150. */
  151. private static function clearOptions()
  152. {
  153. foreach (Option::getForModule('landing') as $key => $option)
  154. {
  155. if (mb_strpos($key, self::OPTION_NAME) === 0)
  156. {
  157. Option::delete('landing', array('name' => $key));
  158. }
  159. }
  160. }
  161. /**
  162. * Create option name by base name and unique ID
  163. * @return string
  164. */
  165. private function getOptionName()
  166. {
  167. return self::OPTION_NAME . $this->status['UPDATER_ID'];
  168. }
  169. private function collectBlocks()
  170. {
  171. $this->dataToUpdate = Option::get(self::$moduleId, $this->getOptionName());
  172. $this->dataToUpdate = ($this->dataToUpdate !== '' ? @unserialize($this->dataToUpdate, ['allowed_classes' => false]) : array());
  173. $this->codesToStep = array_unique(array_keys($this->dataToUpdate['BLOCKS']));
  174. $this->codesToStep = array_slice($this->codesToStep, 0, self::STEP_PORTION);
  175. // load BLOCKS
  176. $filter = array(
  177. 'CODE' => $this->codesToStep,
  178. 'DELETED' => 'N',
  179. );
  180. if (
  181. isset($this->status['PARAMS'][$this->getOptionName()]['UPDATE_PUBLISHED_SITES']) &&
  182. $this->status['PARAMS'][$this->getOptionName()]['UPDATE_PUBLISHED_SITES'] != 'Y'
  183. )
  184. {
  185. $filter['PUBLIC'] = 'N';
  186. }
  187. $resBlock = BlockTable::getList(array(
  188. 'filter' => $filter,
  189. 'select' => array(
  190. 'ID',
  191. 'SORT',
  192. 'CODE',
  193. 'ACTIVE',
  194. 'PUBLIC',
  195. 'DELETED',
  196. 'CONTENT',
  197. 'LID',
  198. 'SITE_ID' => 'LANDING.SITE_ID',
  199. ),
  200. 'order' => array(
  201. 'CODE' => 'ASC',
  202. 'ID' => 'ASC',
  203. ),
  204. ));
  205. while ($row = $resBlock->fetch())
  206. {
  207. $this->blocksToUpdate[$row['CODE']][$row['ID']] = new Block($row['ID'], $row);
  208. if (count($this->blocksToUpdate) > self::STEP_PORTION)
  209. {
  210. unset($this->blocksToUpdate[$row['CODE']]);
  211. break;
  212. }
  213. // save sites ID for current blocks to reset cache later
  214. $this->sitesToUpdate[$row['ID']] = $row['SITE_ID'];
  215. }
  216. }
  217. private function processBlocks()
  218. {
  219. $this->collectBlocks();
  220. foreach ($this->blocksToUpdate as $code => $blocks)
  221. {
  222. foreach ($blocks as $block)
  223. {
  224. if (is_array($this->dataToUpdate['BLOCKS'][$code]) && !empty($this->dataToUpdate['BLOCKS'][$code]))
  225. {
  226. $this->updateBlock($block);
  227. // after processing block save site ID to update cache later (only if update needed)
  228. if (
  229. isset($this->status['PARAMS'][$this->getOptionName()]['UPDATE_PUBLISHED_SITES']) &&
  230. $this->status['PARAMS'][$this->getOptionName()]['UPDATE_PUBLISHED_SITES'] == 'Y'
  231. )
  232. {
  233. $this->status['SITES_TO_UPDATE'][] = $this->sitesToUpdate[$block->getId()];
  234. }
  235. }
  236. $this->status['STEPS']++;
  237. }
  238. }
  239. $this->finishStep();
  240. }
  241. private function updateBlock(Block $block)
  242. {
  243. $code = $block->getCode();
  244. $doc = $block->getDom();
  245. foreach ($this->dataToUpdate['BLOCKS'][$code]['NODES'] as $selector => $rules)
  246. {
  247. $resultList = $doc->querySelectorAll($selector);
  248. // prepare ATTRS
  249. $nodeAttrs = array();
  250. if (is_array($rules['ATTRS_ADD']) && !empty($rules['ATTRS_ADD']))
  251. {
  252. $nodeAttrs = array_merge($nodeAttrs, $rules['ATTRS_ADD']);
  253. }
  254. if (is_array($rules['ATTRS_REMOVE']) && !empty($rules['ATTRS_REMOVE']))
  255. {
  256. $nodeAttrs = array_merge($nodeAttrs, array_fill_keys(array_values($rules['ATTRS_REMOVE']), ''));
  257. }
  258. // PROCESS
  259. foreach ($resultList as $nth => $resultNode)
  260. {
  261. // FILTER
  262. // use until cant add some filters in DOM\Parser
  263. if (is_array($rules['FILTER']) && !empty($rules['FILTER']))
  264. {
  265. $notFilterd = false;
  266. // By content. May have 'NOT' key
  267. if (
  268. isset($rules['FILTER']['CONTENT']) && is_array($rules['FILTER']['CONTENT']) &&
  269. (
  270. $rules['FILTER']['CONTENT']['VALUE'] != $resultNode->getInnerHTML() ||
  271. (
  272. $rules['FILTER']['CONTENT']['NOT'] &&
  273. $rules['FILTER']['CONTENT']['VALUE'] == $resultNode->getInnerHTML()
  274. )
  275. )
  276. )
  277. {
  278. $notFilterd = true;
  279. }
  280. // by position in DOM
  281. if (
  282. isset($rules['FILTER']['NTH']) && is_array($rules['FILTER']['NTH']) &&
  283. isset($rules['FILTER']['NTH']['VALUE']) &&
  284. $nth + 1 != $rules['FILTER']['NTH']['VALUE']
  285. )
  286. {
  287. $notFilterd = true;
  288. }
  289. if ($notFilterd)
  290. {
  291. continue;
  292. }
  293. }
  294. // CLASSES
  295. $classesChange = false;
  296. $nodeClasses = $resultNode->getClassList();
  297. if (is_array($rules['CLASSES_REMOVE']) && !empty($rules['CLASSES_REMOVE']))
  298. {
  299. $nodeClasses = array_diff($nodeClasses, $rules['CLASSES_REMOVE']);
  300. $classesChange = true;
  301. }
  302. if (is_array($rules['CLASSES_ADD']) && !empty($rules['CLASSES_ADD']))
  303. {
  304. $nodeClasses = array_merge($nodeClasses, $rules['CLASSES_ADD']);
  305. $classesChange = true;
  306. }
  307. if (is_array($rules['CLASSES_REPLACE']) &&
  308. array_key_exists('PATTERN', $rules['CLASSES_REPLACE']) &&
  309. array_key_exists('REPLACE', $rules['CLASSES_REPLACE']))
  310. {
  311. $nodeClassesStr = implode(' ', $nodeClasses);
  312. $nodeClassesReplace = preg_replace(
  313. '/' . $rules['CLASSES_REPLACE']['PATTERN'] . '/i',
  314. $rules['CLASSES_REPLACE']['REPLACE'],
  315. $nodeClassesStr
  316. );
  317. if ($nodeClassesReplace !== null)
  318. {
  319. $nodeClasses = explode(' ', $nodeClassesReplace);
  320. $classesChange = true;
  321. }
  322. }
  323. // APPLY changes
  324. $nodeClasses = array_unique($nodeClasses);
  325. if ($classesChange)
  326. {
  327. $resultNode->setClassName(implode(' ', $nodeClasses));
  328. }
  329. // ID
  330. if ($rules['ID_REMOVE'] && $rules['ID_REMOVE'] == 'Y')
  331. {
  332. $resultNode->removeAttribute('id');
  333. }
  334. // ATTRS
  335. foreach ($nodeAttrs as $name => $value)
  336. {
  337. // reduce string (in attributes may be a complex data)
  338. $value = str_replace(array("\n", "\t"), "", $value);
  339. if ($value)
  340. {
  341. $resultNode->setAttribute($name, is_array($value) ? json_encode($value) : $value);
  342. }
  343. else
  344. {
  345. $resultNode->removeAttribute($name);
  346. }
  347. }
  348. // REMOVE NODE
  349. if (isset($rules['NODE_REMOVE']) && $rules['NODE_REMOVE'] === true)
  350. {
  351. $resultNode->getParentNode()->removeChild($resultNode);
  352. }
  353. // REPLACE CONTENT by regexp
  354. // be CAREFUL!
  355. if (
  356. isset($rules['REPLACE_CONTENT']) && is_array($rules['REPLACE_CONTENT']) &&
  357. array_key_exists('regexp', $rules['REPLACE_CONTENT']) &&
  358. array_key_exists('replace', $rules['REPLACE_CONTENT'])
  359. )
  360. {
  361. $innerHtml = $resultNode->getInnerHTML();
  362. $innerHtml = preg_replace($rules['REPLACE_CONTENT']['regexp'], $rules['REPLACE_CONTENT']['replace'], $innerHtml);
  363. if($innerHtml <> '')
  364. {
  365. $resultNode->setInnerHTML($innerHtml);
  366. }
  367. }
  368. }
  369. // add CONTAINER around nodes.
  370. if (
  371. isset($rules['CONTAINER_ADD']) && is_array($rules['CONTAINER_ADD']) &&
  372. isset($rules['CONTAINER_ADD']['CLASSES']) &&
  373. !empty($resultList)
  374. )
  375. {
  376. if (!is_array($rules['CONTAINER_ADD']['CLASSES']))
  377. {
  378. $rules['CONTAINER_ADD']['CLASSES'] = [$rules['CONTAINER_ADD']['CLASSES']];
  379. }
  380. // check if container exist
  381. $firstNode = $resultList[0];
  382. $parentNode = $firstNode->getParentNode();
  383. $parentClasses = $parentNode->getClassList();
  384. if (!empty(array_diff($rules['CONTAINER_ADD']['CLASSES'], $parentClasses)))
  385. {
  386. // param TO_EACH - add container to each element. Default (false) - add container once to all nodes
  387. if (!isset($rules['CONTAINER_ADD']['TO_EACH']) || $rules['CONTAINER_ADD']['TO_EACH'] !== true)
  388. {
  389. $containerNode = new Element($rules['CONTAINER_ADD']['TAG'] ? $rules['CONTAINER_ADD']['TAG'] : 'div');
  390. $containerNode->setOwnerDocument($doc);
  391. $containerNode->setClassName(implode(' ', $rules['CONTAINER_ADD']['CLASSES']));
  392. $parentNode->insertBefore($containerNode, $firstNode);
  393. foreach ($resultList as $resultNode)
  394. {
  395. $parentNode->removeChild($resultNode);
  396. $containerNode->appendChild($resultNode);
  397. }
  398. }
  399. else
  400. {
  401. foreach ($resultList as $resultNode)
  402. {
  403. $containerNode = new Element($rules['CONTAINER_ADD']['TAG'] ? $rules['CONTAINER_ADD']['TAG'] : 'div');
  404. $containerNode->setOwnerDocument($doc);
  405. $containerNode->setClassName(implode(' ', $rules['CONTAINER_ADD']['CLASSES']));
  406. $parentNode->insertBefore($containerNode, $resultNode);
  407. $parentNode->removeChild($resultNode);
  408. $containerNode->appendChild($resultNode);
  409. }
  410. }
  411. }
  412. }
  413. }
  414. $block->saveContent($doc->saveHTML());
  415. // updates COMPONENTS params
  416. if (is_array($this->dataToUpdate['BLOCKS'][$code]['UPDATE_COMPONENTS']))
  417. {
  418. foreach ($this->dataToUpdate['BLOCKS'][$code]['UPDATE_COMPONENTS'] as $selector => $params)
  419. {
  420. $block->updateNodes(array($selector => $params));
  421. }
  422. }
  423. // if need remove PHP - we must use block content directly, not DOM parser
  424. if (
  425. $this->dataToUpdate['BLOCKS'][$code]['CLEAR_PHP'] &&
  426. $this->dataToUpdate['BLOCKS'][$code]['CLEAR_PHP'] == 'Y'
  427. )
  428. {
  429. $content = $block->getContent();
  430. $content = preg_replace('/<\?.*\?>/s', '', $content);
  431. $block->saveContent($content);
  432. }
  433. // change block SORT
  434. if (
  435. $this->dataToUpdate['BLOCKS'][$code]['SET_SORT'] &&
  436. is_numeric($this->dataToUpdate['BLOCKS'][$code]['SET_SORT'])
  437. )
  438. {
  439. $block->setSort($this->dataToUpdate['BLOCKS'][$code]['SET_SORT']);
  440. }
  441. $block->save();
  442. }
  443. private function finishStep()
  444. {
  445. // processed blocks must be removed from data
  446. foreach ($this->codesToStep as $code)
  447. {
  448. unset($this->dataToUpdate['BLOCKS'][$code]);
  449. }
  450. Option::set('landing', $this->getOptionName(), serialize($this->dataToUpdate));
  451. Option::set('landing', self::OPTION_STATUS_NAME, serialize($this->status));
  452. }
  453. private function finishOption()
  454. {
  455. // clean cloud sites cache only if needed
  456. $this->updateSites();
  457. // finish current updater id, try next
  458. Option::delete('landing', array('name' => $this->getOptionName()));
  459. $this->status['SITES_TO_UPDATE'] = array();
  460. $this->status['UPDATER_ID'] = '';
  461. Option::set('landing', self::OPTION_STATUS_NAME, serialize($this->status));
  462. }
  463. private function updateSites()
  464. {
  465. if (
  466. isset($this->status['PARAMS'][$this->getOptionName()]['UPDATE_PUBLISHED_SITES']) &&
  467. $this->status['PARAMS'][$this->getOptionName()]['UPDATE_PUBLISHED_SITES'] == 'Y' &&
  468. Loader::includeModule('bitrix24')
  469. && false
  470. // dbg: need this?
  471. )
  472. {
  473. foreach (array_unique($this->status['SITES_TO_UPDATE']) as $siteId)
  474. {
  475. if (intval($siteId))
  476. {
  477. // Site::update($siteId, array());
  478. }
  479. }
  480. }
  481. }
  482. /**
  483. * Before delete block handler.
  484. * @param Entity\Event $event Event instance.
  485. * @return Entity\EventResult
  486. */
  487. public static function disableBlockDelete(Entity\Event $event)
  488. {
  489. if (\Bitrix\Landing\Update\Stepper::checkAgentActivity('\Bitrix\Landing\Update\Block\NodeAttributes'))
  490. {
  491. $result = new Entity\EventResult();
  492. $result->setErrors(array(
  493. new Entity\EntityError(
  494. Loc::getMessage('LANDING_BLOCK_DISABLE_DELETE'),
  495. 'BLOCK_DISABLE_DELETE'
  496. ),
  497. ));
  498. return $result;
  499. }
  500. else
  501. {
  502. self::removeCustomEvents();
  503. }
  504. }
  505. /**
  506. * Before publication landing handler.
  507. * @param \Bitrix\Main\Event $event Event instance.
  508. * @return Entity\EventResult
  509. */
  510. public static function disablePublication(\Bitrix\Main\Event $event)
  511. {
  512. if (\Bitrix\Landing\Update\Stepper::checkAgentActivity('\Bitrix\Landing\Update\Block\NodeAttributes'))
  513. {
  514. $result = new Entity\EventResult;
  515. $result->setErrors(array(
  516. new \Bitrix\Main\Entity\EntityError(
  517. Loc::getMessage('LANDING_DISABLE_PUBLICATION'),
  518. 'LANDING_DISABLE_PUBLICATION'
  519. ),
  520. ));
  521. return $result;
  522. }
  523. else
  524. {
  525. self::removeCustomEvents();
  526. }
  527. }
  528. /**
  529. * If agent not exist - we must broke events, to preserve infinity blocking publication and delete
  530. */
  531. public static function removeCustomEvents()
  532. {
  533. $eventManager = \Bitrix\Main\EventManager::getInstance();
  534. $eventManager->unregisterEventHandler(
  535. 'landing',
  536. '\Bitrix\Landing\Internals\Block::OnBeforeDelete',
  537. 'landing',
  538. '\Bitrix\Landing\Update\Block\NodeAttributes',
  539. 'disableBlockDelete');
  540. $eventManager->unregisterEventHandler(
  541. 'landing',
  542. 'onLandingPublication',
  543. 'landing',
  544. '\Bitrix\Landing\Update\Block\NodeAttributes',
  545. 'disablePublication'
  546. );
  547. }
  548. /**
  549. * Update form domain, when updated b24 connector
  550. * @param Event $event
  551. * @deprecated
  552. */
  553. public static function updateFormDomainByConnector($event)
  554. {
  555. trigger_error(
  556. "Now using embedded forms, no need domain. You must remove updateFormDomainByConnector() call",
  557. E_USER_WARNING
  558. );
  559. }
  560. /**
  561. * Set data for NodeUpdater to updating form domain
  562. *
  563. * @param array $domains
  564. * @deprecated
  565. */
  566. public static function updateFormDomain($domains = array())
  567. {
  568. trigger_error(
  569. "Now using embedded forms, no need domain. You must remove updateFormDomain() call",
  570. E_USER_WARNING
  571. );
  572. }
  573. /**
  574. * Code for updater.php see in landing/dev/updater/nodeattributesupdaters.pph
  575. */
  576. }