Passed
Push — develop ( db1623...3b1851 )
by Andrew
12:14
created

MetaValue::internalRenderObjectTemplate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 6
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 12
ccs 0
cts 6
cp 0
crap 2
rs 10
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
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @package tag
Loading history...
Coding Style introduced by
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
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
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
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
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
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
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
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 */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
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';
0 ignored issues
show
Bug introduced by
Are you sure the usage of $element::refHandle() targeting craft\base\Element::refHandle() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
217
            if ($matchedElementType) {
218
                self::$templateObjectVars[$matchedElementType] = $element;
219
                self::$templatePreviewVars[$matchedElementType] = $element;
220
            }
221
        }
222
        self::$templatePreviewVars['object'] = self::$templateObjectVars;
223
        self::$templatePreviewVars['seomatic'] = Seomatic::$seomaticVariable;
224
225
        self::$view = Seomatic::$view;
226
    }
227
228
    // Protected Methods
229
    // =========================================================================
230
231
    /**
232
     * @param string|Asset $metaValue
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
233
     * @param bool         $resolveAliases Whether @ aliases should be resolved
234
     *                                     in this string
235
     * @param bool         $parseAsTwig    Whether items should be parsed as a
236
     *                                     Twig template in this string
237
     *
238
     * @return null|string
239
     */
240 1
    protected static function parseMetaString($metaValue, bool $resolveAliases = true, bool $parseAsTwig = true)
241
    {
242
        // Handle being passed in a string
243 1
        if (\is_string($metaValue)) {
244 1
            if ($resolveAliases) {
245
                // Resolve it as an alias
246 1
                if (Seomatic::$craft31) {
247
                    try {
248 1
                        $alias = Craft::parseEnv($metaValue);
249
                    } catch (\Exception $e) {
250 1
                        $alias = false;
251
                    }
252
                } else {
253
                    try {
254
                        $alias = Craft::getAlias($metaValue, false);
255
                    } catch (\Exception $e) {
256
                        $alias = false;
257
                    }
258
                }
259 1
                if (\is_string($alias)) {
260 1
                    $metaValue = $alias;
261
                }
262
            }
263
            // Ensure we aren't passed in an absurdly large object template to parse
264 1
            if (strlen($metaValue) > self::MAX_TEMPLATE_LENGTH) {
265
                $metaValue = mb_substr($metaValue, 0, self::MAX_TEMPLATE_LENGTH);
266
            }
267
            // If there are no dynamic tags, just return the template
268 1
            if (!$parseAsTwig || !StringHelper::contains($metaValue, '{')) {
269 1
                return trim(html_entity_decode($metaValue, ENT_NOQUOTES, 'UTF-8'));
270
            }
271
            $oldTemplateMode = self::$view->getTemplateMode();
272
            try {
273
                // Render in site template mode so that we get globals injected
274
                if ($oldTemplateMode !== self::$view::TEMPLATE_MODE_SITE) {
275
                    self::$view->setTemplateMode(self::$view::TEMPLATE_MODE_SITE);
276
                }
277
                // Render the template out
278
                $metaValue = trim(html_entity_decode(
279
                    self::internalRenderObjectTemplate($metaValue, self::$templatePreviewVars),
280
                    ENT_NOQUOTES,
281
                    'UTF-8'
282
                ));
283
                // Restore the template mode
284
                if ($oldTemplateMode !== self::$view::TEMPLATE_MODE_SITE) {
285
                    self::$view->setTemplateMode($oldTemplateMode);
286
                }
287
            } catch (\Throwable $e) {
288
                $metaValue = Craft::t(
289
                    'seomatic',
290
                    'Error rendering `{template}` -> {error}',
291
                    ['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

291
                    ['template' => $metaValue, 'error' => $e->getMessage().' - './** @scrutinizer ignore-type */ print_r($metaValue, true)]
Loading history...
292
                );
293
                Craft::error($metaValue, __METHOD__);
294
                Craft::$app->getErrorHandler()->logException($e);
295
                // Restore the template mode
296
                if ($oldTemplateMode !== self::$view::TEMPLATE_MODE_SITE) {
297
                    try {
298
                        self::$view->setTemplateMode($oldTemplateMode);
299
                    } catch (Exception $e) {
300
                        Craft::error($e->getMessage(), __METHOD__);
301
                    }
302
                }
303
304
                return null;
305
            }
306
        }
307
        // Handle being passed in an object
308
        if (\is_object($metaValue)) {
309
            if ($metaValue instanceof Markup) {
0 ignored issues
show
introduced by
$metaValue is never a sub-type of Twig\Markup.
Loading history...
310
                return trim(html_entity_decode((string)$metaValue, ENT_NOQUOTES, 'UTF-8'));
311
            }
312
            if ($metaValue instanceof Asset) {
0 ignored issues
show
introduced by
$metaValue is always a sub-type of craft\elements\Asset.
Loading history...
313
                /** @var Asset $metaValue */
0 ignored issues
show
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
314
                return $metaValue->uri;
315
            }
316
        }
317
318
        return $metaValue;
319
    }
320
321
    /**
322
     * Replacement for self::$view->renderObjectTemplate that just handles changing { woof } into {{ woof }}
323
     *
324
     * @param string $template
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
325
     * @param array $variables
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 2 spaces after parameter type; 1 found
Loading history...
326
     * @return string
0 ignored issues
show
Coding Style introduced by
Tag @return cannot be grouped with parameter tags in a doc comment
Loading history...
327
     * @throws \Twig\Error\LoaderError
0 ignored issues
show
Coding Style introduced by
Tag @throws cannot be grouped with parameter tags in a doc comment
Loading history...
328
     * @throws \Twig\Error\SyntaxError
0 ignored issues
show
Coding Style introduced by
Tag @throws cannot be grouped with parameter tags in a doc comment
Loading history...
329
     * @throws \yii\base\ExitException
0 ignored issues
show
Coding Style introduced by
Tag @throws cannot be grouped with parameter tags in a doc comment
Loading history...
330
     */
331
    protected static function internalRenderObjectTemplate(string $template, array $variables = []): string
332
    {
333
        // Swap out the remaining {xyz} tags with {{xyz}}
334
        $template = preg_replace_callback('/(?<!\{)\{\s*(\w+)([^\{]*?)\}/', function(array $match) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FUNCTION keyword; 0 found
Loading history...
335
            $replace = $match[1] . $match[2];
336
            return "{{ $replace|raw }}";
337
        }, $template);
338
339
340
        $result = self::$view->renderString($template, $variables);
341
342
        return $result;
343
    }
344
}
345