vendor/pimcore/pimcore/models/Search/Backend/Data.php line 485

Open in your IDE?
  1. <?php
  2. /**
  3. * Pimcore
  4. *
  5. * This source file is available under two different licenses:
  6. * - GNU General Public License version 3 (GPLv3)
  7. * - Pimcore Commercial License (PCL)
  8. * Full copyright and license information is available in
  9. * LICENSE.md which is distributed with this source code.
  10. *
  11. * @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
  12. * @license http://www.pimcore.org/license GPLv3 and PCL
  13. */
  14. namespace Pimcore\Model\Search\Backend;
  15. use ForceUTF8\Encoding;
  16. use Pimcore\Event\Model\SearchBackendEvent;
  17. use Pimcore\Event\SearchBackendEvents;
  18. use Pimcore\Loader\ImplementationLoader\Exception\UnsupportedException;
  19. use Pimcore\Logger;
  20. use Pimcore\Model\Asset;
  21. use Pimcore\Model\DataObject;
  22. use Pimcore\Model\Document;
  23. use Pimcore\Model\Element;
  24. use Pimcore\Model\Search\Backend\Data\Dao;
  25. /**
  26. * @internal
  27. *
  28. * @method Dao getDao()
  29. */
  30. class Data extends \Pimcore\Model\AbstractModel
  31. {
  32. // if a word occures more often than this number it will get stripped to keep the search_backend_data table from getting too big
  33. const MAX_WORD_OCCURENCES = 3;
  34. /**
  35. * @var Data\Id|null
  36. */
  37. protected ?Data\Id $id = null;
  38. /**
  39. * @var string
  40. */
  41. protected $fullPath;
  42. /**
  43. * document | object | asset
  44. *
  45. * @var string
  46. */
  47. protected $maintype;
  48. /**
  49. * webresource type (e.g. page, snippet ...)
  50. *
  51. * @var string
  52. */
  53. protected $type;
  54. /**
  55. * currently only relevant for objects where it portrays the class name
  56. *
  57. * @var string
  58. */
  59. protected $subtype;
  60. /**
  61. * published or not
  62. *
  63. * @var bool
  64. */
  65. protected $published;
  66. /**
  67. * timestamp of creation date
  68. *
  69. * @var int
  70. */
  71. protected $creationDate;
  72. /**
  73. * timestamp of modification date
  74. *
  75. * @var int
  76. */
  77. protected $modificationDate;
  78. /**
  79. * User-ID of the owner
  80. *
  81. * @var int
  82. */
  83. protected $userOwner;
  84. /**
  85. * User-ID of the user last modified the element
  86. *
  87. * @var int
  88. */
  89. protected $userModification;
  90. /**
  91. * @var string|null
  92. */
  93. protected $data;
  94. /**
  95. * @var string
  96. */
  97. protected $properties;
  98. /**
  99. * @param Element\ElementInterface $element
  100. */
  101. public function __construct($element = null)
  102. {
  103. if ($element instanceof Element\ElementInterface) {
  104. $this->setDataFromElement($element);
  105. }
  106. }
  107. /**
  108. * @return Data\Id|null
  109. */
  110. public function getId(): ?Data\Id
  111. {
  112. return $this->id;
  113. }
  114. /**
  115. * @param Data\Id|null $id
  116. *
  117. * @return $this
  118. */
  119. public function setId(?Data\Id $id)
  120. {
  121. $this->id = $id;
  122. return $this;
  123. }
  124. /**
  125. * @return string
  126. */
  127. public function getFullPath()
  128. {
  129. return $this->fullPath;
  130. }
  131. /**
  132. * @param string $fullpath
  133. *
  134. * @return $this
  135. */
  136. public function setFullPath($fullpath)
  137. {
  138. $this->fullPath = $fullpath;
  139. return $this;
  140. }
  141. /**
  142. * @return string
  143. */
  144. public function getType()
  145. {
  146. return $this->type;
  147. }
  148. /**
  149. * @param string $type
  150. *
  151. * @return $this
  152. */
  153. public function setType($type)
  154. {
  155. $this->type = $type;
  156. return $this;
  157. }
  158. /**
  159. * @return string
  160. */
  161. public function getSubtype()
  162. {
  163. return $this->subtype;
  164. }
  165. /**
  166. * @param string $subtype
  167. *
  168. * @return $this
  169. */
  170. public function setSubtype($subtype)
  171. {
  172. $this->subtype = $subtype;
  173. return $this;
  174. }
  175. /**
  176. * @return int
  177. */
  178. public function getCreationDate()
  179. {
  180. return $this->creationDate;
  181. }
  182. /**
  183. * @param int $creationDate
  184. *
  185. * @return $this
  186. */
  187. public function setCreationDate($creationDate)
  188. {
  189. $this->creationDate = $creationDate;
  190. return $this;
  191. }
  192. /**
  193. * @return int
  194. */
  195. public function getModificationDate()
  196. {
  197. return $this->modificationDate;
  198. }
  199. /**
  200. * @param int $modificationDate
  201. *
  202. * @return $this
  203. */
  204. public function setModificationDate($modificationDate)
  205. {
  206. $this->modificationDate = $modificationDate;
  207. return $this;
  208. }
  209. /**
  210. * @return int
  211. */
  212. public function getUserModification()
  213. {
  214. return $this->userModification;
  215. }
  216. /**
  217. * @param int $userModification
  218. *
  219. * @return $this
  220. */
  221. public function setUserModification($userModification)
  222. {
  223. $this->userModification = $userModification;
  224. return $this;
  225. }
  226. /**
  227. * @return int
  228. */
  229. public function getUserOwner()
  230. {
  231. return $this->userOwner;
  232. }
  233. /**
  234. * @param int $userOwner
  235. *
  236. * @return $this
  237. */
  238. public function setUserOwner($userOwner)
  239. {
  240. $this->userOwner = $userOwner;
  241. return $this;
  242. }
  243. /**
  244. * @return bool
  245. */
  246. public function isPublished()
  247. {
  248. return (bool) $this->getPublished();
  249. }
  250. /**
  251. * @return bool
  252. */
  253. public function getPublished()
  254. {
  255. return (bool) $this->published;
  256. }
  257. /**
  258. * @param bool $published
  259. *
  260. * @return $this
  261. */
  262. public function setPublished($published)
  263. {
  264. $this->published = (bool) $published;
  265. return $this;
  266. }
  267. /**
  268. * @return string
  269. */
  270. public function getData()
  271. {
  272. return $this->data;
  273. }
  274. /**
  275. * @param string $data
  276. *
  277. * @return $this
  278. */
  279. public function setData($data)
  280. {
  281. $this->data = $data;
  282. return $this;
  283. }
  284. /**
  285. * @return string
  286. */
  287. public function getProperties()
  288. {
  289. return $this->properties;
  290. }
  291. /**
  292. * @param string $properties
  293. *
  294. * @return $this
  295. */
  296. public function setProperties($properties)
  297. {
  298. $this->properties = $properties;
  299. return $this;
  300. }
  301. /**
  302. * @param Element\ElementInterface $element
  303. *
  304. * @return $this
  305. */
  306. public function setDataFromElement(Element\ElementInterface $element)
  307. {
  308. $this->data = null;
  309. $this->id = new Data\Id($element);
  310. $this->fullPath = $element->getRealFullPath();
  311. $this->creationDate = $element->getCreationDate();
  312. $this->modificationDate = $element->getModificationDate();
  313. $this->userModification = $element->getUserModification();
  314. $this->userOwner = $element->getUserOwner();
  315. $this->type = $element->getType();
  316. if ($element instanceof DataObject\Concrete) {
  317. $this->subtype = $element->getClassName();
  318. } else {
  319. $this->subtype = $this->type;
  320. }
  321. $this->properties = '';
  322. $properties = $element->getProperties();
  323. if (is_array($properties)) {
  324. foreach ($properties as $nextProperty) {
  325. $pData = (string) $nextProperty->getData();
  326. if ($nextProperty->getName() === 'bool') {
  327. $pData = $pData ? 'true' : 'false';
  328. }
  329. $this->properties .= $nextProperty->getName() . ':' . $pData .' ';
  330. }
  331. }
  332. $this->data = $element->getKey();
  333. if ($element instanceof Document) {
  334. if ($element instanceof Document\Folder) {
  335. $this->published = true;
  336. } elseif ($element instanceof Document\Link) {
  337. $this->published = $element->isPublished();
  338. $this->data = ' ' . $element->getHref();
  339. } elseif ($element instanceof Document\PageSnippet) {
  340. $this->published = $element->isPublished();
  341. $editables = $element->getEditables();
  342. if (is_array($editables) && !empty($editables)) {
  343. foreach ($editables as $editable) {
  344. if ($editable instanceof Document\Editable\EditableInterface) {
  345. // areabrick elements are handled by getElementTypes()/getElements() as they return area elements as well
  346. if ($editable instanceof Document\Editable\Area || $editable instanceof Document\Editable\Areablock) {
  347. continue;
  348. }
  349. ob_start();
  350. $this->data .= strip_tags($editable->frontend()).' ';
  351. $this->data .= ob_get_clean();
  352. }
  353. }
  354. }
  355. if ($element instanceof Document\Page) {
  356. $this->published = $element->isPublished();
  357. $this->data .= ' '.$element->getTitle().' '.$element->getDescription().' ' . $element->getPrettyUrl();
  358. }
  359. }
  360. } elseif ($element instanceof Asset) {
  361. $elementMetadata = $element->getMetadata();
  362. if (is_array($elementMetadata)) {
  363. foreach ($elementMetadata as $md) {
  364. try {
  365. $loader = \Pimcore::getContainer()->get('pimcore.implementation_loader.asset.metadata.data');
  366. /** @var \Pimcore\Model\Asset\MetaData\ClassDefinition\Data\Data $instance */
  367. $instance = $loader->build($md['type']);
  368. $dataForSearchIndex = $instance->getDataForSearchIndex($md['data'], $md);
  369. if ($dataForSearchIndex) {
  370. $this->data .= ' ' . $dataForSearchIndex;
  371. }
  372. } catch (UnsupportedException $e) {
  373. Logger::error('asset metadata type ' . $md['type'] . ' could not be resolved');
  374. }
  375. }
  376. }
  377. if ($element instanceof Asset\Document && \Pimcore\Document::isAvailable()) {
  378. if (\Pimcore\Document::isFileTypeSupported($element->getFilename())) {
  379. try {
  380. $contentText = $element->getText();
  381. if ($contentText) {
  382. $contentText = Encoding::toUTF8($contentText);
  383. $contentText = str_replace(["\r\n", "\r", "\n", "\t", "\f"], ' ', $contentText);
  384. $contentText = preg_replace('/[ ]+/', ' ', $contentText);
  385. $this->data .= ' ' . $contentText;
  386. }
  387. } catch (\Exception $e) {
  388. Logger::error($e);
  389. }
  390. }
  391. } elseif ($element instanceof Asset\Text) {
  392. try {
  393. if ($element->getFileSize() < 2000000) {
  394. // it doesn't make sense to add text files bigger than 2MB to the full text index (performance)
  395. $contentText = $element->getData();
  396. $contentText = Encoding::toUTF8($contentText);
  397. $this->data .= ' ' . $contentText;
  398. }
  399. } catch (\Exception $e) {
  400. Logger::error($e);
  401. }
  402. } elseif ($element instanceof Asset\Image) {
  403. try {
  404. $metaData = array_merge($element->getEXIFData(), $element->getIPTCData());
  405. foreach ($metaData as $key => $value) {
  406. if (is_array($value)) {
  407. $this->data .= ' ' . $key . ' : ' . implode(' - ', $value);
  408. } else {
  409. $this->data .= ' ' . $key . ' : ' . $value;
  410. }
  411. }
  412. } catch (\Exception $e) {
  413. Logger::error($e);
  414. }
  415. }
  416. $this->published = true;
  417. } elseif ($element instanceof DataObject\AbstractObject) {
  418. if ($element instanceof DataObject\Concrete) {
  419. $getInheritedValues = DataObject::doGetInheritedValues();
  420. DataObject::setGetInheritedValues(true);
  421. $this->published = $element->isPublished();
  422. foreach ($element->getClass()->getFieldDefinitions() as $key => $value) {
  423. $this->data .= ' ' . $value->getDataForSearchIndex($element);
  424. }
  425. DataObject::setGetInheritedValues($getInheritedValues);
  426. } elseif ($element instanceof DataObject\Folder) {
  427. $this->published = true;
  428. }
  429. } else {
  430. Logger::crit('Search\\Backend\\Data received an unknown element!');
  431. }
  432. // replace all occurrences of @ to # because when using InnoDB @ is reserved for the @distance operator
  433. $this->data = str_replace('@', '#', $this->data);
  434. $pathWords = str_replace([ '-', '_', '/', '.', '(', ')'], ' ', $this->getFullPath());
  435. $this->data .= ' ' . $pathWords;
  436. $this->data = 'ID: ' . $element->getId() . " \nPath: " . $this->getFullPath() . " \n" . $this->cleanupData($this->data);
  437. return $this;
  438. }
  439. /**
  440. * @param string $data
  441. *
  442. * @return string
  443. */
  444. protected function cleanupData($data)
  445. {
  446. $data = preg_replace('/(<\?.*?(\?>|$)|<[^<]+>)/s', '', $data);
  447. $data = html_entity_decode($data, ENT_QUOTES, 'UTF-8');
  448. // we don't remove ".", otherwise it would be impossible to search for email addresses
  449. $data = str_replace([',', ':', ';', "'", '"'], ' ', $data);
  450. $data = str_replace(["\r\n", "\n", "\r", "\t"], ' ', $data);
  451. $data = preg_replace('#[ ]+#', ' ', $data);
  452. $minWordLength = $this->getDao()->getMinWordLengthForFulltextIndex();
  453. $maxWordLength = $this->getDao()->getMaxWordLengthForFulltextIndex();
  454. $words = explode(' ', $data);
  455. $wordOccurrences = [];
  456. foreach ($words as $key => $word) {
  457. $wordLength = \mb_strlen($word);
  458. if ($wordLength < $minWordLength || $wordLength > $maxWordLength) {
  459. unset($words[$key]);
  460. continue;
  461. }
  462. $wordOccurrences[$word] = ($wordOccurrences[$word] ?? 0) + 1;
  463. if ($wordOccurrences[$word] > self::MAX_WORD_OCCURENCES) {
  464. unset($words[$key]);
  465. }
  466. }
  467. $data = implode(' ', $words);
  468. return $data;
  469. }
  470. /**
  471. * @param Element\ElementInterface $element
  472. *
  473. * @return self
  474. */
  475. public static function getForElement(Element\ElementInterface $element): self
  476. {
  477. $data = new self();
  478. $data->getDao()->getForElement($element);
  479. return $data;
  480. }
  481. public function delete()
  482. {
  483. $this->getDao()->delete();
  484. }
  485. /**
  486. * @throws \Exception
  487. */
  488. public function save()
  489. {
  490. if ($this->id instanceof Data\Id) {
  491. \Pimcore::getEventDispatcher()->dispatch(new SearchBackendEvent($this), SearchBackendEvents::PRE_SAVE);
  492. $maxRetries = 5;
  493. for ($retries = 0; $retries < $maxRetries; $retries++) {
  494. try {
  495. $this->getDao()->save();
  496. // successfully completed, so we cancel the loop here -> no restart required
  497. break;
  498. } catch (\Exception $e) {
  499. // we try to start saving $maxRetries times again (deadlocks, ...)
  500. if ($retries < ($maxRetries - 1)) {
  501. $run = $retries + 1;
  502. $waitTime = rand(1, 5) * 100000;
  503. Logger::warn('Unable to finish transaction (' . $run . ". run) because of the following reason '" . $e->getMessage() . "'. --> Retrying in " . $waitTime . ' microseconds ... (' . ($run + 1) . ' of ' . $maxRetries . ')');
  504. // wait specified time until we restart
  505. usleep($waitTime);
  506. } else {
  507. // if we fail after $maxRetries retries, we throw out the exception
  508. throw $e;
  509. }
  510. }
  511. }
  512. \Pimcore::getEventDispatcher()->dispatch(new SearchBackendEvent($this), SearchBackendEvents::POST_SAVE);
  513. } else {
  514. throw new \Exception('Search\\Backend\\Data cannot be saved - no id set!');
  515. }
  516. }
  517. }