Passed
Push — develop-v4 ( 2d4983...bb740f )
by Andrew
23:04
created

MetaValue::cache()   B

Complexity

Conditions 6
Paths 17

Size

Total Lines 38
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 24
dl 0
loc 38
ccs 0
cts 25
cp 0
rs 8.9137
c 1
b 0
f 0
cc 6
nc 17
nop 0
crap 42
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
     * @param bool $recursive Whether to recursively parse the array
117
     */
118
    public static function parseArray(array &$metaArray, bool $resolveAliases = true, bool $parseAsTwig = true, bool $recursive = false)
119
    {
120
        // Do this here as well so that parseString() won't potentially be constantly switching modes
121
        // while parsing through the array
122
        $oldTemplateMode = self::$view->getTemplateMode();
123
        // Render in site template mode so that we get globals injected
124
        if ($oldTemplateMode !== self::$view::TEMPLATE_MODE_SITE) {
125
            try {
126
                self::$view->setTemplateMode(self::$view::TEMPLATE_MODE_SITE);
127
            } catch (Exception $e) {
128
                Craft::error($e->getMessage(), __METHOD__);
129
            }
130
        }
131
        foreach ($metaArray as $key => $value) {
132
            if ($recursive && is_array($value)) {
133
                self::parseArray($value, $resolveAliases, $parseAsTwig, $recursive);
134
            }
135
            $shouldParse = $parseAsTwig;
136
            $shouldAlias = $resolveAliases;
137
            $tries = self::MAX_PARSE_TRIES;
138
            if (in_array($key, self::NO_ALIASES, true)) {
139
                $shouldAlias = false;
140
            }
141
            if (in_array($key, self::NO_PARSING, true)) {
142
                $shouldParse = false;
143
            }
144
            if (in_array($key, self::PARSE_ONCE, true)) {
145
                $tries = 1;
146
                if (is_string($value) && $value[0] !== '{') {
147
                    $shouldParse = false;
148
                }
149
            }
150
            if ($value !== null) {
151
                $metaArray[$key] = self::parseString($value, $shouldAlias, $shouldParse, $tries);
152
            }
153
        }
154
        // Restore the template mode
155
        if ($oldTemplateMode !== self::$view::TEMPLATE_MODE_SITE) {
156
            try {
157
                self::$view->setTemplateMode($oldTemplateMode);
158
            } catch (Exception $e) {
159
                Craft::error($e->getMessage(), __METHOD__);
160
            }
161
        }
162
163
        // Remove any empty values
164
        $metaArray = array_filter(
165
            $metaArray,
166
            [ArrayHelper::class, 'preserveNumerics']
167
        );
168
    }
169
170
    /**
171
     * Get the language from a siteId
172
     *
173
     * @param null|int $siteId
174
     *
175
     * @return string
176
     */
177
    public static function getSiteLanguage(int $siteId = null): string
178
    {
179
        if ($siteId === null) {
180
            try {
181
                $siteId = Craft::$app->getSites()->getCurrentSite()->id;
182
            } catch (SiteNotFoundException $e) {
183
                $siteId = 1;
184
                Craft::error($e->getMessage(), __METHOD__);
185
            }
186
        }
187
        $site = Craft::$app->getSites()->getSiteById($siteId);
188
        if ($site) {
189
            $language = $site->language;
190
        } else {
191
            $language = Craft::$app->language;
192
        }
193
        $language = strtolower($language);
194
        $language = str_replace('_', '-', $language);
195
196
        return $language;
197
    }
198
199
    /**
200
     * Cache frequently accessed properties locally
201
     */
202
    public static function cache()
203
    {
204
        self::$templateObjectVars = [
205
            'seomatic' => Seomatic::$seomaticVariable,
206
        ];
207
208
        $element = Seomatic::$matchedElement;
209
        /** @var Element $element */
210
        if ($element !== null) {
211
            $refHandle = null;
212
            // Get a fallback from the element's root class name
213
            try {
214
                $reflector = new ReflectionClass($element);
215
            } catch (ReflectionException $e) {
216
                $reflector = null;
217
                Craft::error($e->getMessage(), __METHOD__);
218
            }
219
            if ($reflector) {
220
                $refHandle = strtolower($reflector->getShortName());
221
            }
222
            $elementRefHandle = $element::refHandle();
223
            // Use the SeoElement interface to get the refHandle
224
            $metaBundleSourceType = Seomatic::$plugin->seoElements->getMetaBundleTypeFromElement($element);
225
            $seoElement = Seomatic::$plugin->seoElements->getSeoElementByMetaBundleType($metaBundleSourceType);
226
            if ($seoElement) {
227
                $elementRefHandle = $seoElement::getElementRefHandle();
228
            }
229
            // Prefer $element::refHandle()
230
            $matchedElementType = $elementRefHandle ?? $refHandle ?? 'entry';
231
            if ($matchedElementType) {
232
                self::$templateObjectVars[$matchedElementType] = $element;
233
                self::$templatePreviewVars[$matchedElementType] = $element;
234
            }
235
        }
236
        self::$templatePreviewVars['object'] = self::$templateObjectVars;
237
        self::$templatePreviewVars['seomatic'] = Seomatic::$seomaticVariable;
238
239
        self::$view = Seomatic::$view;
240
    }
