Magento 2 Documentation  2.3
Documentation for Magento 2 CMS v2.3 (December 2018)
Parser.php
Go to the documentation of this file.
1 <?php
8 
15 class Parser implements \Magento\Framework\Translate\Inline\ParserInterface
16 {
20  const DATA_TRANSLATE = 'data-translate';
21 
27  protected $_content;
28 
34  protected $_isJson = false;
35 
41  protected $_maxTranslateBlocks = 7;
42 
48  protected $_allowedTagsGlobal = ['script' => 'String in Javascript', 'title' => 'Page title'];
49 
55  protected $_allowedTagsSimple = [
56  'legend' => 'Caption for the fieldset element',
57  'label' => 'Label for an input element.',
58  'button' => 'Push button',
59  'a' => 'Link label',
60  'b' => 'Bold text',
61  'strong' => 'Strong emphasized text',
62  'i' => 'Italic text',
63  'em' => 'Emphasized text',
64  'u' => 'Underlined text',
65  'sup' => 'Superscript text',
66  'sub' => 'Subscript text',
67  'span' => 'Span element',
68  'small' => 'Smaller text',
69  'big' => 'Bigger text',
70  'address' => 'Contact information',
71  'blockquote' => 'Long quotation',
72  'q' => 'Short quotation',
73  'cite' => 'Citation',
74  'caption' => 'Table caption',
75  'abbr' => 'Abbreviated phrase',
76  'acronym' => 'An acronym',
77  'var' => 'Variable part of a text',
78  'dfn' => 'Term',
79  'strike' => 'Strikethrough text',
80  'del' => 'Deleted text',
81  'ins' => 'Inserted text',
82  'h1' => 'Heading level 1',
83  'h2' => 'Heading level 2',
84  'h3' => 'Heading level 3',
85  'h4' => 'Heading level 4',
86  'h5' => 'Heading level 5',
87  'h6' => 'Heading level 6',
88  'center' => 'Centered text',
89  'select' => 'List options',
90  'img' => 'Image',
91  'input' => 'Form element',
92  ];
93 
97  protected $_resourceFactory;
98 
102  protected $_storeManager;
103 
107  protected $_inputFilter;
108 
112  protected $_appState;
113 
117  protected $_translateInline;
118 
122  protected $_appCache;
123 
127  private $cacheManager;
128 
132  private $relatedCacheTypes;
133 
139  private function getCacheManger()
140  {
141  if (!$this->cacheManager instanceof \Magento\Translation\Model\Inline\CacheManager) {
142  $this->cacheManager = \Magento\Framework\App\ObjectManager::getInstance()->get(
143  \Magento\Translation\Model\Inline\CacheManager::class
144  );
145  }
146  return $this->cacheManager;
147  }
148 
160  public function __construct(
161  \Magento\Translation\Model\ResourceModel\StringUtilsFactory $resource,
162  \Magento\Store\Model\StoreManagerInterface $storeManager,
163  \Zend_Filter_Interface $inputFilter,
164  \Magento\Framework\App\State $appState,
165  \Magento\Framework\App\Cache\TypeListInterface $appCache,
166  \Magento\Framework\Translate\InlineInterface $translateInline,
167  array $relatedCacheTypes = []
168  ) {
169  $this->_resourceFactory = $resource;
170  $this->_storeManager = $storeManager;
171  $this->_inputFilter = $inputFilter;
172  $this->_appState = $appState;
173  $this->_appCache = $appCache;
174  $this->_translateInline = $translateInline;
175  $this->relatedCacheTypes = $relatedCacheTypes;
176  }
177 
184  public function processAjaxPost(array $translateParams)
185  {
186  if (!$this->_translateInline->isAllowed()) {
187  return ['inline' => 'not allowed'];
188  }
189  if (!empty($this->relatedCacheTypes)) {
190  $this->_appCache->invalidate($this->relatedCacheTypes);
191  }
192 
193  $this->_validateTranslationParams($translateParams);
194  $this->_filterTranslationParams($translateParams, ['custom']);
195 
197  $validStoreId = $this->_storeManager->getStore()->getId();
198 
200  $resource = $this->_resourceFactory->create();
201  foreach ($translateParams as $param) {
202  if ($this->_appState->getAreaCode() == \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE) {
203  $storeId = 0;
204  } else {
205  if (empty($param['perstore'])) {
206  $resource->deleteTranslate($param['original'], null, false);
207  $storeId = 0;
208  } else {
209  $storeId = $validStoreId;
210  }
211  }
212  $resource->saveTranslate($param['original'], $param['custom'], null, $storeId);
213  }
214 
215  return $this->getCacheManger()->updateAndGetTranslations();
216  }
217 
225  protected function _validateTranslationParams(array $translateParams)
226  {
227  foreach ($translateParams as $param) {
228  if (!is_array($param) || !isset($param['original']) || !isset($param['custom'])) {
229  throw new \InvalidArgumentException(
230  'Both original and custom phrases are required for inline translation.'
231  );
232  }
233  }
234  }
235 
243  protected function _filterTranslationParams(array &$translateParams, array $fieldNames)
244  {
245  foreach ($translateParams as &$param) {
246  foreach ($fieldNames as $fieldName) {
247  $param[$fieldName] = $this->_inputFilter->filter($param[$fieldName]);
248  }
249  }
250  }
251 
258  public function processResponseBodyString($body)
259  {
260  $this->_content = $body;
261 
262  $this->_specialTags();
263  $this->_tagAttributes();
264  $this->_otherText();
265 
266  return $this->_content;
267  }
268 
274  public function getContent()
275  {
276  return $this->_content;
277  }
278 
285  public function setContent($content)
286  {
287  $this->_content = $content;
288  }
289 
296  public function setIsJson($flag)
297  {
298  $this->_isJson = $flag;
299  return $this;
300  }
301 
310  protected function _getAttributeLocation($matches, $options)
311  {
312  // return value should not be translated.
313  return 'Tag attribute (ALT, TITLE, etc.)';
314  }
315 
324  protected function _getTagLocation($matches, $options)
325  {
326  $tagName = strtolower($options['tagName']);
327 
328  if (isset($options['tagList'][$tagName])) {
329  return $options['tagList'][$tagName];
330  }
331 
332  return ucfirst($tagName) . ' Text';
333  }
334 
343  protected function _applySpecialTagsFormat($tagHtml, $tagName, $trArr)
344  {
345  $specialTags = $tagHtml . '<span class="translate-inline-' . $tagName . '" ' . $this->_getHtmlAttribute(
346  self::DATA_TRANSLATE,
347  '[' . htmlspecialchars(join(',', $trArr)) . ']'
348  );
349  $additionalAttr = $this->_getAdditionalHtmlAttribute($tagName);
350  if ($additionalAttr !== null) {
351  $specialTags .= ' ' . $additionalAttr . '>';
352  } else {
353  $specialTags .= '>' . strtoupper($tagName);
354  }
355  $specialTags .= '</span>';
356  return $specialTags;
357  }
358 
367  protected function _applySimpleTagsFormat($tagHtml, $tagName, $trArr)
368  {
369  $simpleTags = substr(
370  $tagHtml,
371  0,
372  strlen($tagName) + 1
373  ) . ' ' . $this->_getHtmlAttribute(
374  self::DATA_TRANSLATE,
375  htmlspecialchars('[' . join(',', $trArr) . ']')
376  );
377  $additionalAttr = $this->_getAdditionalHtmlAttribute($tagName);
378  if ($additionalAttr !== null) {
379  $simpleTags .= ' ' . $additionalAttr;
380  }
381  $simpleTags .= substr($tagHtml, strlen($tagName) + 1);
382  return $simpleTags;
383  }
384 
394  private function _getTranslateData($regexp, &$text, $locationCallback, $options = [])
395  {
396  $trArr = [];
397  $next = 0;
398  while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE, $next)) {
399  $trArr[] = json_encode(
400  [
401  'shown' => htmlspecialchars_decode($matches[1][0]),
402  'translated' => htmlspecialchars_decode($matches[2][0]),
403  'original' => htmlspecialchars_decode($matches[3][0]),
404  'location' => htmlspecialchars_decode(call_user_func($locationCallback, $matches, $options)),
405  ]
406  );
407  $text = substr_replace($text, $matches[1][0], $matches[0][1], strlen($matches[0][0]));
408  $next = $matches[0][1];
409  }
410  return $trArr;
411  }
412 
418  private function _tagAttributes()
419  {
420  $this->_prepareTagAttributesForContent($this->_content);
421  }
422 
429  private function _prepareTagAttributesForContent(&$content)
430  {
431  $quoteHtml = $this->_getHtmlQuote();
432  $tagMatch = [];
433  $nextTag = 0;
434  $tagRegExp = '#<([a-z]+)\s*?[^>]+?((' . self::REGEXP_TOKEN . ')[^>]*?)+\\\\?/?>#iS';
435  while (preg_match($tagRegExp, $content, $tagMatch, PREG_OFFSET_CAPTURE, $nextTag)) {
436  $tagHtml = $tagMatch[0][0];
437  $matches = [];
438  $attrRegExp = '#' . self::REGEXP_TOKEN . '#S';
439  $trArr = $this->_getTranslateData($attrRegExp, $tagHtml, [$this, '_getAttributeLocation']);
440  if ($trArr) {
441  $transRegExp = '# ' . $this->_getHtmlAttribute(
442  self::DATA_TRANSLATE,
443  '\[([^' . preg_quote($quoteHtml) . ']*)]'
444  ) . '#i';
445  if (preg_match($transRegExp, $tagHtml, $matches)) {
446  $tagHtml = str_replace($matches[0], '', $tagHtml);
447  $trAttr = ' ' . $this->_getHtmlAttribute(
448  self::DATA_TRANSLATE,
449  '[' . htmlspecialchars($matches[1]) . ',' . str_replace("\"", "'", join(',', $trArr)) . ']'
450  );
451  } else {
452  $trAttr = ' ' . $this->_getHtmlAttribute(
453  self::DATA_TRANSLATE,
454  '[' . str_replace("\"", "'", join(',', $trArr)) . ']'
455  );
456  }
457  $trAttr = $this->_addTranslateAttribute($trAttr);
458 
459  $tagHtml = substr_replace($tagHtml, $trAttr, strlen($tagMatch[1][0]) + 1, 1);
460  $content = substr_replace($content, $tagHtml, $tagMatch[0][1], strlen($tagMatch[0][0]));
461  }
462  $nextTag = $tagMatch[0][1] + strlen($tagHtml);
463  }
464  }
465 
473  private function _getHtmlAttribute($name, $value)
474  {
475  return $name . '=' . $this->_getHtmlQuote() . $value . $this->_getHtmlQuote();
476  }
477 
484  private function _addTranslateAttribute($trAttr)
485  {
486  $translateAttr = $trAttr;
487  $additionalAttr = $this->_getAdditionalHtmlAttribute();
488  if ($additionalAttr !== null) {
489  $translateAttr .= ' ' . $additionalAttr . ' ';
490  }
491  return $translateAttr;
492  }
493 
499  private function _getHtmlQuote()
500  {
501  if ($this->_isJson) {
502  return '\"';
503  } else {
504  return '"';
505  }
506  }
507 
513  private function _specialTags()
514  {
515  $this->_translateTags($this->_content, $this->_allowedTagsGlobal, '_applySpecialTagsFormat');
516  $this->_translateTags($this->_content, $this->_allowedTagsSimple, '_applySimpleTagsFormat');
517  }
518 
527  private function _translateTags(&$content, $tagsList, $formatCallback)
528  {
529  $nextTag = 0;
530  $tagRegExpBody = '#<(body)(/?>| \s*[^>]*+/?>)#iSU';
531 
532  $tags = implode('|', array_keys($tagsList));
533  $tagRegExp = '#<(' . $tags . ')(/?>| \s*[^>]*+/?>)#iSU';
534  $tagMatch = [];
535  $headTranslateTags = '';
536  while (preg_match($tagRegExp, $content, $tagMatch, PREG_OFFSET_CAPTURE, $nextTag)) {
537  $tagName = strtolower($tagMatch[1][0]);
538  if (substr($tagMatch[0][0], -2) == '/>') {
539  $tagClosurePos = $tagMatch[0][1] + strlen($tagMatch[0][0]);
540  } else {
541  $tagClosurePos = $this->_findEndOfTag($content, $tagName, $tagMatch[0][1]);
542  }
543 
544  if ($tagClosurePos === false) {
545  $nextTag += strlen($tagMatch[0][0]);
546  continue;
547  }
548 
549  $tagLength = $tagClosurePos - $tagMatch[0][1];
550 
551  $tagStartLength = strlen($tagMatch[0][0]);
552 
553  $tagHtml = $tagMatch[0][0] . substr(
554  $content,
555  $tagMatch[0][1] + $tagStartLength,
556  $tagLength - $tagStartLength
557  );
558  $tagClosurePos = $tagMatch[0][1] + strlen($tagHtml);
559 
560  $trArr = $this->_getTranslateData(
561  '#' . self::REGEXP_TOKEN . '#iS',
562  $tagHtml,
563  [$this, '_getTagLocation'],
564  ['tagName' => $tagName, 'tagList' => $tagsList]
565  );
566 
567  if (!empty($trArr)) {
568  $trArr = array_unique($trArr);
569 
570  $tagBodyMatch = [];
571  preg_match($tagRegExpBody, $content, $tagBodyMatch, PREG_OFFSET_CAPTURE);
572  if (!empty($tagBodyMatch)) {
573  $tagBodyOpenStartPosition = $tagBodyMatch[0][1];
574 
575  if (array_key_exists($tagName, $this->_allowedTagsGlobal)
576  && $tagBodyOpenStartPosition > $tagMatch[0][1]
577  ) {
578  $tagHtmlHead = call_user_func([$this, $formatCallback], $tagHtml, $tagName, $trArr);
579  $headTranslateTags .= substr($tagHtmlHead, strlen($tagHtml));
580  } else {
581  $tagHtml = call_user_func([$this, $formatCallback], $tagHtml, $tagName, $trArr);
582  }
583  }
584 
585  $tagClosurePos = $tagMatch[0][1] + strlen($tagHtml);
586  $content = substr_replace($content, $tagHtml, $tagMatch[0][1], $tagLength);
587  }
588  $nextTag = $tagClosurePos;
589  }
590  if ($headTranslateTags) {
591  $tagBodyMatch = [];
592  preg_match($tagRegExpBody, $content, $tagBodyMatch, PREG_OFFSET_CAPTURE);
593  $tagBodyOpenStartPosition = $tagBodyMatch[0][1];
594  $openTagBodyEndPosition = $tagBodyOpenStartPosition + strlen($tagBodyMatch[0][0]);
595  $content = substr($content, 0, $openTagBodyEndPosition)
596  . $headTranslateTags
597  . substr($content, $openTagBodyEndPosition);
598  }
599  }
600 
609  private function _findEndOfTag($body, $tagName, $from)
610  {
611  $openTag = '<' . $tagName;
612  $closeTag = ($this->_isJson ? '<\\/' : '</') . $tagName;
613  $tagLength = strlen($tagName);
614  $length = $tagLength + 1;
615  $end = $from + 1;
616  while (substr_count($body, $openTag, $from, $length) !== substr_count($body, $closeTag, $from, $length)) {
617  $end = strpos($body, $closeTag, $end + $tagLength + 1);
618  if ($end === false) {
619  return false;
620  }
621  $length = $end - $from + $tagLength + 3;
622  }
623  if (preg_match('#<\\\\?\/' . $tagName . '\s*?>#i', $body, $tagMatch, null, $end)) {
624  return $end + strlen($tagMatch[0]);
625  } else {
626  return false;
627  }
628  }
629 
635  private function _otherText()
636  {
637  $next = 0;
638  $matches = [];
639  while (preg_match('#' . self::REGEXP_TOKEN . '#', $this->_content, $matches, PREG_OFFSET_CAPTURE, $next)) {
640  $translateProperties = json_encode(
641  [
642  'shown' => $matches[1][0],
643  'translated' => $matches[2][0],
644  'original' => $matches[3][0],
645  'location' => 'Text',
646  'scope' => $matches[4][0],
647  ],
648  JSON_HEX_QUOT
649  );
650 
651  $spanHtml = $this->_getDataTranslateSpan(
652  '[' . htmlspecialchars($translateProperties) . ']',
653  $matches[1][0]
654  );
655  $this->_content = substr_replace($this->_content, $spanHtml, $matches[0][1], strlen($matches[0][0]));
656  $next = $matches[0][1] + strlen($spanHtml) - 1;
657  }
658  }
659 
667  protected function _getDataTranslateSpan($data, $text)
668  {
669  $translateSpan = '<span ' . $this->_getHtmlAttribute(self::DATA_TRANSLATE, $data);
670  $additionalAttr = $this->_getAdditionalHtmlAttribute();
671  if ($additionalAttr !== null) {
672  $translateSpan .= ' ' . $additionalAttr;
673  }
674  $translateSpan .= '>' . $text . '</span>';
675  return $translateSpan;
676  }
677 
684  protected function _getAdditionalHtmlAttribute($tagName = null)
685  {
686  return $this->_translateInline->getAdditionalHtmlAttribute($tagName);
687  }
688 }
_getTagLocation($matches, $options)
Definition: Parser.php:324
_validateTranslationParams(array $translateParams)
Definition: Parser.php:225
$storeManager
endifif( $block->getLastPageNum()>1)( 'Page') ?></strong >< ul class $text
Definition: pager.phtml:43
$resource
Definition: bulk.php:12
$value
Definition: gender.phtml:16
_filterTranslationParams(array &$translateParams, array $fieldNames)
Definition: Parser.php:243
_applySimpleTagsFormat($tagHtml, $tagName, $trArr)
Definition: Parser.php:367
_applySpecialTagsFormat($tagHtml, $tagName, $trArr)
Definition: Parser.php:343
_getAttributeLocation($matches, $options)
Definition: Parser.php:310
__construct(\Magento\Translation\Model\ResourceModel\StringUtilsFactory $resource, \Magento\Store\Model\StoreManagerInterface $storeManager, \Zend_Filter_Interface $inputFilter, \Magento\Framework\App\State $appState, \Magento\Framework\App\Cache\TypeListInterface $appCache, \Magento\Framework\Translate\InlineInterface $translateInline, array $relatedCacheTypes=[])
Definition: Parser.php:160
_getAdditionalHtmlAttribute($tagName=null)
Definition: Parser.php:684
if(!isset($_GET['name'])) $name
Definition: log.php:14