Issues (231)

src/fields/SeoSettings.php (4 issues)

1
<?php
2
/**
3
 * SEOmatic plugin for Craft CMS 3.x
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;
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
    const CACHE_KEY = 'seomatic_fieldmeta_';
51
52
    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 displayName(): string
115
    {
116
        return Craft::t('seomatic', 'SEO Settings');
117
    }
118
119
    // Public Methods
120
    // =========================================================================
121
122
    /**
123
     * @inheritdoc
124
     */
125
    public function rules()
126
    {
127
        $rules = parent::rules();
128
        $rules = array_merge($rules, [
129
            [
130
                [
131
                    'elementDisplayPreviewType',
132
                ],
133
                'string',
134
            ],
135
            [
136
                [
137
                    'generalTabEnabled',
138
                    'twitterTabEnabled',
139
                    'facebookTabEnabled',
140
                    'sitemapTabEnabled',
141
                ],
142
                'boolean',
143
            ],
144
            [
145
                [
146
                    'generalEnabledFields',
147
                    'twitterEnabledFields',
148
                    'facebookEnabledFields',
149
                    'sitemapEnabledFields',
150
                ],
151
                'each', 'rule' => ['string'],
152
            ],
153
154
        ]);
155
156
        return $rules;
157
    }
158
159
    /**
160
     * @inheritdoc
161
     */
162
    public function getContentColumnType(): string
163
    {
164
        return Schema::TYPE_TEXT;
165
    }
166
167
    /**
168
     * @inheritdoc
169
     * @since 2.0.0
170
     */
171
    public function useFieldset(): bool
172
    {
173
        return false;
174
    }
175
176
    /**
177
     * @inheritdoc
178
     */
179
    public function normalizeValue($value, ElementInterface $element = null)
180
    {
181
        $config = [];
182
        // Handle incoming values potentially being JSON, an array, or an object
183
        if (!empty($value)) {
184
            if (is_string($value)) {
185
                // Decode any html entities
186
                $value = html_entity_decode($value, ENT_NOQUOTES, 'UTF-8');
187
                $config = Json::decodeIfJson($value);
188
            }
189
            if (is_array($value)) {
190
                $config = $value;
191
            }
192
            if (is_object($value) && $value instanceof MetaBundle) {
193
                $config = $value->toArray();
194
            }
195
        } else {
196
            /** @var null|Element $element */
197
            $config = MigrationHelper::configFromSeomaticMeta(
198
                $element,
199
                MigrationHelper::FIELD_MIGRATION_CONTEXT
200
            );
201
        }
202
        // If the config isn't empty, do some processing on the values
203
        if (!empty($config)) {
204
            $elementName = '';
205
            /** @var Element $element */
206
            if ($element !== null) {
207
                $reflector = new ReflectionClass($element);
208
                $elementName = strtolower($reflector->getShortName());
209
            }
210
            // Handle the pull fields
211
            if (!empty($config['metaGlobalVars']) && !empty($config['metaBundleSettings'])) {
212
                PullFieldHelper::parseTextSources(
213
                    $elementName,
214
                    $config['metaGlobalVars'],
215
                    $config['metaBundleSettings']
216
                );
217
                PullFieldHelper::parseImageSources(
218
                    $elementName,
219
                    $config['metaGlobalVars'],
220
                    $config['metaBundleSettings'],
221
                    null
222
                );
223
            }
224
            // Handle the mainEntityOfPage
225
            $mainEntity = '';
226
            if (in_array('mainEntityOfPage', $this->generalEnabledFields, false) &&
227
                !empty($config['metaBundleSettings'])) {
228
                $mainEntity = SchemaHelper::getSpecificEntityType($config['metaBundleSettings'], true);
229
            }
230
            if (!empty($config['metaGlobalVars'])) {
231
                $config['metaGlobalVars']['mainEntityOfPage'] = $mainEntity;
232
            }
233
        }
234
        // Create a new meta bundle with propagated defaults
235
        $metaBundleDefaults = ArrayHelper::merge(
236
            ConfigHelper::getConfigFromFile('fieldmeta/Bundle'),
237
            $config
238
        );
239
240
        return MetaBundle::create($metaBundleDefaults);
241
    }
242
243
    /**
244
     * @inheritdoc
245
     */
246
    public function serializeValue($value, ElementInterface $element = null)
247
    {
248
        $value = parent::serializeValue($value, $element);
249
        if (!Craft::$app->getDb()->getSupportsMb4()) {
250
            if (is_string($value)) {
251
                // Encode any 4-byte UTF-8 characters.
252
                $value = StringHelper::encodeMb4($value);
253
            }
254
            if (is_array($value)) {
255
                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

255
                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...
256
                    if ($arrayValue !== null && is_string($arrayValue)) {
257
                        $arrayValue = StringHelper::encodeMb4($arrayValue);
258
                    }
259
                });
260
            }
