Passed
Push — v3 ( 1e51d3...178405 )
by Andrew
25:46
created

src/helpers/MetaValue.php (2 issues)

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 nystudio107\seomatic\Seomatic;
15
16
use Craft;
17
use craft\base\Element;
18
use craft\elements\Asset;
19
use craft\errors\SiteNotFoundException;
20
use craft\helpers\StringHelper;
21
use craft\web\View;
22
23
use Twig\Markup;
24
25
use yii\base\Exception;
26
27
/**
28
 * @author    nystudio107
29
 * @package   Seomatic
30
 * @since     3.0.0
31
 */
32
class MetaValue
33
{
34
    // Constants
35
    // =========================================================================
36
37
    const MAX_TEMPLATE_LENGTH = 4096;
38
    const MAX_PARSE_TRIES = 5;
39
    // Semicolon because that is the resolved config key when rendering tags,
40
    // kebab-case because that is the config keys as defined in the config files.
41
    const NO_ALIASES = [
42
        'twitter:site',
43
        'twitter:creator',
44
        'twitterSite',
45
        'twitterCreator',
46
    ];
47
    const NO_PARSING = [
48
        'siteLinksSearchTarget',
49
    ];
50
    const PARSE_ONCE = [
51
        'target',
52
        'urlTemplate',
53
    ];
54
55
    // Static Properties
56
    // =========================================================================
57
58
    /**
59
     * @var array
60
     */
61
    public static $templateObjectVars;
62
63
    /**
64
     * @var array
65
     */
66
    public static $templatePreviewVars = [];
67
68
    /**
69
     * @var View
70
     */
71
    public static $view;
72
73
    // Static Methods
74
    // =========================================================================
75
76
    /**
77
     * @param string $metaValue
78
     * @param bool   $resolveAliases Whether @ aliases should be resolved in
79
     *                               this string
80
     * @param bool   $parseAsTwig    Whether items should be parsed as a Twig
81
     *                               template in this string
82
     * @param int    $tries          The number of times to parse the string
83
     *
84
     * @return string
85
     */
86 1
    public static function parseString(
87
        $metaValue,
88
        bool $resolveAliases = true,
89
        bool $parseAsTwig = true,
90
        $tries = self::MAX_PARSE_TRIES
91
    ) {
92
        // If it's a string, and there are no dynamic tags, just return the template
93 1
        if (\is_string($metaValue) && !StringHelper::contains($metaValue, '{')) {
94 1
            return self::parseMetaString($metaValue, $resolveAliases, $parseAsTwig) ?? $metaValue;
95
        }
96
        // Parse it repeatedly until it doesn't change
97
        $value = '';
98
        while ($metaValue !== $value && $tries) {
99
            $tries--;
100
            $value = $metaValue;
101
            $metaValue = self::parseMetaString($value, $resolveAliases, $parseAsTwig) ?? $metaValue;
102
        }
103
104
        return $metaValue;
105
    }
106
107
    /**
108
     * @param array $metaArray
109
     * @param bool  $resolveAliases Whether @ aliases should be resolved in
110
     *                              this array
111
     * @param bool  $parseAsTwig    Whether items should be parsed as a Twig
112
     *                              template in this array
113
     */
114
    public static function parseArray(array &$metaArray, bool $resolveAliases = true, bool $parseAsTwig = true)
115
    {
116
        // Do this here as well so that parseString() won't potentially be constantly switching modes
117
        // while parsing through the array
118
        $oldTemplateMode = self::$view->getTemplateMode();
119
        // Render in site template mode so that we get globals injected
120
        if ($oldTemplateMode !== self::$view::TEMPLATE_MODE_SITE) {
121
            try {
122
                self::$view->setTemplateMode(self::$view::TEMPLATE_MODE_SITE);
123
            } catch (Exception $e) {
124
                Craft::error($e->getMessage(), __METHOD__);
125
            }
126
        }
127
        foreach ($metaArray as $key => $value) {
128
            $shouldParse = $parseAsTwig;
129
            $shouldAlias = $resolveAliases;
130
            $tries = self::MAX_PARSE_TRIES;
131
            if (\in_array($key, self::NO_ALIASES, true)) {
132
                $shouldAlias = false;
133
            }
134
            if (\in_array($key, self::NO_PARSING, true)) {
135
                $shouldParse = false;
136
            }
137
            if (\in_array($key, self::PARSE_ONCE, true)) {
138
                $tries = 1;
139
                if (is_string($value) && $value[0] !== '{') {
140
                    $shouldParse = false;
141
                }
142
            }
143
            if ($value !== null) {
144
                $metaArray[$key] = self::parseString($value, $shouldAlias, $shouldParse, $tries);
145
            }
146
        }
147
        // Restore the template mode
148
        if ($oldTemplateMode !== self::$view::TEMPLATE_MODE_SITE) {
149
            try {
150
                self::$view->setTemplateMode($oldTemplateMode);
151
            } catch (Exception $e) {
152
                Craft::error($e->getMessage(), __METHOD__);
153
            }
154
        }
155
156
        // Remove any empty values
157
        $metaArray = array_filter(
158
            $metaArray,
159
            [ArrayHelper::class, 'preserveNumerics']
160
        );
161
    }
162
163
    /**
164
     * Get the language from a siteId
165
     *
166
     * @param null|int $siteId
167
     *
168
     * @return string
169
     */
170
    public static function getSiteLanguage(int $siteId = null): string
171
    {
172
        if ($siteId === null) {
173
            try {
174
                $siteId = Craft::$app->getSites()->getCurrentSite()->id;
175
            } catch (SiteNotFoundException $e) {
176
                $siteId = 1;
177
                Craft::error($e->getMessage(), __METHOD__);
178
            }
179
        }
180
        $site = Craft::$app->getSites()->getSiteById($siteId);
181
        if ($site) {
182
            $language = $site->language;
183
        } else {
184
            $language = Craft::$app->language;
185
        }
186
        $language = strtolower($language);
187
        $language = str_replace('_', '-', $language);
188
189
        return $language;
190
    }
191
192
    /**
193
     * Cache frequently accessed properties locally
194
     */
195
    public static function cache()
196
    {
197
        self::$templateObjectVars = [
198
            'seomatic' => Seomatic::$seomaticVariable,
199
        ];
200
201
        $element = Seomatic::$matchedElement;
202
        /** @var Element $element */
203
        if ($element !== null) {
204
            $refHandle = null;
205
            // Get a fallback from the element's root class name
206
            try {
207
                $reflector = new \ReflectionClass($element);
208
            } catch (\ReflectionException $e) {
209
                $reflector = null;
210
                Craft::error($e->getMessage(), __METHOD__);
211
            }
212
            if ($reflector) {
213
                $refHandle = strtolower($reflector->getShortName());
214
            }
215
            // Prefer $element::refHandle()
216
            $matchedElementType = $element::refHandle() ?? $refHandle ?? 'entry';
217
            if ($matchedElementType) {
218
                self::$templateObjectVars[$matchedElementType] = $element;
219
            }
220
        }
221
222
        self::$view = Seomatic::$view;
223
    }
224
225
    // Protected Methods
226
    // =========================================================================
227
228
    /**
229
     * @param string|Asset $metaValue
230
     * @param bool         $resolveAliases Whether @ aliases should be resolved
231
     *                                     in this string
232
     * @param bool         $parseAsTwig    Whether items should be parsed as a
233
     *                                     Twig template in this string
234
     *
235
     * @return null|string
236
     */
237 1
    protected static function parseMetaString($metaValue, bool $resolveAliases = true, bool $parseAsTwig = true)
238
    {
239
        // Handle being passed in a string
240 1
        if (\is_string($metaValue)) {
241 1
            if ($resolveAliases) {
242
                // Resolve it as an alias
243 1
                if (Seomatic::$craft31) {
244
                    try {
245 1
                        $alias = Craft::parseEnv($metaValue);
246
                    } catch (\Exception $e) {
247 1
                        $alias = false;
248
                    }
249
                } else {
250
                    try {
251
                        $alias = Craft::getAlias($metaValue, false);
252
                    } catch (\Exception $e) {
253
                        $alias = false;
254
                    }
255
                }
256 1
                if (\is_string($alias)) {
257 1
                    $metaValue = $alias;
258
                }
259
            }
260
            // Ensure we aren't passed in an absurdly large object template to parse
261 1
            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 1
            if (!$parseAsTwig || !StringHelper::contains($metaValue, '{')) {
266 1
                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)]
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) {
0 ignored issues
show
$metaValue is never a sub-type of Twig\Markup.
Loading history...
307
                return trim(html_entity_decode((string)$metaValue, ENT_NOQUOTES, 'UTF-8'));
308
            }
309
            if ($metaValue instanceof Asset) {
0 ignored issues
show
$metaValue is always a sub-type of craft\elements\Asset.
Loading history...
310
                /** @var Asset $metaValue */
311
                return $metaValue->uri;
312
            }
313
        }
314
315
        return $metaValue;
316
    }
317
}
318