Issues (257)

src/fields/SeoSettings.php (5 issues)

1
<?php
2
/**
3
 * SEOmatic plugin for Craft CMS
4
 *
5
 * @link      https://nystudio107.com/
6
 * @copyright Copyright (c) 2017 nystudio107
7
 * @license   https://nystudio107.com/license
8
 */
9
10
namespace nystudio107\seomatic\fields;
11
12
use Craft;
13
use craft\base\Element;
14
use craft\base\ElementInterface;
0 ignored issues
show
The type craft\base\ElementInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
use craft\base\Field;
16
use craft\base\PreviewableFieldInterface;
17
use craft\elements\Asset;
18
use craft\helpers\Json;
19
use craft\helpers\StringHelper;
20
use nystudio107\seomatic\assetbundles\seomatic\SeomaticAsset;
21
use nystudio107\seomatic\helpers\ArrayHelper;
22
use nystudio107\seomatic\helpers\Config as ConfigHelper;
23
use nystudio107\seomatic\helpers\Field as FieldHelper;
24
use nystudio107\seomatic\helpers\Migration as MigrationHelper;
25
use nystudio107\seomatic\helpers\PullField as PullFieldHelper;
26
use nystudio107\seomatic\helpers\Schema as SchemaHelper;
27
use nystudio107\seomatic\models\MetaBundle;
28
use nystudio107\seomatic\seoelements\SeoEntry;
29
use nystudio107\seomatic\Seomatic;
30
use nystudio107\seomatic\services\MetaContainers;
31
use ReflectionClass;
32
use yii\base\InvalidConfigException;
33
use yii\caching\TagDependency;
34
use yii\db\Schema;
35
use function in_array;
36
use function is_array;
37
use function is_object;
38
use function is_string;
39
40
/**
41
 * @author    nystudio107
42
 * @package   Seomatic
43
 * @since     3.0.0
44
 */
