Passed
Push — develop-v4 ( 07a1f5...3af9d3 )
by Andrew
09:56 queued 13s
created

MetaValue::cache()   A

Complexity

Conditions 5
Paths 9

Size

Total Lines 34
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
eloc 21
c 0
b 0
f 0
dl 0
loc 34
ccs 0
cts 22
cp 0
rs 9.2728
cc 5
nc 9
nop 0
crap 30
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
            // Use the SeoElement interface to get the refHandle
219
            $metaBundleSourceType = Seomatic::$plugin->seoElements->getMetaBundleTypeFromElement($element);
220
            $seoElement = Seomatic::$plugin->seoElements->getSeoElementByMetaBundleType($metaBundleSourceType);
221
            // Prefer $element::refHandle()
222
            $matchedElementType = $seoElement::getElementRefHandle() ?? $refHandle ?? 'entry';
223
            if ($matchedElementType) {
224
                self::$templateObjectVars[$matchedElementType] = $element;
225
                self::$templatePreviewVars[$matchedElementType] = $element;
226
            }
227
        }
228
        self::$templatePreviewVars['object'] = self::$templateObjectVars;
229
        self::$templatePreviewVars['seomatic'] = Seomatic::$seomaticVariable;
230
231
        self::$view = Seomatic::$view;
232
    }
233
234
    // Protected Methods
235
    // =========================================================================
236
237
    /**
238
     * @param string|Asset $metaValue
239
     * @param bool $resolveAliases Whether @ aliases should be resolved
240
     *                                     in this string
241
     * @param bool $parseAsTwig Whether items should be parsed as a
242
     *                                     Twig template in this string
243
     *
244
     * @return null|string
245
     */
246 1
    protected static function parseMetaString($metaValue, bool $resolveAliases = true, bool $parseAsTwig = true)
247
    {
248
        // Handle being passed in a string
249 1
        if (is_string($metaValue)) {
250 1
            if ($resolveAliases) {
251
                // Resolve it as an alias
252
                try {
253 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

253
                    $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...
254
                } catch (\Exception $e) {
255
                    $alias = false;
256
                }
257 1
                if (is_string($alias)) {
258 1
                    $metaValue = $alias;
259
                }
260
            }
261
            // Ensure we aren't passed in an absurdly large object template to parse
262 1
            if (strlen($metaValue) > self::MAX_TEMPLATE_LENGTH) {
263
                $metaValue = mb_substr($metaValue, 0, self::MAX_TEMPLATE_LENGTH);
264
            }
265
            // If there are no dynamic tags, just return the template
266 1
            if (!$parseAsTwig || !str_contains($metaValue, '{')) {
267 1
                return trim(html_entity_decode($metaValue, ENT_NOQUOTES, 'UTF-8'));
268
            }
269
            $oldTemplateMode = self::$view->getTemplateMode();
270
            try {
271
                // Render in site template mode so that we get globals injected
272
                if ($oldTemplateMode !== self::$view::TEMPLATE_MODE_SITE) {
273
                    self::$view->setTemplateMode(self::$view::TEMPLATE_MODE_SITE);
274
                }
275
                // Render the template out
276
                $metaValue = trim(html_entity_decode(
277
                    self::$view->renderObjectTemplate($metaValue, self::$templateObjectVars, self::$templatePreviewVars),
278
                    ENT_NOQUOTES,
279
                    'UTF-8'
280
                ));
281
                // Restore the template mode
282
                if ($oldTemplateMode !== self::$view::TEMPLATE_MODE_SITE) {
283
                    self::$view->setTemplateMode($oldTemplateMode);
284
                }
285
            } catch (Throwable $e) {
286
                $metaValue = Craft::t(
287
                    'seomatic',
288
                    'Error rendering `{template}` -> {error}',
289
                    ['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

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