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