241
242
    // Protected Methods
243
    // =========================================================================
244
245
    /**
246
     * @param string|Asset $metaValue
247
     * @param bool $resolveAliases Whether @ aliases should be resolved
248
     *                                     in this string
249
     * @param bool $parseAsTwig Whether items should be parsed as a
250
     *                                     Twig template in this string
251
     *
252
     * @return null|string
253
     */
254 1
    protected static function parseMetaString($metaValue, bool $resolveAliases = true, bool $parseAsTwig = true)
255
    {
256
        // Handle being passed in a string
257 1
        if (is_string($metaValue)) {
258 1
            if ($resolveAliases) {
259
                // Resolve it as an alias
260
                try {
261 1
                    $alias = Craft::parseEnv($metaValue);
0 ignored issues
show
Deprecated Code introduced by
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 ignore-deprecated  annotation

261
                    $alias = /** @scrutinizer ignore-deprecated */ Craft::parseEnv($metaValue);

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.

Loading history...
262
                } catch (\Exception $e) {
263
                    $alias = false;
264
                }
265 1
                if (is_string($alias)) {
266 1
                    $metaValue = $alias;
267
                }
268
            }
269
            // Ensure we aren't passed in an absurdly large object template to parse
270 1
            if (strlen($metaValue) > self::MAX_TEMPLATE_LENGTH) {
271
                $metaValue = mb_substr($metaValue, 0, self::MAX_TEMPLATE_LENGTH);
272
            }
273
            // If there are no dynamic tags, just return the template
274 1
            if (!$parseAsTwig || !str_contains($metaValue, '{')) {
275 1
                return trim(html_entity_decode($metaValue, ENT_NOQUOTES, 'UTF-8'));
276
            }
277
            $oldTemplateMode = self::$view->getTemplateMode();
278
            try {
279
                // Render in site template mode so that we get globals injected
280
                if ($oldTemplateMode !== self::$view::TEMPLATE_MODE_SITE) {
281
                    self::$view->setTemplateMode(self::$view::TEMPLATE_MODE_SITE);
282
                }
283
                // Render the template out
284
                $metaValue = trim(html_entity_decode(
285
                    self::$view->renderObjectTemplate($metaValue, self::$templateObjectVars, self::$templatePreviewVars),
286
                    ENT_NOQUOTES,
287
                    'UTF-8'
288
                ));
289
                // Restore the template mode
290
                if ($oldTemplateMode !== self::$view::TEMPLATE_MODE_SITE) {
291
                    self::$view->setTemplateMode($oldTemplateMode);
292
                }
293
            } catch (Throwable $e) {
294
                $metaValue = Craft::t(
295
                    'seomatic',
296
                    'Error rendering `{template}` -> {error}',
297
                    ['template' => $metaValue, 'error' => $e->getMessage() . ' - ' . print_r($metaValue, true)]
0 ignored issues
show
Bug introduced by
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 ignore-type  annotation

297
                    ['template' => $metaValue, 'error' => $e->getMessage() . ' - ' . /** @scrutinizer ignore-type */ print_r($metaValue, true)]
Loading history...
298
                );
299
                Craft::error($metaValue, __METHOD__);
300
                Craft::$app->getErrorHandler()->logException($e);
301
                // Restore the template mode
302
                if ($oldTemplateMode !== self::$view::TEMPLATE_MODE_SITE) {
303
                    try {
304
                        self::$view->setTemplateMode($oldTemplateMode);
305
                    } catch (Exception $e) {
306
                        Craft::error($e->getMessage(), __METHOD__);
307
                    }
308
                }
309
310
                return null;
311
            }
312
        }
313
        // Handle being passed in an object
314
        if (is_object($metaValue)) {
315
            if ($metaValue instanceof Markup) {
316
                return trim(html_entity_decode((string)$metaValue, ENT_NOQUOTES, 'UTF-8'));
317
            }
318
            if ($metaValue instanceof Asset) {
0 ignored issues
show
introduced by
$metaValue is always a sub-type of craft\elements\Asset.
Loading history...
319
                /** @var Asset $metaValue */
320
                return $metaValue->uri;
321
            }
322
        }
323
324
        return $metaValue;
325
    }
326
}
327