|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* SEOmatic plugin for Craft CMS 3.x |
|
4
|
|
|
* |
|
5
|
|
|
* A turnkey SEO implementation for Craft CMS that is comprehensive, powerful, |
|
6
|
|
|
* and flexible |
|
7
|
|
|
* |
|
8
|
|
|
* @link https://nystudio107.com |
|
9
|
|
|
* @copyright Copyright (c) 2017 nystudio107 |
|
10
|
|
|
*/ |
|
11
|
|
|
|
|
12
|
|
|
namespace nystudio107\seomatic\helpers; |
|
13
|
|
|
|
|
14
|
|
|
use Craft; |
|
15
|
|
|
use craft\base\Element; |
|
16
|
|
|
use craft\elements\Asset; |
|
17
|
|
|
use craft\errors\SiteNotFoundException; |
|
18
|
|
|
use craft\web\View; |
|
19
|
|
|
use nystudio107\seomatic\Seomatic; |
|
20
|
|
|
use ReflectionClass; |
|
21
|
|
|
use ReflectionException; |
|
22
|
|
|
use Throwable; |
|
23
|
|
|
use Twig\Markup; |
|
24
|
|
|
use yii\base\Exception; |
|
25
|
|
|
use function in_array; |
|
26
|
|
|
use function is_object; |
|
27
|
|
|
use function is_string; |
|
28
|
|
|
|
|
29
|
|
|
/** |
|
30
|
|
|
* @author nystudio107 |
|
31
|
|
|
* @package Seomatic |
|
32
|
|
|
* @since 3.0.0 |
|
33
|
|
|
*/ |
|
34
|
|
|
class MetaValue |
|
35
|
|
|
{ |
|
36
|
|
|
// Constants |
|
37
|
|
|
// ========================================================================= |
|
38
|
|
|
|
|
39
|
|
|
const MAX_TEMPLATE_LENGTH = 4096; |
|
40
|
|
|
const MAX_PARSE_TRIES = 5; |
|
41
|
|
|
// Semicolon because that is the resolved config key when rendering tags, |
|
42
|
|
|
// kebab-case because that is the config keys as defined in the config files. |
|
43
|
|
|
const NO_ALIASES = [ |
|
44
|
|
|
'twitter:site', |
|
45
|
|
|
'twitter:creator', |
|
46
|
|
|
'twitterSite', |
|
47
|
|
|
'twitterCreator', |
|
48
|
|
|
]; |
|
49
|
|
|
const NO_PARSING = [ |
|
50
|
|
|
'siteLinksSearchTarget', |
|
51
|
|
|
]; |
|
52
|
|
|
const PARSE_ONCE = [ |
|
53
|
|
|
'target', |
|
54
|
|
|
'urlTemplate', |
|
55
|
|
|
]; |
|
56
|
|
|
|
|
57
|
|
|
// Static Properties |
|
58
|
|
|
// ========================================================================= |
|
59
|
|
|
|
|
60
|
|
|
/** |
|
61
|
|
|
* @var array |
|
62
|
|
|
*/ |
|
63
|
|
|
public static $templateObjectVars; |
|
64
|
|
|
|
|
65
|
|
|
/** |
|
66
|
|
|
* @var array |
|
67
|
|
|
*/ |
|
68
|
|
|
public static $templatePreviewVars = []; |
|
69
|
|
|
|
|
70
|
|
|
/** |
|
71
|
|
|
* @var View |
|
72
|
|
|
*/ |
|
73
|
|
|
public static $view; |
|
74
|
|
|
|
|
75
|
|
|
// Static Methods |
|
76
|
|
|
// ========================================================================= |
|
77
|
|
|
|
|
78
|
|
|
/** |
|
79
|
|
|
* @param string $metaValue |
|
80
|
|
|
* @param bool $resolveAliases Whether @ aliases should be resolved in |
|
81
|
|
|
* this string |
|
82
|
|
|
* @param bool $parseAsTwig Whether items should be parsed as a Twig |
|
83
|
|
|
* template in this string |
|
84
|
|
|
* @param int $tries The number of times to parse the string |
|
85
|
|
|
* |
|
86
|
|
|
* @return string |
|
87
|
|
|
*/ |
|
88
|
1 |
|
public static function parseString( |
|
89
|
|
|
$metaValue, |
|
90
|
|
|
bool $resolveAliases = true, |
|
91
|
|
|
bool $parseAsTwig = true, |
|
92
|
|
|
$tries = self::MAX_PARSE_TRIES |
|
93
|
|
|
) |
|
94
|
|
|
{ |
|
95
|
|
|
// If it's a string, and there are no dynamic tags, just return the template |
|
96
|
1 |
|
if (is_string($metaValue) && !str_contains($metaValue, '{')) { |
|
97
|
1 |
|
return self::parseMetaString($metaValue, $resolveAliases, $parseAsTwig) ?? $metaValue; |
|
98
|
|
|
} |
|
99
|
|
|
// Parse it repeatedly until it doesn't change |
|
100
|
|
|
$value = ''; |
|
101
|
|
|
while ($metaValue !== $value && $tries) { |
|
102
|
|
|
$tries--; |
|
103
|
|
|
$value = $metaValue; |
|
104
|
|
|
$metaValue = self::parseMetaString($value, $resolveAliases, $parseAsTwig) ?? $metaValue; |
|
105
|
|
|
} |
|
106
|
|
|
|
|
107
|
|
|
return $metaValue; |
|
108
|
|
|
} |
|
109
|
|
|
|
|
110
|
|
|
/** |
|
111
|
|
|
* @param array $metaArray |
|
112
|
|
|
* @param bool $resolveAliases Whether @ aliases should be resolved in |
|
113
|
|
|
* this array |
|
114
|
|
|
* @param bool $parseAsTwig Whether items should be parsed as a Twig |
|
115
|
|
|
* template in this array |
|
116
|
|
|
*/ |
|
117
|
|
|
public static function parseArray(array &$metaArray, bool $resolveAliases = true, bool $parseAsTwig = true) |
|
118
|
|
|
{ |
|
119
|
|
|
// Do this here as well so that parseString() won't potentially be constantly switching modes |
|
120
|
|
|
// while parsing through the array |
|
121
|
|
|
$oldTemplateMode = self::$view->getTemplateMode(); |
|
122
|
|
|
// Render in site template mode so that we get globals injected |
|
123
|
|
|
if ($oldTemplateMode !== self::$view::TEMPLATE_MODE_SITE) { |
|
124
|
|
|
try { |
|
125
|
|
|
self::$view->setTemplateMode(self::$view::TEMPLATE_MODE_SITE); |
|
126
|
|
|
} catch (Exception $e) { |
|
127
|
|
|
Craft::error($e->getMessage(), __METHOD__); |
|
128
|
|
|
} |
|
129
|
|
|
} |
|
130
|
|
|
foreach ($metaArray as $key => $value) { |
|
131
|
|
|
$shouldParse = $parseAsTwig; |
|
132
|
|
|
$shouldAlias = $resolveAliases; |
|
133
|
|
|
$tries = self::MAX_PARSE_TRIES; |
|
134
|
|
|
if (in_array($key, self::NO_ALIASES, true)) { |
|
135
|
|
|
$shouldAlias = false; |
|
136
|
|
|
} |
|
137
|
|
|
if (in_array($key, self::NO_PARSING, true)) { |
|
138
|
|
|
$shouldParse = false; |
|
139
|
|
|
} |
|
140
|
|
|
if (in_array($key, self::PARSE_ONCE, true)) { |
|
141
|
|
|
$tries = 1; |
|
142
|
|
|
if (is_string($value) && $value[0] !== '{') { |
|
143
|
|
|
$shouldParse = false; |
|
144
|
|
|
} |
|
145
|
|
|
} |
|
146
|
|
|
if ($value !== null) { |
|
147
|
|
|
$metaArray[$key] = self::parseString($value, $shouldAlias, $shouldParse, $tries); |
|
148
|
|
|
} |
|
149
|
|
|
} |
|
150
|
|
|
// Restore the template mode |
|
151
|
|
|
if ($oldTemplateMode !== self::$view::TEMPLATE_MODE_SITE) { |
|
152
|
|
|
try { |
|
153
|
|
|
self::$view->setTemplateMode($oldTemplateMode); |
|
154
|
|
|
} catch (Exception $e) { |
|
155
|
|
|
Craft::error($e->getMessage(), __METHOD__); |
|
156
|
|
|
} |
|
157
|
|
|
} |
|
158
|
|
|
|
|
159
|
|
|
// Remove any empty values |
|
160
|
|
|
$metaArray = array_filter( |
|
161
|
|
|
$metaArray, |
|
162
|
|
|
[ArrayHelper::class, 'preserveNumerics'] |
|
163
|
|
|
); |
|
164
|
|
|
} |
|
165
|
|
|
|
|
166
|
|
|
/** |
|
167
|
|
|
* Get the language from a siteId |
|
168
|
|
|
* |
|
169
|
|
|
* @param null|int $siteId |
|
170
|
|
|
* |
|
171
|
|
|
* @return string |
|
172
|
|
|
*/ |
|
173
|
|
|
public static function getSiteLanguage(int $siteId = null): string |
|
174
|
|
|
{ |
|
175
|
|
|
if ($siteId === null) { |
|
176
|
|
|
try { |
|
177
|
|
|
$siteId = Craft::$app->getSites()->getCurrentSite()->id; |
|
178
|
|
|
} catch (SiteNotFoundException $e) { |
|
179
|
|
|
$siteId = 1; |
|
180
|
|
|
Craft::error($e->getMessage(), __METHOD__); |
|
181
|
|
|
} |
|
182
|
|
|
} |
|
183
|
|
|
$site = Craft::$app->getSites()->getSiteById($siteId); |
|
184
|
|
|
if ($site) { |
|
185
|
|
|
$language = $site->language; |
|
186
|
|
|
} else { |
|
187
|
|
|
$language = Craft::$app->language; |
|
188
|
|
|
} |
|
189
|
|
|
$language = strtolower($language); |
|
190
|
|
|
$language = str_replace('_', '-', $language); |
|
191
|
|
|
|
|
192
|
|
|
return $language; |
|
193
|
|
|
} |
|
194
|
|
|
|
|
195
|
|
|
/** |
|
196
|
|
|
* Cache frequently accessed properties locally |
|
197
|
|
|
*/ |
|
198
|
|
|
public static function cache() |
|
199
|
|
|
{ |
|
200
|
|
|
self::$templateObjectVars = [ |
|
201
|
|
|
'seomatic' => Seomatic::$seomaticVariable, |
|
202
|
|
|
]; |
|
203
|
|
|
|
|
204
|
|
|
$element = Seomatic::$matchedElement; |
|
205
|
|
|
/** @var Element $element */ |
|
206
|
|
|
if ($element !== null) { |
|
207
|
|
|
$refHandle = null; |
|
208
|
|
|
// Get a fallback from the element's root class name |
|
209
|
|
|
try { |
|
210
|
|
|
$reflector = new ReflectionClass($element); |
|
211
|
|
|
} catch (ReflectionException $e) { |
|
212
|
|
|
$reflector = null; |
|
213
|
|
|
Craft::error($e->getMessage(), __METHOD__); |
|
214
|
|
|
} |
|
215
|
|
|
if ($reflector) { |
|
216
|
|
|
$refHandle = strtolower($reflector->getShortName()); |
|
217
|
|
|
} |
|
218
|
|
|
$elementRefHandle = $element::refHandle(); |
|
219
|
|
|
// Use the SeoElement interface to get the refHandle |
|
220
|
|
|
$metaBundleSourceType = Seomatic::$plugin->seoElements->getMetaBundleTypeFromElement($element); |
|
221
|
|
|
$seoElement = Seomatic::$plugin->seoElements->getSeoElementByMetaBundleType($metaBundleSourceType); |
|
222
|
|
|
if ($seoElement) { |
|
223
|
|
|
$elementRefHandle = $seoElement::getElementRefHandle(); |
|
224
|
|
|
} |
|
225
|
|
|
// Prefer $element::refHandle() |
|
226
|
|
|
$matchedElementType = $elementRefHandle ?? $refHandle ?? 'entry'; |
|
227
|
|
|
if ($matchedElementType) { |
|
228
|
|
|
self::$templateObjectVars[$matchedElementType] = $element; |
|
229
|
|
|
self::$templatePreviewVars[$matchedElementType] = $element; |
|
230
|
|
|
} |
|
231
|
|
|
} |
|
232
|
|
|
self::$templatePreviewVars['object'] = self::$templateObjectVars; |
|
233
|
|
|
self::$templatePreviewVars['seomatic'] = Seomatic::$seomaticVariable; |
|
234
|
|
|
|
|
235
|
|
|
self::$view = Seomatic::$view; |
|
236
|
|
|
} |
|
237
|
|
|
|
|
238
|
|
|
// Protected Methods |
|
239
|
|
|
// ========================================================================= |
|
240
|
|
|
|
|
241
|
|
|
/** |
|
242
|
|
|
* @param string|Asset $metaValue |
|
243
|
|
|
* @param bool $resolveAliases Whether @ aliases should be resolved |
|
244
|
|
|
* in this string |
|
245
|
|
|
* @param bool $parseAsTwig Whether items should be parsed as a |
|
246
|
|
|
* Twig template in this string |
|
247
|
|
|
* |
|
248
|
|
|
* @return null|string |
|
249
|
|
|
*/ |
|
250
|
1 |
|
protected static function parseMetaString($metaValue, bool $resolveAliases = true, bool $parseAsTwig = true) |
|
251
|
|
|
{ |
|
252
|
|
|
// Handle being passed in a string |
|
253
|
1 |
|
if (is_string($metaValue)) { |
|
254
|
1 |
|
if ($resolveAliases) { |
|
255
|
|
|
// Resolve it as an alias |
|
256
|
|
|
try { |
|
257
|
1 |
|
$alias = Craft::parseEnv($metaValue); |
|
|
|
|
|
|
258
|
|
|
} catch (\Exception $e) { |
|
259
|
|
|
$alias = false; |
|
260
|
|
|
} |
|
261
|
1 |
|
if (is_string($alias)) { |
|
262
|
1 |
|
$metaValue = $alias; |
|
263
|
|
|
} |
|
264
|
|
|
} |
|
265
|
|
|
// Ensure we aren't passed in an absurdly large object template to parse |
|
266
|
1 |
|
if (strlen($metaValue) > self::MAX_TEMPLATE_LENGTH) { |
|
267
|
|
|
$metaValue = mb_substr($metaValue, 0, self::MAX_TEMPLATE_LENGTH); |
|
268
|
|
|
} |
|
269
|
|
|
// If there are no dynamic tags, just return the template |
|
270
|
1 |
|
if (!$parseAsTwig || !str_contains($metaValue, '{')) { |
|
271
|
1 |
|
return trim(html_entity_decode($metaValue, ENT_NOQUOTES, 'UTF-8')); |
|
272
|
|
|
} |
|
273
|
|
|
$oldTemplateMode = self::$view->getTemplateMode(); |
|
274
|
|
|
try { |
|
275
|
|
|
// Render in site template mode so that we get globals injected |
|
276
|
|
|
if ($oldTemplateMode !== self::$view::TEMPLATE_MODE_SITE) { |
|
277
|
|
|
self::$view->setTemplateMode(self::$view::TEMPLATE_MODE_SITE); |
|
278
|
|
|
} |
|
279
|
|
|
// Render the template out |
|
280
|
|
|
$metaValue = trim(html_entity_decode( |
|
281
|
|
|
self::$view->renderObjectTemplate($metaValue, self::$templateObjectVars, self::$templatePreviewVars), |
|
282
|
|
|
ENT_NOQUOTES, |
|
283
|
|
|
'UTF-8' |
|
284
|
|
|
)); |
|
285
|
|
|
// Restore the template mode |
|
286
|
|
|
if ($oldTemplateMode !== self::$view::TEMPLATE_MODE_SITE) { |
|
287
|
|
|
self::$view->setTemplateMode($oldTemplateMode); |
|
288
|
|
|
} |
|
289
|
|
|
} catch (Throwable $e) { |
|
290
|
|
|
$metaValue = Craft::t( |
|
291
|
|
|
'seomatic', |
|
292
|
|
|
'Error rendering `{template}` -> {error}', |
|
293
|
|
|
['template' => $metaValue, 'error' => $e->getMessage() . ' - ' . print_r($metaValue, true)] |
|
|
|
|
|
|
294
|
|
|
); |
|
295
|
|
|
Craft::error($metaValue, __METHOD__); |
|
296
|
|
|
Craft::$app->getErrorHandler()->logException($e); |
|
297
|
|
|
// Restore the template mode |
|
298
|
|
|
if ($oldTemplateMode !== self::$view::TEMPLATE_MODE_SITE) { |
|
299
|
|
|
try { |
|
300
|
|
|
self::$view->setTemplateMode($oldTemplateMode); |
|
301
|
|
|
} catch (Exception $e) { |
|
302
|
|
|
Craft::error($e->getMessage(), __METHOD__); |
|
303
|
|
|
} |
|
304
|
|
|
} |
|
305
|
|
|
|
|
306
|
|
|
return null; |
|
307
|
|
|
} |
|
308
|
|
|
} |
|
309
|
|
|
// Handle being passed in an object |
|
310
|
|
|
if (is_object($metaValue)) { |
|
311
|
|
|
if ($metaValue instanceof Markup) { |
|
312
|
|
|
return trim(html_entity_decode((string)$metaValue, ENT_NOQUOTES, 'UTF-8')); |
|
313
|
|
|
} |
|
314
|
|
|
if ($metaValue instanceof Asset) { |
|
|
|
|
|
|
315
|
|
|
/** @var Asset $metaValue */ |
|
316
|
|
|
return $metaValue->uri; |
|
317
|
|
|
} |
|
318
|
|
|
} |
|
319
|
|
|
|
|
320
|
|
|
return $metaValue; |
|
321
|
|
|
} |
|
322
|
|
|
} |
|
323
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.