261
        }
262
263
        return $value;
264
    }
265
266
    /**
267
     * @inheritdoc
268
     */
269
    public function getSettingsHtml()
270
    {
271
        $variables = [];
272
        $tagOptions = [
273
            'depends' => [
274
                'nystudio107\\seomatic\\assetbundles\\seomatic\\SeomaticAsset',
275
            ],
276
        ];
277
        // JS/CSS modules
278
        try {
279
            Seomatic::$view->registerAssetBundle(SeomaticAsset::class);
280
            Seomatic::$plugin->vite->register('src/js/seomatic.js', false, $tagOptions, $tagOptions);
281
            Seomatic::$plugin->vite->register('src/js/seomatic-meta.js', false, $tagOptions, $tagOptions);
282
        } catch (InvalidConfigException $e) {
283
            Craft::error($e->getMessage(), __METHOD__);
284
        }
285
        // Asset bundle
286
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(
287
            '@nystudio107/seomatic/web/assets/dist',
288
            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

288
        /** @scrutinizer ignore-call */ 
289
        $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...
289
        );
290
        $variables['field'] = $this;
291
292
        // Render the settings template
293
        return Craft::$app->getView()->renderTemplate(
294
            'seomatic/_components/fields/SeoSettings_settings',
295
            $variables
296
        );
297
    }
298
299
    /**
300
     * @inheritdoc
301
     */
302
    public function getInputHtml($value, ElementInterface $element = null): string
303
    {
304
        $variables = [];
305
        // JS/CSS modules
306
        $tagOptions = [
307
            'depends' => [
308
                'nystudio107\\seomatic\\assetbundles\\seomatic\\SeomaticAsset',
309
            ],
310
        ];
311
        // JS/CSS modules
312
        try {
313
            Seomatic::$view->registerAssetBundle(SeomaticAsset::class);
314
            Seomatic::$plugin->vite->register('src/js/seomatic.js', false, $tagOptions, $tagOptions);
315
            Seomatic::$plugin->vite->register('src/js/seomatic-meta.js', false, $tagOptions, $tagOptions);
316
        } catch (InvalidConfigException $e) {
317
            Craft::error($e->getMessage(), __METHOD__);
318
        }
319
        // Asset bundle
320
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(
321
            '@nystudio107/seomatic/web/assets/dist',
322
            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

322
        /** @scrutinizer ignore-call */ 
323
        $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...
323
        );
324
        // Basic variables
325
        $variables['name'] = $this->handle;
326
        $variables['value'] = $value;
327
        $variables['field'] = $this;
328
        $variables['currentSourceBundleType'] = 'entry';
329
        $variables['entitySchemaPath'] = SchemaHelper::getEntityPath($value->metaBundleSettings);
330
331
        // Get our id and namespace
332
        $id = Craft::$app->getView()->formatInputId($this->handle);
333
        $nameSpacedId = Craft::$app->getView()->namespaceInputId($id);
334
        $variables['id'] = $id;
335
        $variables['nameSpacedId'] = $nameSpacedId;
336
337
        // Make sure the *Sources variables at least exist, for things like the QuickPost widget
338
        $variables['textFieldSources'] = [];
339
        $variables['assetFieldSources'] = [];
340
        $variables['assetVolumeTextFieldSources'] = [];
341
        $variables['userFieldSources'] = [];
342
        // Pull field sources
343
        if ($element !== null) {
344
            /** @var Element $element */
345
            $this->setContentFieldSourceVariables($element, 'Entry', $variables);
346
        }
347
348
        /** @var MetaBundle $value */
349
        $variables['elementType'] = Asset::class;
350
351
        $variables['parentBundles'] = [];
352
        // Preview the containers so the preview is correct in the field
353
        if ($element !== null && $element->uri !== null) {
354
            Seomatic::$plugin->metaContainers->previewMetaContainers($element->uri, $element->siteId, true, true, $element);
355
            $contentMeta = Seomatic::$plugin->metaBundles->getContentMetaBundleForElement($element);
356
            $globalMeta = Seomatic::$plugin->metaBundles->getGlobalMetaBundle($element->siteId);
0 ignored issues
show
It seems like $element->siteId can also be of type null; however, parameter $sourceSiteId of nystudio107\seomatic\ser...::getGlobalMetaBundle() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

356
            $globalMeta = Seomatic::$plugin->metaBundles->getGlobalMetaBundle(/** @scrutinizer ignore-type */ $element->siteId);
Loading history...
357
            $variables['parentBundles'] = [$contentMeta, $globalMeta];
358
        }
359
360
        // Render the input template
361
        return Craft::$app->getView()->renderTemplate(
362
            'seomatic/_components/fields/SeoSettings_input',
363
            $variables
364
        );
365
    }
