This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | /** |
||
4 | * @copyright Copyright (c) Flipbox Digital Limited |
||
5 | * @license https://flipboxfactory.com/software/meta/license |
||
6 | * @link https://www.flipboxfactory.com/software/meta/ |
||
7 | */ |
||
8 | |||
9 | namespace flipbox\meta\services; |
||
10 | |||
11 | use Craft; |
||
12 | use craft\base\Element; |
||
13 | use craft\base\ElementInterface; |
||
14 | use craft\base\FieldInterface; |
||
15 | use craft\elements\db\ElementQueryInterface; |
||
16 | use craft\fields\BaseRelationField; |
||
17 | use flipbox\craft\sortable\associations\db\SortableAssociationQueryInterface; |
||
18 | use flipbox\craft\sortable\associations\records\SortableAssociationInterface; |
||
19 | use flipbox\craft\sortable\associations\services\SortableFields; |
||
20 | use flipbox\meta\db\MetaQuery; |
||
21 | use flipbox\meta\elements\Meta as MetaElement; |
||
22 | use flipbox\meta\fields\Meta; |
||
23 | use flipbox\meta\fields\Meta as MetaField; |
||
24 | use flipbox\meta\Meta as MetaPlugin; |
||
25 | use flipbox\meta\records\Meta as MetaRecord; |
||
26 | use yii\base\Exception; |
||
27 | |||
28 | /** |
||
29 | * @author Flipbox Factory <[email protected]> |
||
30 | * @since 1.0.0 |
||
31 | * |
||
32 | * @method MetaQuery find() |
||
33 | */ |
||
34 | class Fields extends SortableFields |
||
35 | { |
||
36 | /** |
||
37 | * @inheritdoc |
||
38 | */ |
||
39 | const SOURCE_ATTRIBUTE = MetaRecord::SOURCE_ATTRIBUTE; |
||
40 | |||
41 | /** |
||
42 | * @inheritdoc |
||
43 | */ |
||
44 | const TARGET_ATTRIBUTE = MetaRecord::TARGET_ATTRIBUTE; |
||
45 | |||
46 | /** |
||
47 | * @inheritdoc |
||
48 | */ |
||
49 | protected static function tableAlias(): string |
||
50 | { |
||
51 | return MetaRecord::tableAlias(); |
||
52 | } |
||
53 | |||
54 | /** |
||
55 | * @param FieldInterface $field |
||
56 | * @throws Exception |
||
57 | */ |
||
58 | private function ensureField(FieldInterface $field) |
||
59 | { |
||
60 | if (!$field instanceof MetaField) { |
||
61 | throw new Exception(sprintf( |
||
62 | "The field must be an instance of '%s', '%s' given.", |
||
63 | (string)MetaField::class, |
||
64 | (string)get_class($field) |
||
65 | )); |
||
66 | } |
||
67 | } |
||
68 | |||
69 | /** |
||
70 | * @inheritdoc |
||
71 | */ |
||
72 | public function getQuery( |
||
73 | FieldInterface $field, |
||
74 | ElementInterface $element = null |
||
75 | ): SortableAssociationQueryInterface { |
||
76 | /** @var MetaField $field */ |
||
77 | $this->ensureField($field); |
||
78 | |||
79 | $query = MetaPlugin::getInstance()->getElements()->getQuery(); |
||
80 | |||
81 | $query->siteId = $this->targetSiteId($element); |
||
82 | $query->fieldId = $field->id; |
||
83 | |||
84 | return $query; |
||
85 | } |
||
86 | |||
87 | |||
88 | /******************************************* |
||
89 | * NORMALIZE VALUE |
||
90 | *******************************************/ |
||
91 | |||
92 | /** |
||
93 | * @param FieldInterface $field |
||
94 | * @param $value |
||
95 | * @param ElementInterface|null $element |
||
96 | * @return array |
||
97 | */ |
||
98 | public function serializeValue( |
||
99 | FieldInterface $field, |
||
0 ignored issues
–
show
|
|||
100 | $value, |
||
101 | ElementInterface $element = null |
||
0 ignored issues
–
show
|
|||
102 | ): array { |
||
103 | /** @var MetaQuery $value */ |
||
104 | $serialized = []; |
||
105 | $new = 0; |
||
106 | |||
107 | foreach ($value->all() as $meta) { |
||
108 | $metaId = $meta->id ?? 'new' . ++$new; |
||
109 | $serialized[$metaId] = [ |
||
110 | 'enabled' => $meta->enabled, |
||
111 | 'fields' => $meta->getSerializedFieldValues(), |
||
112 | ]; |
||
113 | } |
||
114 | |||
115 | return $serialized; |
||
116 | } |
||
117 | |||
118 | /******************************************* |
||
119 | * NORMALIZE VALUE |
||
120 | *******************************************/ |
||
121 | |||
122 | /** |
||
123 | * Accepts input data and converts it into an array of associated Meta elements |
||
124 | * |
||
125 | * @param FieldInterface $field |
||
126 | * @param SortableAssociationQueryInterface $query |
||
127 | * @param array $value |
||
128 | * @param ElementInterface|null $element |
||
129 | */ |
||
130 | protected function normalizeQueryInputValues( |
||
131 | FieldInterface $field, |
||
132 | SortableAssociationQueryInterface $query, |
||
133 | array $value, |
||
134 | ElementInterface $element = null |
||
135 | ) { |
||
136 | /** @var MetaField $field */ |
||
137 | |||
138 | $models = []; |
||
139 | $sortOrder = 1; |
||
140 | $prevElement = null; |
||
141 | /** @var MetaElement|null $prevElement */ |
||
142 | |||
143 | // Get existing values |
||
144 | $existingValues = $element === null ? [] : $this->getExistingValues($field, $value, $element); |
||
145 | $ownerId = $element->id ?? null; |
||
146 | |||
147 | foreach ($value as $metaId => $metaData) { |
||
148 | // Is this new? (Or has it been deleted?) |
||
149 | if (strpos($metaId, 'new') === 0 || !isset($existingValues[$metaId])) { |
||
150 | $meta = MetaPlugin::getInstance()->getElements()->create([ |
||
151 | 'fieldId' => $field->id, |
||
152 | 'ownerId' => $ownerId, |
||
153 | 'siteId' => $this->targetSiteId($element) |
||
154 | ]); |
||
155 | } else { |
||
156 | $meta = $existingValues[$metaId]; |
||
157 | } |
||
158 | |||
159 | /** @var MetaElement $meta */ |
||
160 | |||
161 | $meta->enabled = (bool)$metaData['enabled'] ?? true; |
||
162 | $meta->setOwnerId($ownerId); |
||
163 | |||
164 | // Set the content post location on the element if we can |
||
165 | $fieldNamespace = $element->getFieldParamNamespace(); |
||
166 | |||
167 | if ($fieldNamespace !== null) { |
||
168 | $metaFieldNamespace = ($fieldNamespace ? $fieldNamespace . '.' : '') . |
||
169 | '.' . $field->handle . |
||
170 | '.' . $metaId . |
||
171 | '.fields'; |
||
172 | $meta->setFieldParamNamespace($metaFieldNamespace); |
||
173 | } |
||
174 | |||
175 | if (isset($metaData['fields'])) { |
||
176 | $meta->setFieldValues($metaData['fields']); |
||
177 | } |
||
178 | |||
179 | $sortOrder++; |
||
180 | $meta->sortOrder = $sortOrder; |
||
181 | |||
182 | // Set the prev/next elements |
||
183 | if ($prevElement) { |
||
184 | $prevElement->setNext($meta); |
||
185 | $meta->setPrev($prevElement); |
||
186 | } |
||
187 | $prevElement = $meta; |
||
188 | |||
189 | $models[] = $meta; |
||
190 | } |
||
191 | $query->setCachedResult($models); |
||
192 | } |
||
193 | |||
194 | /** |
||
195 | * @param MetaField $field |
||
196 | * @param array $values |
||
197 | * @param ElementInterface $element |
||
198 | * @return array |
||
199 | */ |
||
200 | protected function getExistingValues(MetaField $field, array $values, ElementInterface $element): array |
||
201 | { |
||
202 | /** @var Element $element */ |
||
203 | if (!empty($element->id)) { |
||
204 | $ids = []; |
||
205 | |||
206 | foreach ($values as $metaId => $meta) { |
||
207 | if (is_numeric($metaId) && $metaId !== 0) { |
||
208 | $ids[] = $metaId; |
||
209 | } |
||
210 | } |
||
211 | |||
212 | if (!empty($ids)) { |
||
213 | $oldMetaQuery = MetaPlugin::getInstance()->getElements()->getQuery(); |
||
214 | $oldMetaQuery->fieldId($field->id); |
||
215 | $oldMetaQuery->ownerId($element->id); |
||
216 | $oldMetaQuery->id($ids); |
||
217 | $oldMetaQuery->limit(null); |
||
218 | $oldMetaQuery->status(null); |
||
219 | $oldMetaQuery->enabledForSite(false); |
||
220 | $oldMetaQuery->siteId($element->siteId); |
||
221 | $oldMetaQuery->indexBy('id'); |
||
222 | return $oldMetaQuery->all(); |
||
223 | } |
||
224 | } |
||
225 | |||
226 | return []; |
||
227 | } |
||
228 | |||
229 | |||
230 | /******************************************* |
||
231 | * ELEMENT EVENTS |
||
232 | *******************************************/ |
||
233 | |||
234 | /** |
||
235 | * @param MetaField $field |
||
236 | * @param ElementInterface $element |
||
237 | * @return bool |
||
238 | * @throws \Throwable |
||
239 | */ |
||
240 | public function beforeElementDelete(MetaField $field, ElementInterface $element): bool |
||
241 | { |
||
242 | // Delete any meta elements that belong to this element(s) |
||
243 | foreach (Craft::$app->getSites()->getAllSiteIds() as $siteId) { |
||
244 | $query = MetaElement::find(); |
||
245 | $query->status(null); |
||
246 | $query->enabledForSite(false); |
||
247 | $query->fieldId($field->id); |
||
248 | $query->siteId($siteId); |
||
249 | $query->owner($element); |
||
250 | |||
251 | /** @var MetaElement $meta */ |
||
252 | foreach ($query->all() as $meta) { |
||
253 | Craft::$app->getElements()->deleteElement($meta); |
||
254 | } |
||
255 | } |
||
256 | |||
257 | return true; |
||
258 | } |
||
259 | |||
260 | /** |
||
261 | * @param Meta $field |
||
262 | * @param ElementInterface $owner |
||
263 | * @throws \Exception |
||
264 | * @throws \Throwable |
||
265 | * @throws \yii\db\Exception |
||
266 | */ |
||
267 | public function afterElementSave(MetaField $field, ElementInterface $owner) |
||
268 | { |
||
269 | /** @var Element $owner */ |
||
270 | |||
271 | /** @var MetaQuery $query */ |
||
272 | $query = $owner->getFieldValue($field->handle); |
||
273 | |||
274 | // Skip if the query's site ID is different than the element's |
||
275 | // (Indicates that the value as copied from another site for element propagation) |
||
276 | if ($query->siteId != $owner->siteId) { |
||
277 | return; |
||
278 | } |
||
279 | |||
280 | if (null === ($elements = $query->getCachedResult())) { |
||
281 | $query = clone $query; |
||
282 | $query->status = null; |
||
283 | $query->enabledForSite = false; |
||
284 | $elements = $query->all(); // existing meta |
||
285 | } |
||
286 | |||
287 | $transaction = Craft::$app->getDb()->beginTransaction(); |
||
288 | try { |
||
289 | // If this is a preexisting element, make sure that the blocks for this field/owner respect the |
||
290 | // field's translation setting |
||
291 | if ($query->ownerId) { |
||
292 | $this->applyFieldTranslationSetting($query->ownerId, $query->siteId, $field); |
||
0 ignored issues
–
show
It seems like
$query->ownerId can also be of type array<integer,integer> ; however, flipbox\meta\services\Fi...eldTranslationSetting() does only seem to accept integer , maybe add an additional type check?
If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check: /**
* @return array|string
*/
function returnsDifferentValues($x) {
if ($x) {
return 'foo';
}
return array();
}
$x = returnsDifferentValues($y);
if (is_array($x)) {
// $x is an array.
}
If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue. ![]() |
|||
293 | } |
||
294 | |||
295 | // If the query is set to fetch blocks of a different owner, we're probably duplicating an element |
||
296 | if ($query->ownerId && $query->ownerId != $owner->id) { |
||
297 | $this->duplicate($field, $owner, $query, $elements); |
||
298 | } else { |
||
299 | $this->save($field, $owner, $elements); |
||
300 | } |
||
301 | |||
302 | $transaction->commit(); |
||
303 | } catch (\Exception $e) { |
||
304 | $transaction->rollback(); |
||
305 | throw $e; |
||
306 | } |
||
307 | |||
308 | return; |
||
309 | } |
||
310 | |||
311 | /** |
||
312 | * @param MetaField $field |
||
313 | * @param ElementInterface $owner |
||
314 | * @param MetaQuery $query |
||
315 | * @param array $elements |
||
316 | * @throws \Throwable |
||
317 | * @throws \craft\errors\InvalidElementException |
||
318 | */ |
||
319 | private function duplicate(MetaField $field, ElementInterface $owner, MetaQuery $query, array $elements) |
||
320 | { |
||
321 | /** @var Element $owner */ |
||
322 | |||
323 | $newQuery = clone $query; |
||
324 | $newQuery->ownerId = $owner->id; |
||
325 | if (!$newQuery->exists()) { |
||
326 | // Duplicate for the new owner |
||
327 | $elementsService = Craft::$app->getElements(); |
||
328 | foreach ($elements as $element) { |
||
329 | $elementsService->duplicateElement($element, [ |
||
330 | 'ownerId' => $owner->id, |
||
331 | 'ownerSiteId' => $field->localize ? $owner->siteId : null |
||
332 | ]); |
||
333 | } |
||
334 | } |
||
335 | } |
||
336 | |||
337 | /** |
||
338 | * @param MetaField $field |
||
339 | * @param ElementInterface $owner |
||
340 | * @param MetaElement[] $elements |
||
341 | * @throws Exception |
||
342 | * @throws \Throwable |
||
343 | * @throws \craft\errors\ElementNotFoundException |
||
344 | */ |
||
345 | private function save(MetaField $field, ElementInterface $owner, array $elements) |
||
346 | { |
||
347 | /** @var Element $owner */ |
||
348 | |||
349 | $elementIds = []; |
||
350 | |||
351 | // Only propagate the blocks if the owner isn't being propagated |
||
352 | $propagate = !$owner->propagating; |
||
353 | |||
354 | /** @var MetaElement $element */ |
||
355 | foreach ($elements as $element) { |
||
356 | $element->setOwner($owner); |
||
357 | $element->ownerSiteId = ($field->localize ? $owner->siteId : null); |
||
358 | $element->propagating = $owner->propagating; |
||
359 | |||
360 | Craft::$app->getElements()->saveElement($element, false, $propagate); |
||
361 | |||
362 | $elementIds[] = $element->id; |
||
363 | } |
||
364 | |||
365 | // Delete any elements that have been removed |
||
366 | $this->deleteOld($field, $owner, $elementIds); |
||
367 | } |
||
368 | |||
369 | /** |
||
370 | * @param MetaField $field |
||
371 | * @param ElementInterface $owner |
||
372 | * @param array $excludeIds |
||
373 | * @throws \Throwable |
||
374 | */ |
||
375 | private function deleteOld(MetaField $field, ElementInterface $owner, array $excludeIds) |
||
376 | { |
||
377 | /** @var Element $owner */ |
||
378 | |||
379 | $deleteElementsQuery = MetaElement::find() |
||
380 | ->status(null) |
||
381 | ->enabledForSite(false) |
||
382 | ->ownerId($owner->id) |
||
383 | ->fieldId($field->id) |
||
384 | ->where(['not', ['elements.id' => $excludeIds]]); |
||
385 | |||
386 | if ($field->localize) { |
||
387 | $deleteElementsQuery->ownerSiteId($owner->siteId); |
||
388 | } else { |
||
389 | $deleteElementsQuery->siteId($owner->siteId); |
||
390 | } |
||
391 | |||
392 | foreach ($deleteElementsQuery->all() as $deleteElement) { |
||
393 | Craft::$app->getElements()->deleteElement($deleteElement); |
||
394 | } |
||
395 | } |
||
396 | |||
397 | /** |
||
398 | * Applies the field's translation setting to a set of blocks. |
||
399 | * |
||
400 | * @param int $ownerId |
||
401 | * @param int $ownerSiteId |
||
402 | * @param Meta $field |
||
403 | * @throws Exception |
||
404 | * @throws \Throwable |
||
405 | * @throws \craft\errors\ElementNotFoundException |
||
406 | */ |
||
407 | private function applyFieldTranslationSetting(int $ownerId, int $ownerSiteId, MetaField $field) |
||
408 | { |
||
409 | // If the field is translatable, see if there are any global blocks that should be localized |
||
410 | if ($field->localize) { |
||
411 | $this->saveFieldTranslations($field, $ownerId, $ownerSiteId); |
||
412 | } else { |
||
413 | // Otherwise, see if the field has any localized blocks that should be deleted |
||
414 | foreach (Craft::$app->getSites()->getAllSiteIds() as $siteId) { |
||
415 | if ($siteId != $ownerSiteId) { |
||
416 | /** @var MetaQuery $elements */ |
||
417 | $elements = MetaElement::find() |
||
0 ignored issues
–
show
The method
ownerSiteId() does not exist on craft\elements\db\ElementQuery . Did you maybe mean siteId() ?
This check marks calls to methods that do not seem to exist on an object. This is most likely the result of a method being renamed without all references to it being renamed likewise. ![]() |
|||
418 | ->fieldId($field->id) |
||
419 | ->ownerId($ownerId) |
||
420 | ->status(null) |
||
421 | ->enabledForSite(false) |
||
422 | ->limit(null) |
||
423 | ->siteId($siteId) |
||
424 | ->ownerSiteId($siteId) |
||
425 | ->all(); |
||
426 | |||
427 | foreach ($elements as $element) { |
||
428 | Craft::$app->getElements()->deleteElement($element); |
||
429 | } |
||
430 | } |
||
431 | } |
||
432 | } |
||
433 | } |
||
434 | |||
435 | /** |
||
436 | * @param MetaField $field |
||
437 | * @param int $ownerId |
||
438 | * @param int $ownerSiteId |
||
439 | * @throws Exception |
||
440 | * @throws \Throwable |
||
441 | * @throws \craft\errors\ElementNotFoundException |
||
442 | */ |
||
443 | private function saveFieldTranslations(MetaField $field, int $ownerId, int $ownerSiteId) |
||
444 | { |
||
445 | /** @var MetaQuery $elements */ |
||
446 | $elementQuery = MetaElement::find() |
||
0 ignored issues
–
show
The method
ownerSiteId() does not exist on craft\elements\db\ElementQuery . Did you maybe mean siteId() ?
This check marks calls to methods that do not seem to exist on an object. This is most likely the result of a method being renamed without all references to it being renamed likewise. ![]() |
|||
447 | ->fieldId($field->id) |
||
448 | ->ownerId($ownerId) |
||
449 | ->status(null) |
||
450 | ->enabledForSite(false) |
||
451 | ->limit(null) |
||
452 | ->siteId($ownerSiteId) |
||
453 | ->ownerSiteId(':empty:'); |
||
454 | |||
455 | $elements = $elementQuery->all(); |
||
456 | |||
457 | if (empty($elements)) { |
||
458 | return; |
||
459 | } |
||
460 | |||
461 | // Prefetch the blocks in all the other sites, in case they have any localized content |
||
462 | $otherSiteMeta = $this->getOtherSiteMeta($elementQuery, $ownerSiteId); |
||
463 | |||
464 | // Explicitly assign the current site's blocks to the current site |
||
465 | foreach ($elements as $element) { |
||
466 | $element->ownerSiteId = $ownerSiteId; |
||
467 | Craft::$app->getElements()->saveElement($element, false); |
||
468 | } |
||
469 | |||
470 | // Now save the other sites' blocks as new site-specific blocks |
||
471 | foreach ($otherSiteMeta as $siteId => $siteElements) { |
||
472 | foreach ($siteElements as $element) { |
||
473 | $element->id = null; |
||
474 | $element->contentId = null; |
||
475 | $element->siteId = (int)$siteId; |
||
476 | $element->ownerSiteId = (int)$siteId; |
||
477 | Craft::$app->getElements()->saveElement($element, false); |
||
478 | } |
||
479 | } |
||
480 | } |
||
481 | |||
482 | /** |
||
483 | * @param MetaQuery $query |
||
484 | * @param int $ownerSiteId |
||
485 | * @return array |
||
486 | */ |
||
487 | private function getOtherSiteMeta(MetaQuery $query, int $ownerSiteId) |
||
488 | { |
||
489 | // Find any relational fields |
||
490 | $relationFields = $this->getRelationFields($query->all()); |
||
491 | |||
492 | $otherSiteMeta = []; |
||
493 | foreach (Craft::$app->getSites()->getAllSiteIds() as $siteId) { |
||
494 | if ($siteId != $ownerSiteId) { |
||
495 | /** @var MetaElement[] $siteElements */ |
||
496 | $siteElements = $otherSiteMeta[$siteId] = $query->siteId($siteId)->all(); |
||
497 | |||
498 | // Hard-set the relation IDs |
||
499 | foreach ($siteElements as $element) { |
||
500 | foreach ($relationFields as $handle) { |
||
501 | /** @var ElementQueryInterface $relationQuery */ |
||
502 | $relationQuery = $element->getFieldValue($handle); |
||
503 | $element->setFieldValue($handle, $relationQuery->ids()); |
||
504 | } |
||
505 | } |
||
506 | } |
||
507 | } |
||
508 | |||
509 | return $otherSiteMeta; |
||
510 | } |
||
511 | |||
512 | /** |
||
513 | * @param array $elements |
||
514 | * @return array |
||
515 | */ |
||
516 | private function getRelationFields(array $elements) |
||
517 | { |
||
518 | // Find any relational fields on these blocks |
||
519 | $relationFields = []; |
||
520 | |||
521 | foreach ($elements as $element) { |
||
522 | foreach ($element->getFieldLayout()->getFields() as $field) { |
||
523 | if ($field instanceof BaseRelationField) { |
||
524 | $relationFields[] = $field->handle; |
||
525 | } |
||
526 | } |
||
527 | break; |
||
528 | } |
||
529 | |||
530 | return $relationFields; |
||
531 | } |
||
532 | |||
533 | /** |
||
534 | * @inheritdoc |
||
535 | */ |
||
536 | protected function normalizeQueryInputValue( |
||
537 | FieldInterface $field, |
||
538 | $value, |
||
539 | int &$sortOrder, |
||
540 | ElementInterface $element = null |
||
541 | ): SortableAssociationInterface { |
||
542 | |||
543 | throw new Exception(__METHOD__ . ' is not implemented'); |
||
544 | } |
||
545 | } |
||
546 |
This check looks from parameters that have been defined for a function or method, but which are not used in the method body.