45
class SeoSettings extends Field implements PreviewableFieldInterface
46
{
47
    // Constants
48
    // =========================================================================
49
50
    public const CACHE_KEY = 'seomatic_fieldmeta_';
51
52
    public const BUNDLE_COMPARE_FIELDS = [
53
        'metaGlobalVars',
54
    ];
55
56
    // Public Properties
57
    // =========================================================================
58
59
    /**
60
     * @var string
61
     */
62
    public $elementDisplayPreviewType = 'google';
63
64
    /**
65
     * @var bool
66
     */
67
    public $generalTabEnabled = true;
68
69
    /**
70
     * @var array
71
     */
72
    public $generalEnabledFields = [
73
        'seoTitle',
74
        'seoDescription',
75
        'seoImage',
76
    ];
77
78
    /**
79
     * @var bool
80
     */
81
    public $twitterTabEnabled = false;
82
83
    /**
84
     * @var array
85
     */
86
    public $twitterEnabledFields = [];
87
88
    /**
89
     * @var bool
90
     */
91
    public $facebookTabEnabled = false;
92
93
    /**
94
     * @var array
95
     */
96
    public $facebookEnabledFields = [];
97
98
    /**
99
     * @var bool
100
     */
101
    public $sitemapTabEnabled = false;
102
103
    /**
104
     * @var array
105
     */
106
    public $sitemapEnabledFields = [];
107
108
    // Static Methods
109
    // =========================================================================
110
111
    /**
112
     * @inheritdoc
113
     */
114
    public static function dbType(): array|string|null
115
    {
116
        return Schema::TYPE_TEXT;
117
    }
118
119
    /**
120
     * @inheritdoc
121
     */
122
    public static function displayName(): string
123
    {
124
        return Craft::t('seomatic', 'SEO Settings');
125
    }
126
127
    /**
128
     * @inheritdoc
129
     */
130
    public static function icon(): string
131
    {
132
        return '@nystudio107/seomatic/icon-mask.svg';
133
    }
134
135
    /**
136
     * @inheritdoc
137
     */
138
    public static function phpType(): string
139
    {
140
        return sprintf('\\%s', MetaBundle::class);
141
    }
142
143
    // Public Methods
144
    // =========================================================================
145
146
    /**
147
     * @inheritdoc
148
     */
149
    public function rules(): array
150
    {
151
        $rules = parent::rules();
152
        $rules = array_merge($rules, [
153
            [
154
                [
155
                    'elementDisplayPreviewType',
156
                ],
157
                'string',
158
            ],
159
            [
160
                [
161
                    'generalTabEnabled',
162
                    'twitterTabEnabled',
163
                    'facebookTabEnabled',
164
                    'sitemapTabEnabled',
165
                ],
166
                'boolean',
167
            ],
168
            [
169
                [
170
                    'generalEnabledFields',
171
                    'twitterEnabledFields',
172
                    'facebookEnabledFields',
173
                    'sitemapEnabledFields',
174
                ],
175
                'each', 'rule' => ['string'],
176
            ],
177
178
        ]);
179
180
        return $rules;
181
    }
182
183
    /**
184
     * @inheritdoc
185
     * @since 2.0.0
186
     */
187
    public function useFieldset(): bool
188
    {
189
        return false;
190
    }
191
192
    /**
193
     * @inheritdoc
194
     */
195
    public function normalizeValue(mixed $value, ?ElementInterface $element = null): mixed
196
    {
197
        $config = [];
198
        // Handle incoming values potentially being JSON, an array, or an object
199
        if (!empty($value)) {
200
            if (is_string($value)) {
201
                // Decode any html entities
202
                $value = html_entity_decode($value, ENT_NOQUOTES, 'UTF-8');
203
                $config = Json::decodeIfJson($value);
204
            }
205
            if (is_array($value)) {
206
                $config = $value;
207
            }
208
            if (is_object($value) && $value instanceof MetaBundle) {
209
                $config = $value->toArray();
210
            }
211
        } else {
212
            /** @var null|Element $element */
213
            $config = MigrationHelper::configFromSeomaticMeta(
214
                $element,
215
                MigrationHelper::FIELD_MIGRATION_CONTEXT
216
            );
217
        }
218
        // If the config isn't empty, do some processing on the values
219
        if (!empty($config)) {
220
            $elementName = '';
221
            /** @var Element $element */
222
            if ($element !== null) {
223
                $reflector = new ReflectionClass($element);
224
                $elementName = strtolower($reflector->getShortName());
225
            }
226
            // Handle the pull fields
227
            if (!empty($config['metaGlobalVars']) && !empty($config['metaBundleSettings'])) {
228
                PullFieldHelper::parseTextSources(
229
                    $elementName,
230
                    $config['metaGlobalVars'],
231
                    $config['metaBundleSettings']
232
                );
233
                PullFieldHelper::parseImageSources(
234
                    $elementName,
235
                    $config['metaGlobalVars'],
236
                    $config['metaBundleSettings'],
237
                    null
238
                );
239
            }
240
            // Handle the mainEntityOfPage
241
            $mainEntity = '';
242
            if (in_array('mainEntityOfPage', (array)$this->generalEnabledFields, false) &&
243
                !empty($config['metaBundleSettings'])) {
244
                $mainEntity = SchemaHelper::getSpecificEntityType($config['metaBundleSettings'], true);
245
            }
246
            if (!empty($config['metaGlobalVars'])) {
247
                /* @var array $config */
248
                $config['metaGlobalVars']['mainEntityOfPage'] = $mainEntity;
249
            }
250
        }
251
        // Create a new meta bundle with propagated defaults
252
        $metaBundleDefaults = ArrayHelper::merge(
253
            ConfigHelper::getConfigFromFile('fieldmeta/Bundle'),
254
            $config
255
        );
256
257
        return MetaBundle::create($metaBundleDefaults);
258
    }
259
260
    /**
261
     * @inheritdoc
262
     */
263
    public function serializeValue(mixed $value, ?ElementInterface $element = null): mixed
264
    {
265
        $value = parent::serializeValue($value, $element);
266
        if (!Craft::$app->getDb()->getSupportsMb4()) {
267
            if (is_string($value)) {
268
                // Encode any 4-byte UTF-8 characters.
269
                $value = StringHelper::encodeMb4($value);
270
            }
271
            if (is_array($value)) {
272
                array_walk_recursive($value, function(&$arrayValue, $arrayKey) {
0 ignored issues
show
The parameter $arrayKey is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

272
                array_walk_recursive($value, function(&$arrayValue, /** @scrutinizer ignore-unused */ $arrayKey) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
273
                    if ($arrayValue !== null && is_string($arrayValue)) {
274
                        $arrayValue = StringHelper::encodeMb4($arrayValue);
275
                    }
276
                });
277
            }
278
        }
279
280
        return $value;
281
    }
282
283
    /**
284
     * @inheritdoc
285
     */
286
    public function getSettingsHtml(): ?string
287
    {
288
        $variables = [];
289
        $tagOptions = [
290
            'depends' => [
291
                'nystudio107\\seomatic\\assetbundles\\seomatic\\SeomaticAsset',
292
            ],
293
        ];
294
        // JS/CSS modules
295
        try {
296
            Seomatic::$view->registerAssetBundle(SeomaticAsset::class);
0 ignored issues
show
The method registerAssetBundle() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

296
            Seomatic::$view->/** @scrutinizer ignore-call */ 
297
                             registerAssetBundle(SeomaticAsset::class);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
297
            Seomatic::$plugin->vite->register('src/js/seomatic.js', false, $tagOptions, $tagOptions);
298
            Seomatic::$plugin->vite->register('src/js/seomatic-meta.js', false, $tagOptions, $tagOptions);
299
        } catch (InvalidConfigException $e) {
300
            Craft::error($e->getMessage(), __METHOD__);
301
        }
302
        // Asset bundle
303
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(
304
            '@nystudio107/seomatic/web/assets/dist',
305
            true
0 ignored issues
show
The call to yii\web\AssetManager::getPublishedUrl() has too many arguments starting with true. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

305
        /** @scrutinizer ignore-call */ 
306
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
306
        );
307
        $variables['field'] = $this;
308
309
        // Render the settings template
310
        return Craft::$app->getView()->renderTemplate(
311
            'seomatic/_components/fields/SeoSettings_settings',
312
            $variables
313
        );
314
    }
315
316
    /**
317
     * @inheritdoc
318
     */
319
    public function getInputHtml(mixed $value, ?ElementInterface $element = null): string
320
    {
321
        $variables = [];
322
        // JS/CSS modules
323
        $tagOptions = [
324
            'depends' => [
325
                'nystudio107\\seomatic\\assetbundles\\seomatic\\SeomaticAsset',
326
            ],
327
        ];
328
        // JS/CSS modules
329
        try {
330
            Seomatic::$view->registerAssetBundle(SeomaticAsset::class);
331
            Seomatic::$plugin->vite->register('src/js/seomatic.js', false, $tagOptions, $tagOptions);
332
            Seomatic::$plugin->vite->register('src/js/seomatic-meta.js', false, $tagOptions, $tagOptions);
333
        } catch (InvalidConfigException $e) {
334
            Craft::error($e->getMessage(), __METHOD__);
335
        }
336
        // Asset bundle
337
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(
338
            '@nystudio107/seomatic/web/assets/dist',
339
            true
0 ignored issues
show
The call to yii\web\AssetManager::getPublishedUrl() has too many arguments starting with true. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

339
        /** @scrutinizer ignore-call */ 
340
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
340
        );
341
        // Basic variables
342
        $variables['name'] = $this->handle;
343
        $variables['value'] = $value;
344
        $variables['field'] = $this;
345
        $variables['currentSourceBundleType'] = 'entry';
346
        $variables['entitySchemaPath'] = SchemaHelper::getEntityPath($value->metaBundleSettings);
347
348
        // Get our id and namespace
349
        $id = Craft::$app->getView()->formatInputId($this->handle);
350
        $nameSpacedId = Craft::$app->getView()->namespaceInputId($id);
351
        $variables['id'] = $id;
352
        $variables['nameSpacedId'] = $nameSpacedId;
353
354
        // Make sure the *Sources variables at least exist, for things like the QuickPost widget
355
        $variables['textFieldSources'] = [];
356
        $variables['assetFieldSources'] = [];
357
        $variables['assetVolumeTextFieldSources'] = [];
358
        $variables['userFieldSources'] = [];
359
        // Pull field sources
360
        if ($element !== null) {
361
            /** @var Element $element */
362
            $this->setContentFieldSourceVariables($element, 'Entry', $variables);
363
        }
364
365
        /** @var MetaBundle $value */
366
        $variables['elementType'] = Asset::class;
367
368
        $variables['parentBundles'] = [];
369
        // Preview the containers so the preview is correct in the field
370
        if ($element !== null && $element->uri !== null) {
371
            Seomatic::$plugin->metaContainers->previewMetaContainers($element->uri, $element->siteId, true, true, $element);
372
            $contentMeta = Seomatic::$plugin->metaBundles->getContentMetaBundleForElement($element);
373
            $globalMeta = Seomatic::$plugin->metaBundles->getGlobalMetaBundle($element->siteId);
374
            $variables['parentBundles'] = [$contentMeta, $globalMeta];
375
        }
376
377
        // Render the input template
378
        return Craft::$app->getView()->renderTemplate(
379
            'seomatic/_components/fields/SeoSettings_input',
380
            $variables
381
        );
382
    }
383
384
    /**
385
     * @inheritdoc
386
     */
387
    public function getPreviewHtml(mixed $value, ElementInterface $element): string
388
    {
389
        $html = '';
390
        // Reset this each time to avoid caching issues
391
        Seomatic::$previewingMetaContainers = false;
392
        /** @var Element $element */
393
        if ($element !== null && $element->uri !== null) {
394
            $siteId = $element->siteId;
395
            $uri = $element->uri;
396
            $cacheKey = self::CACHE_KEY . $uri . $siteId . $this->elementDisplayPreviewType;
397
            $metaBundleSourceType = Seomatic::$plugin->seoElements->getMetaBundleTypeFromElement($element);
398
            $seoElement = Seomatic::$plugin->seoElements->getSeoElementByMetaBundleType($metaBundleSourceType);
399
            $metaBundleSourceType = SeoEntry::getMetaBundleType();
400
            $metaBundleSourceId = '';
401
            if ($seoElement !== null) {
402
                $metaBundleSourceId = $seoElement::sourceIdFromElement($element);
403
            }
404
            $dependency = new TagDependency([
405
                'tags' => [
406
                    MetaContainers::GLOBAL_METACONTAINER_CACHE_TAG,
407
                    MetaContainers::METACONTAINER_CACHE_TAG . $metaBundleSourceId . $metaBundleSourceType . $siteId,
408
                    MetaContainers::METACONTAINER_CACHE_TAG . $uri . $siteId,
409
                    MetaContainers::METACONTAINER_CACHE_TAG . $metaBundleSourceId . $metaBundleSourceType,
410
                ],
411
            ]);
412
            $cache = Craft::$app->getCache();
413
            $cacheDuration = null;
414
            $html = $cache->getOrSet(
415
                self::CACHE_KEY . $cacheKey,
416
                function() use ($uri, $siteId, $element) {
417
                    Seomatic::$plugin->metaContainers->previewMetaContainers($uri, $siteId, true, true, $element);
418
                    $variables = [
419
                        'previewTypes' => [
420
                            $this->elementDisplayPreviewType,
421
                        ],
422
                        'previewElementId' => $element->id,
423
                    ];
424
                    // Render our preview table template
425
                    if (Seomatic::$matchedElement) {
426
                        return Craft::$app->getView()->renderTemplate(
427
                            'seomatic/_includes/table-preview.twig',
428
                            $variables
429
                        );
430
                    }
431
432
                    return '';
433
                },
434
                $cacheDuration,
435
                $dependency
436
            );
437
        }
438
439
        // Render the input template
440
        return $html;
441
    }
442
443
    /**
444
     * @inheritdoc
445
     */
446
    public function previewPlaceholderHtml(mixed $value, ?ElementInterface $element): string
447
    {
448
        if ($element !== null) {
449
            return $this->getPreviewHtml($value, $element);
450
        }
451
452
        return '';
453
    }
454
455
    // Protected Methods
456
    // =========================================================================
457
458
    /**
459
     * @param Element $element
460
     * @param string $groupName
461
     * @param array $variables
462
     */
463
    protected function setContentFieldSourceVariables(
464
        Element $element,
465
        string  $groupName,
466
        array   &$variables,
467
    ) {
468
        $variables['textFieldSources'] = array_merge(
469
            ['entryGroup' => ['optgroup' => $groupName . ' Fields'], 'title' => 'Title'],
470
            FieldHelper::fieldsOfTypeFromElement(
471
                $element,
472
                FieldHelper::TEXT_FIELD_CLASS_KEY,
473
                false
474
            )
475
        );
476
        $variables['assetFieldSources'] = array_merge(
477
            ['entryGroup' => ['optgroup' => $groupName . ' Fields']],
478
            FieldHelper::fieldsOfTypeFromElement(
479
                $element,
480
                FieldHelper::ASSET_FIELD_CLASS_KEY,
481
                false
482
            )
483
        );
484
        $variables['assetVolumeTextFieldSources'] = array_merge(
485
            ['entryGroup' => ['optgroup' => 'Asset Volume Fields'], 'title' => 'Title'],
486
            FieldHelper::fieldsOfTypeFromAssetVolumes(
487
                FieldHelper::TEXT_FIELD_CLASS_KEY,
488
                false
489
            )
490
        );
491
        $variables['userFieldSources'] = array_merge(
492
            ['entryGroup' => ['optgroup' => 'User Fields']],
493
            FieldHelper::fieldsOfTypeFromUsers(
494
                FieldHelper::TEXT_FIELD_CLASS_KEY,
495
                false
496
            )
497
        );
498
    }
499
}
500