366
367
    /**
368
     * @inheritdoc
369
     */
370
    public function getTableAttributeHtml($value, ElementInterface $element): string
371
    {
372
        $html = '';
373
        // Reset this each time to avoid caching issues
374
        Seomatic::$previewingMetaContainers = false;
375
        /** @var Element $element */
376
        if ($element !== null && $element->uri !== null) {
377
            $siteId = $element->siteId;
378
            $uri = $element->uri;
379
            $cacheKey = self::CACHE_KEY . $uri . $siteId . $this->elementDisplayPreviewType;
380
            $metaBundleSourceType = Seomatic::$plugin->seoElements->getMetaBundleTypeFromElement($element);
381
            $seoElement = Seomatic::$plugin->seoElements->getSeoElementByMetaBundleType($metaBundleSourceType);
382
            $metaBundleSourceType = SeoEntry::getMetaBundleType();
383
            $metaBundleSourceId = '';
384
            if ($seoElement !== null) {
385
                $metaBundleSourceId = $seoElement::sourceIdFromElement($element);
386
            }
387
            $dependency = new TagDependency([
388
                'tags' => [
389
                    MetaContainers::GLOBAL_METACONTAINER_CACHE_TAG,
390
                    MetaContainers::METACONTAINER_CACHE_TAG . $metaBundleSourceId . $metaBundleSourceType . $siteId,
391
                    MetaContainers::METACONTAINER_CACHE_TAG . $uri . $siteId,
392
                    MetaContainers::METACONTAINER_CACHE_TAG . $metaBundleSourceId . $metaBundleSourceType,
393
                ],
394
            ]);
395
            $cache = Craft::$app->getCache();
396
            $cacheDuration = null;
397
            $html = $cache->getOrSet(
398
                self::CACHE_KEY . $cacheKey,
399
                function() use ($uri, $siteId, $element) {
400
                    Seomatic::$plugin->metaContainers->previewMetaContainers($uri, $siteId, true, true, $element);
401
                    $variables = [
402
                        'previewTypes' => [
403
                            $this->elementDisplayPreviewType,
404
                        ],
405
                        'previewElementId' => $element->id,
406
                    ];
407
                    // Render our preview table template
408
                    if (Seomatic::$matchedElement) {
409
                        return Craft::$app->getView()->renderTemplate(
410
                            'seomatic/_includes/table-preview.twig',
411
                            $variables
412
                        );
413
                    }
414
415
                    return '';
416
                },
417
                $cacheDuration,
418
                $dependency
419
            );
420
        }
421
422
        // Render the input template
423
        return $html;
424
    }
425
426
    // Protected Methods
427
    // =========================================================================
428
429
    /**
430
     * @param Element $element
431
     * @param string $groupName
432
     * @param array $variables
433
     */
434
    protected function setContentFieldSourceVariables(
435
        Element $element,
436
        string  $groupName,
437
        array   &$variables
438
    ) {
439
        $variables['textFieldSources'] = array_merge(
440
            ['entryGroup' => ['optgroup' => $groupName . ' Fields'], 'title' => 'Title'],
441
            FieldHelper::fieldsOfTypeFromElement(
442
                $element,
443
                FieldHelper::TEXT_FIELD_CLASS_KEY,
444
                false
445
            )
446
        );
447
        $variables['assetFieldSources'] = array_merge(
448
            ['entryGroup' => ['optgroup' => $groupName . ' Fields']],
449
            FieldHelper::fieldsOfTypeFromElement(
450
                $element,
451
                FieldHelper::ASSET_FIELD_CLASS_KEY,
452
                false
453
            )
454
        );
455
        $variables['assetVolumeTextFieldSources'] = array_merge(
456
            ['entryGroup' => ['optgroup' => 'Asset Volume Fields'], 'title' => 'Title'],
457
            FieldHelper::fieldsOfTypeFromAssetVolumes(
458
                FieldHelper::TEXT_FIELD_CLASS_KEY,
459
                false
460
            )
461
        );
462
        $variables['userFieldSources'] = array_merge(
463
            ['entryGroup' => ['optgroup' => 'User Fields']],
464
            FieldHelper::fieldsOfTypeFromUsers(
465
                FieldHelper::TEXT_FIELD_CLASS_KEY,
466
                false
467
            )
468
        );
469
    }
470
}
471