PluginProbe
Linguise – AI Automatic Multilingual Translation / trunk
Linguise – AI Automatic Multilingual Translation vtrunk
2.2.64 2.2.63 2.2.62 2.2.61 2.2.60 2.2.59 2.2.58 2.2.57 2.2.56 2.2.55 2.2.54 2.2.53 2.2.52 2.2.51 2.2.50 2.2.49 2.2.47 2.2.48 2.2.46 2.2.45 2.2.44 2.2.43 2.2.42 1.9.7 1.9.8 All 255 releases
linguise / src / AttributeHandler.php

AttributeHandler.php in Linguise – AI Automatic Multilingual Translation trunk, at src/AttributeHandler.php

429 lines 16.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Linguise\WordPress;
4
5 use Linguise\WordPress\FragmentHandler;
6 use Linguise\WordPress\HTMLHelper;
7 use Linguise\Vendor\JsonPath\JsonObject;
8 use Linguise\Vendor\Linguise\Script\Core\Debug;
9
10 defined('ABSPATH') || die('');
11
12 /**
13 * AttributeHandler, derived from FragmentHandler
14 *
15 * This class handle parsing and injecting
16 * JSON data from HTML attributes.
17 */
18 class AttributeHandler extends FragmentHandler
19 {
20 /**
21 * Get attribute JSON fragment matching
22 *
23 * Similar to `getJSONOverrideMatcher()` but we use DOMDocument to do the matching
24 * - name: The name of the plugin, e.g. 'wpca', must be unique
25 * - key: the data attribute to match, e.g. 'data-wpca'
26 * - encode: If the JSON data is URL encoded or not, default to false (optional)
27 * - strict: If the JSON data should be strict or not, default to false (optional)
28 * - mode: The mode of the matcher, default to 'json', can be a 'string' or 'json' (optional)
29 * - entity: The entity name to match, e.g. 'sc-wpca', default to nil (optional)
30 *
31 * @param string $html_data The HTML data input
32 *
33 * @return array The array of JSON to match with fragment
34 */
35 private static function getMatcher($html_data)
36 {
37 $current_list = [];
38
39 if (defined('LINGUISE_WP_PLUGIN_TEST_MODE')) {
40 $current_list[] = [
41 'name' => 'linguise-demo-test',
42 'key' => 'data-linguise-demo',
43 ];
44
45 $current_list[] = [
46 'name' => 'linguise-demo-string',
47 'key' => 'data-label',
48 'mode' => 'string',
49 'matchers' => [
50 [
51 'key' => 'sc-linguise-demo',
52 'type' => 'tag'
53 ]
54 ],
55 ];
56
57 $current_list[] = [
58 'name' => 'linguise-demo-string-encoded',
59 'key' => 'data-label-encoded',
60 'mode' => 'string-encoded',
61 'matchers' => [
62 [
63 'key' => 'sc-linguise-demo-encoded',
64 'type' => 'tag'
65 ]
66 ],
67 ];
68 }
69
70 $current_list = apply_filters('linguise_fragment_attributes', $current_list, $html_data);
71
72 // loop through the list and add extra field, use reference
73 foreach ($current_list as &$matcher) {
74 $name_sanitized = str_replace(' ', '-', $matcher['name']);
75 $name_sanitized = sanitize_key($name_sanitized);
76 $matcher['attr_ids'] = 'data-linguise-attribute-' . $name_sanitized;
77 }
78
79 return $current_list;
80 }
81
82 /**
83 * Find the matcher by name.
84 *
85 * @param string $matcher_name The name of the matcher to find
86 * @param array $matchers The list of matchers to search in
87 *
88 * @return array|null The matcher if found, null otherwise
89 */
90 private static function findMatcher($matcher_name, $matchers)
91 {
92 foreach ($matchers as $matcher) {
93 if ($matcher['name'] === $matcher_name) {
94 return $matcher;
95 }
96 }
97
98 return null;
99 }
100
101 /**
102 * Process a list of matchers and see if it would match the entity.
103 *
104 * @param array $matchers The list of matchers to process
105 * @param \DomElement $entity The entity to match against
106 * @param string $match_mode The match mode, can be 'any' or 'all', default to 'any'
107 *
108 * @return boolean True if the entity matches any of the matchers, false otherwise
109 */
110 private static function processMatcher($matchers, $entity, $match_mode = 'any')
111 {
112 if (empty($matchers)) {
113 return true;
114 }
115
116 $matchers_results = [];
117 foreach ($matchers as $matcher) {
118 $kind = isset($matcher['type']) ? $matcher['type'] : 'attribute';
119 if ($kind === 'attribute') {
120 $key = isset($matcher['key']) ? $matcher['key'] : '';
121 if (!empty($key) && $entity->hasAttribute($key)) {
122 $value_orig = $entity->getAttribute($key);
123 if (isset($matcher['value'])) {
124 $matchers_results[] = $value_orig === $matcher['value'];
125 } else {
126 $matchers_results[] = true;
127 }
128 }
129 } elseif ($kind === 'tag') {
130 // Check with tag name
131 if (isset($matcher['key']) && $matcher['key'] === $entity->tagName) {
132 $matchers_results[] = true;
133 } else {
134 $matchers_results[] = false;
135 }
136 } elseif ($kind === 'class') {
137 // Values
138 $values = isset($matcher['value']) ? $matcher['value'] : [];
139 $class_list = explode(' ', $entity->getAttribute('class'));
140 $mode = isset($matcher['mode']) ? $matcher['mode'] : 'any';
141 if ($mode === 'all') {
142 // If all of the class matches, we return true
143 $matchers_results[] = empty(array_diff($values, $class_list));
144 } else {
145 // If any of the class matches, we return true
146 $matchers_results[] = !empty(array_intersect($values, $class_list));
147 }
148 }
149 }
150
151 if ($match_mode === 'all') {
152 // If all of the matchers matched, we return true
153 return !in_array(false, $matchers_results, true);
154 } else {
155 // If any of the matchers matched, we return true
156 return in_array(true, $matchers_results, true);
157 }
158 }
159
160 /**
161 * Parse the HTML input or data into the fragments.
162 *
163 * @param string $html_data The HTML data to be parsed
164 *
165 * @return array
166 */
167 public static function findWPFragments(&$html_data)
168 {
169 $html_dom = HTMLHelper::loadHTML($html_data);
170 if (empty($html_dom)) {
171 return [];
172 }
173
174 $matchers = self::getMatcher($html_data);
175 $internal_counter = 0;
176
177 $all_fragments = [];
178 // Loop through all valid DOM elements
179 $elements = $html_dom->getElementsByTagName('*');
180 foreach ($elements as $element) {
181 foreach ($matchers as $matcher) {
182 $key_data = $element->getAttribute($matcher['key']);
183 if (empty($key_data)) {
184 continue;
185 }
186
187 // Unroll the entity
188 $key_data = html_entity_decode(HTMLHelper::unprotectEntity($key_data), ENT_QUOTES, 'UTF-8');
189
190 // Is the data URL encoded?
191 $should_encode = isset($matcher['encode']) && $matcher['encode'];
192 if ($should_encode) {
193 $key_data = urldecode($key_data);
194 }
195
196 $additional_matchers = isset($matcher['matchers']) ? $matcher['matchers'] : [];
197 $additional_match_mode = isset($matcher['match_mode']) ? $matcher['match_mode'] : 'any';
198 if (!empty($additional_matchers)) {
199 $is_matched = self::processMatcher($additional_matchers, $element, $additional_match_mode);
200 if (!$is_matched) {
201 // Skip unmatched element
202 continue;
203 }
204 }
205
206 if (isset($matcher['entity']) && $matcher['entity'] !== $element->tagName) {
207 // Skip unmatched entity name
208 continue;
209 }
210
211 if (isset($matcher['mode']) && $matcher['mode'] === 'string') {
212 // If the mode is a string we just pass the string as is
213 $cast_data = isset($matcher['cast']) ? $matcher['cast'] : 'html-main';
214 $collected_temp = [
215 [
216 'key' => $matcher['key'],
217 'value' => $key_data,
218 'format' => $cast_data,
219 ]
220 ];
221 } elseif (isset($matcher['mode']) && $matcher['mode'] === 'string-encoded') {
222 $cast_data = isset($matcher['cast']) ? $matcher['cast'] : 'html-main';
223
224 $collected_temp = [
225 [
226 'key' => $matcher['key'],
227 'value' => html_entity_decode($key_data, ENT_QUOTES | ENT_SUBSTITUTE, 'utf-8'),
228 'format' => $cast_data,
229 ]
230 ];
231 } else {
232 $json_data = json_decode($key_data, true);
233 if (json_last_error() !== JSON_ERROR_NONE) {
234 continue;
235 }
236
237 $is_strict = isset($matcher['strict']) && $matcher['strict'];
238 $collected_temp = self::collectFragmentFromJson($json_data, $is_strict);
239 }
240
241 if (!empty($collected_temp)) {
242 $int_marker = $matcher['attr_ids'] . '-' . $internal_counter;
243 $element->setAttribute($matcher['attr_ids'], $int_marker);
244 $all_fragments[$matcher['name']][$int_marker] = [
245 'mode' => 'attribute',
246 'attribute' => $element->tagName,
247 'fragments' => $collected_temp,
248 ];
249 $internal_counter++;
250 }
251 }
252 }
253
254 if (!empty($all_fragments)) {
255 $html_data = HTMLHelper::saveHTML($html_dom);
256 }
257
258 Debug::log('AttributeHandler -> Collected: ' . json_encode($all_fragments, JSON_PRETTY_PRINT));
259
260 return $all_fragments;
261 }
262
263 /**
264 * Find the element by attribute and matcher.
265 *
266 * @param \DOMDocument $html_dom The HTML DOM object
267 * @param string $tag_name The tag name to search for
268 * @param string $attr_name The attribute name to search for
269 * @param string $attr_value The attribute value to search for
270 *
271 * @return \DOMElement|null The found element or null if not found
272 */
273 private static function findElementByAttributeAndMatcher($html_dom, $tag_name, $attr_name, $attr_value)
274 {
275 $elements = $html_dom->getElementsByTagName($tag_name);
276 foreach ($elements as $element) {
277 if ($element->nodeType !== XML_ELEMENT_NODE) {
278 continue;
279 }
280
281 if ($element->hasAttribute($attr_name) && $element->getAttribute($attr_name) === $attr_value) {
282 return $element;
283 }
284 }
285 return null;
286 }
287
288 /**
289 * Inject the fragments into the HTML data.
290 *
291 * @param string $html_data The HTML data to be injected
292 * @param array $fragments The array of fragments to be injected, from intoJSONFragments
293 *
294 * @return string
295 */
296 public static function injectTranslatedWPFragments($html_data, $fragments)
297 {
298 if (empty($fragments)) {
299 return $html_data;
300 }
301
302 $html_dom = HTMLHelper::loadHTML($html_data);
303 if (empty($html_dom)) {
304 return $html_data;
305 }
306
307 Debug::log('AttributeHandler -> Injecting: ' . json_encode($fragments, JSON_PRETTY_PRINT));
308
309 $fragment_matchers = self::getMatcher($html_data);
310 foreach ($fragments as $fragment_name => $fragment_jsons) {
311 $matched = self::findMatcher($fragment_name, $fragment_matchers);
312 foreach ($fragment_jsons as $fragment_param => $fragment_list) {
313 self::removeFragmentMarkers($html_dom, $fragment_name, $fragment_param);
314
315 if (!isset($fragment_list['attribute'])) {
316 continue;
317 }
318 if (empty($matched)) {
319 continue;
320 }
321
322 $attr_html = self::findElementByAttributeAndMatcher(
323 $html_dom,
324 $fragment_list['attribute'],
325 $matched['attr_ids'],
326 $fragment_param
327 );
328
329 if (empty($attr_html)) {
330 continue;
331 }
332
333 $match_data = $attr_html->getAttribute($matched['key']);
334 if (empty($match_data)) {
335 continue;
336 }
337
338 // Since we have protection enabled around this!
339 $match_data = html_entity_decode(HTMLHelper::unprotectEntity($match_data), ENT_QUOTES, 'UTF-8');
340
341 $should_encode = isset($matched['encode']) && $matched['encode'];
342 if ($should_encode) {
343 $match_data = urldecode($match_data);
344 }
345
346 // Check mode, if string mode, we just pass the string as is
347 if (isset($matched['mode']) && $matched['mode'] === 'string') {
348 // Get first item
349 $first_fragment = isset($fragment_list['fragments'][0]) ? $fragment_list['fragments'][0] : null;
350 if (empty($first_fragment)) {
351 continue;
352 }
353
354 $first_value = isset($first_fragment['value']) ? $first_fragment['value'] : null;
355 if (empty($first_value)) {
356 continue;
357 }
358
359 // Replace the data
360 $replaced_text = $first_value;
361 if ($should_encode) {
362 $replaced_text = rawurlencode($replaced_text);
363 }
364
365 $protected_json = HTMLHelper::protectEntity($replaced_text);
366 } elseif (isset($matched['mode']) && $matched['mode'] === 'string-encoded') {
367 // Get first item
368 $first_fragment = isset($fragment_list['fragments'][0]) ? $fragment_list['fragments'][0] : null;
369 if (empty($first_fragment)) {
370 continue;
371 }
372
373 $first_value = isset($first_fragment['value']) ? $first_fragment['value'] : null;
374 if (empty($first_value)) {
375 continue;
376 }
377
378 // Replace the data
379 $replaced_text = htmlspecialchars($first_value, ENT_QUOTES, 'UTF-8', false);
380 if ($should_encode) {
381 $replaced_text = rawurlencode($replaced_text);
382 }
383
384 $protected_json = HTMLHelper::protectEntity($replaced_text);
385 } else {
386 // JSON mode, we need to decode the JSON data
387 $json_decoded = json_decode($match_data, true);
388 if (json_last_error() !== JSON_ERROR_NONE) {
389 continue;
390 }
391
392 $json_data = new JsonObject($json_decoded);
393 foreach ($fragment_list['fragments'] as $fragment) {
394 $dec_key = self::unwrapKey($fragment['key']);
395 try {
396 $json_data->set('$.' . $dec_key, $fragment['value']);
397 } catch (\Linguise\Vendor\JsonPath\InvalidJsonPathException $e) {
398 Debug::log('Failed to set key in attributes: ' . $dec_key . ' -> ' . $e->getMessage());
399 }
400 }
401
402 $replaced_json = $json_data->getJson();
403 if ($should_encode) {
404 $replaced_json = rawurlencode($replaced_json);
405 }
406
407 // Protect the entity back
408 $protected_json = HTMLHelper::protectEntity(htmlspecialchars($replaced_json, ENT_QUOTES, 'UTF-8', false));
409 }
410
411 $attr_html->setAttribute($matched['key'], $protected_json);
412 $attr_html->removeAttribute($matched['attr_ids']);
413 }
414 }
415
416 $html_data = HTMLHelper::saveHTML($html_dom);
417
418 // Unmangle stuff like &amp;#xE5;
419 $html_data = preg_replace('/&amp;#x([0-9A-Fa-f]+);/', '&#x$1;', $html_data);
420
421 $mod_html_data = apply_filters('linguise_after_attribute_translation', $html_data);
422 if (!empty($mod_html_data)) {
423 $html_data = $mod_html_data;
424 }
425
426 return $html_data;
427 }
428 }
429