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
|
|||||||
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
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. ![]() |
|||||||
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
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. ![]() |
|||||||
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
![]() |
|||||||
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 |
This check looks for parameters that have been defined for a function or method, but which are not used in the method body.