From aea652bc79117df9aceb336db6fe85df0a3cec67 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Tue, 19 Jul 2022 17:33:48 -0600 Subject: [PATCH 01/43] Pass the inverse flag to the base Laravel MorphToMany constructor Without this morphedByMany relationships use the incorrect class name when building queries. --- src/Database/Relations/MorphToMany.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Database/Relations/MorphToMany.php b/src/Database/Relations/MorphToMany.php index e8eb53b91..2487139c4 100644 --- a/src/Database/Relations/MorphToMany.php +++ b/src/Database/Relations/MorphToMany.php @@ -45,12 +45,6 @@ public function __construct( $relationName = null, $inverse = false ) { - $this->inverse = $inverse; - - $this->morphType = $name.'_type'; - - $this->morphClass = $inverse ? $query->getModel()->getMorphClass() : $parent->getMorphClass(); - parent::__construct( $query, $parent, @@ -60,7 +54,8 @@ public function __construct( $otherKey, $parentKey, $relatedKey, - $relationName + $relationName, + $inverse ); $this->addDefinedConstraints(); From bcbafd8242acfa58170edaeaee1b197cbb398806 Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Wed, 27 Jul 2022 20:10:54 +0800 Subject: [PATCH 02/43] [1.2] Sortable Relation trait (#94) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add missing model.relation.before/after events - New HasSortableRelations trait Co-authored-by: Tobias Kündig Co-authored-by: Marc Jauvin --- phpstan-baseline.neon | 5 - src/Database/Relations/BelongsTo.php | 2 + .../Relations/Concerns/AttachOneOrMany.php | 60 ++++++++ .../Relations/Concerns/BelongsOrMorphsTo.php | 88 +++++++++++ .../Relations/Concerns/HasOneOrMany.php | 59 ++++++++ .../Relations/Concerns/MorphOneOrMany.php | 60 ++++++++ src/Database/Relations/MorphTo.php | 1 + src/Database/Traits/HasSortableRelations.php | 138 ++++++++++++++++++ 8 files changed, 408 insertions(+), 5 deletions(-) create mode 100644 src/Database/Relations/Concerns/BelongsOrMorphsTo.php create mode 100644 src/Database/Traits/HasSortableRelations.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index fd99a24a5..b50ce78ce 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -260,11 +260,6 @@ parameters: count: 1 path: src/Database/Relations/BelongsTo.php - - - message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:getRelationDefinition\\(\\)\\.$#" - count: 3 - path: src/Database/Relations/BelongsTo.php - - message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:getForeignKey\\(\\)\\.$#" count: 3 diff --git a/src/Database/Relations/BelongsTo.php b/src/Database/Relations/BelongsTo.php index 798906eac..1a133f6b1 100644 --- a/src/Database/Relations/BelongsTo.php +++ b/src/Database/Relations/BelongsTo.php @@ -6,9 +6,11 @@ /** * @phpstan-property \Winter\Storm\Database\Model $child + * @phpstan-property \Winter\Storm\Database\Model $parent */ class BelongsTo extends BelongsToBase { + use Concerns\BelongsOrMorphsTo; use Concerns\DeferOneOrMany; use Concerns\DefinedConstraints; diff --git a/src/Database/Relations/Concerns/AttachOneOrMany.php b/src/Database/Relations/Concerns/AttachOneOrMany.php index 441e6b218..22e761e44 100644 --- a/src/Database/Relations/Concerns/AttachOneOrMany.php +++ b/src/Database/Relations/Concerns/AttachOneOrMany.php @@ -167,6 +167,21 @@ public function add(Model $model, $sessionKey = null) } if ($sessionKey === null) { + /** + * @event model.relation.beforeAdd + * Called before adding a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations) + * + * Example usage: + * + * $model->bindEvent('model.relation.beforeAdd', function (string $relationName, \Winter\Storm\Database\Model $relatedModel) use (\Winter\Storm\Database\Model $model) { + * if ($relationName === 'dummyRelation') { + * throw new \Exception("Invalid relation!"); + * } + * }); + * + */ + $this->parent->fireEvent('model.relation.beforeAdd', [$this->relationName, $model]); + // Delete siblings for single attachments if ($this instanceof AttachOne) { $this->delete(); @@ -186,6 +201,21 @@ public function add(Model $model, $sessionKey = null) else { $this->parent->reloadRelations($this->relationName); } + + /** + * @event model.relation.afterAdd + * Called after adding a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations) + * + * Example usage: + * + * $model->bindEvent('model.relation.afterAdd', function (string $relationName, \Winter\Storm\Database\Model $relatedModel) use (\Winter\Storm\Database\Model $model) { + * $relatedClass = get_class($relatedModel); + * $modelClass = get_class($model); + * traceLog("{$relatedClass} was added as {$relationName} to {$modelClass}."); + * }); + * + */ + $this->parent->fireEvent('model.relation.afterAdd', [$this->relationName, $model]); } else { $this->parent->bindDeferred($this->relationName, $model, $sessionKey); @@ -210,6 +240,21 @@ public function addMany($models, $sessionKey = null) public function remove(Model $model, $sessionKey = null) { if ($sessionKey === null) { + /** + * @event model.relation.beforeRemove + * Called before removing a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations) + * + * Example usage: + * + * $model->bindEvent('model.relation.beforeRemove', function (string $relationName, \Winter\Storm\Database\Model $relatedModel) use (\Winter\Storm\Database\Model $model) { + * if ($relationName === 'permanentRelation') { + * throw new \Exception("Cannot dissociate a permanent relation!"); + * } + * }); + * + */ + $this->parent->fireEvent('model.relation.beforeRemove', [$this->relationName, $model]); + $options = $this->parent->getRelationDefinition($this->relationName); if (array_get($options, 'delete', false)) { @@ -234,6 +279,21 @@ public function remove(Model $model, $sessionKey = null) else { $this->parent->reloadRelations($this->relationName); } + + /** + * @event model.relation.afterRemove + * Called after removing a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations) + * + * Example usage: + * + * $model->bindEvent('model.relation.afterRemove', function (string $relationName, \Winter\Storm\Database\Model $relatedModel) use (\Winter\Storm\Database\Model $model) { + * $relatedClass = get_class($relatedModel); + * $modelClass = get_class($model); + * traceLog("{$relatedClass} was removed from {$modelClass}."); + * }); + * + */ + $this->parent->fireEvent('model.relation.afterRemove', [$this->relationName, $model]); } else { $this->parent->unbindDeferred($this->relationName, $model, $sessionKey); diff --git a/src/Database/Relations/Concerns/BelongsOrMorphsTo.php b/src/Database/Relations/Concerns/BelongsOrMorphsTo.php new file mode 100644 index 000000000..8c51f88ed --- /dev/null +++ b/src/Database/Relations/Concerns/BelongsOrMorphsTo.php @@ -0,0 +1,88 @@ +bindEvent('model.relation.beforeAssociate', function (string $relationName, \Winter\Storm\Database\Model $relatedModel) use (\Winter\Storm\Database\Model $model) { + * if ($relationName === 'dummyRelation') { + * throw new \Exception("Invalid relation!"); + * } + * }); + * + */ + $this->parent->fireEvent('model.relation.beforeAssociate', [$this->relationName, $model]); + + $result = parent::associate($model); + + /** + * @event model.relation.afterAssociate + * Called after associating a relation to the model (only for BelongsTo/MorphTo relations) + * + * Example usage: + * + * $model->bindEvent('model.relation.afterAssociate', function (string $relationName, \Winter\Storm\Database\Model $relatedModel) use (\Winter\Storm\Database\Model $model) { + * $relatedClass = get_class($relatedModel); + * $modelClass = get_class($model); + * traceLog("{$relatedClass} was associated as {$relationName} to {$modelClass}."); + * }); + * + */ + $this->parent->fireEvent('model.relation.afterAssociate', [$this->relationName, $model]); + + return $result; + } + + /** + * Dissociate previously associated model from the given parent. + * + * @return \Illuminate\Database\Eloquent\Model + */ + public function dissociate() + { + /** + * @event model.relation.beforeDissociate + * Called before dissociating a relation to the model (only for BelongsTo/MorphTo relations) + * + * Example usage: + * + * $model->bindEvent('model.relation.beforeDissociate', function (string $relationName) use (\Winter\Storm\Database\Model $model) { + * if ($relationName === 'permanentRelation') { + * throw new \Exception("Cannot dissociate a permanent relation!"); + * } + * }); + * + */ + $this->parent->fireEvent('model.relation.beforeDissociate', [$this->relationName]); + + $result = parent::dissociate(); + + /** + * @event model.relation.afterDissociate + * Called after dissociating a relation to the model (only for BelongsTo/MorphTo relations) + * + * Example usage: + * + * $model->bindEvent('model.relation.afterDissociate', function (string $relationName) use (\Winter\Storm\Database\Model $model) { + * $modelClass = get_class($model); + * traceLog("{$relationName} was dissociated from {$modelClass}."); + * }); + * + */ + $this->parent->fireEvent('model.relation.afterDissociate', [$this->relationName]); + + return $result; + } +} diff --git a/src/Database/Relations/Concerns/HasOneOrMany.php b/src/Database/Relations/Concerns/HasOneOrMany.php index 24cb271e8..918604cb9 100644 --- a/src/Database/Relations/Concerns/HasOneOrMany.php +++ b/src/Database/Relations/Concerns/HasOneOrMany.php @@ -57,6 +57,21 @@ public function create(array $attributes = [], $sessionKey = null) public function add(Model $model, $sessionKey = null) { if ($sessionKey === null) { + /** + * @event model.relation.beforeAdd + * Called before adding a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations) + * + * Example usage: + * + * $model->bindEvent('model.relation.beforeAdd', function (string $relationName, \Winter\Storm\Database\Model $relatedModel) use (\Winter\Storm\Database\Model $model) { + * if ($relationName === 'dummyRelation') { + * throw new \Exception("Invalid relation!"); + * } + * }); + * + */ + $this->parent->fireEvent('model.relation.beforeAdd', [$this->relationName, $model]); + $model->setAttribute($this->getForeignKeyName(), $this->getParentKey()); if (!$model->exists || $model->isDirty()) { @@ -72,6 +87,21 @@ public function add(Model $model, $sessionKey = null) else { $this->parent->reloadRelations($this->relationName); } + + /** + * @event model.relation.afterAdd + * Called after adding a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations) + * + * Example usage: + * + * $model->bindEvent('model.relation.afterAdd', function (string $relationName, \Winter\Storm\Database\Model $relatedModel) use (\Winter\Storm\Database\Model $model) { + * $relatedClass = get_class($relatedModel); + * $modelClass = get_class($model); + * traceLog("{$relatedClass} was added as {$relationName} to {$modelClass}."); + * }); + * + */ + $this->parent->fireEvent('model.relation.afterAdd', [$this->relationName, $model]); } else { $this->parent->bindDeferred($this->relationName, $model, $sessionKey); @@ -96,6 +126,21 @@ public function addMany($models, $sessionKey = null) public function remove(Model $model, $sessionKey = null) { if ($sessionKey === null) { + /** + * @event model.relation.beforeRemove + * Called before removing a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations) + * + * Example usage: + * + * $model->bindEvent('model.relation.beforeRemove', function (string $relationName, \Winter\Storm\Database\Model $relatedModel) use (\Winter\Storm\Database\Model $model) { + * if ($relationName === 'permanentRelation') { + * throw new \Exception("Cannot dissociate a permanent relation!"); + * } + * }); + * + */ + $this->parent->fireEvent('model.relation.beforeRemove', [$this->relationName, $model]); + $model->setAttribute($this->getForeignKeyName(), null); $model->save(); @@ -108,6 +153,20 @@ public function remove(Model $model, $sessionKey = null) else { $this->parent->reloadRelations($this->relationName); } + /** + * @event model.relation.afterRemove + * Called after removing a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations) + * + * Example usage: + * + * $model->bindEvent('model.relation.afterRemove', function (string $relationName, \Winter\Storm\Database\Model $relatedModel) use (\Winter\Storm\Database\Model $model) { + * $relatedClass = get_class($relatedModel); + * $modelClass = get_class($model); + * traceLog("{$relatedClass} was removed from {$modelClass}."); + * }); + * + */ + $this->parent->fireEvent('model.relation.afterRemove', [$this->relationName, $model]); } else { $this->parent->unbindDeferred($this->relationName, $model, $sessionKey); diff --git a/src/Database/Relations/Concerns/MorphOneOrMany.php b/src/Database/Relations/Concerns/MorphOneOrMany.php index b7166aeea..6add0dcb4 100644 --- a/src/Database/Relations/Concerns/MorphOneOrMany.php +++ b/src/Database/Relations/Concerns/MorphOneOrMany.php @@ -45,6 +45,21 @@ public function create(array $attributes = [], $sessionKey = null) public function add(Model $model, $sessionKey = null) { if ($sessionKey === null) { + /** + * @event model.relation.beforeAdd + * Called before adding a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations) + * + * Example usage: + * + * $model->bindEvent('model.relation.beforeAdd', function (string $relationName, \Winter\Storm\Database\Model $relatedModel) use (\Winter\Storm\Database\Model $model) { + * if ($relationName === 'dummyRelation') { + * throw new \Exception("Invalid relation!"); + * } + * }); + * + */ + $this->parent->fireEvent('model.relation.beforeAdd', [$this->relationName, $model]); + $model->setAttribute($this->getForeignKeyName(), $this->getParentKey()); $model->setAttribute($this->getMorphType(), $this->morphClass); $model->save(); @@ -58,6 +73,21 @@ public function add(Model $model, $sessionKey = null) else { $this->parent->reloadRelations($this->relationName); } + + /** + * @event model.relation.afterAdd + * Called after adding a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations) + * + * Example usage: + * + * $model->bindEvent('model.relation.afterAdd', function (string $relationName, \Winter\Storm\Database\Model $relatedModel) use (\Winter\Storm\Database\Model $model) { + * $relatedClass = get_class($relatedModel); + * $modelClass = get_class($model); + * traceLog("{$relatedClass} was added as {$relationName} to {$modelClass}."); + * }); + * + */ + $this->parent->fireEvent('model.relation.afterAdd', [$this->relationName, $model]); } else { $this->parent->bindDeferred($this->relationName, $model, $sessionKey); @@ -70,6 +100,21 @@ public function add(Model $model, $sessionKey = null) public function remove(Model $model, $sessionKey = null) { if ($sessionKey === null) { + /** + * @event model.relation.beforeRemove + * Called before removing a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations) + * + * Example usage: + * + * $model->bindEvent('model.relation.beforeRemove', function (string $relationName, \Winter\Storm\Database\Model $relatedModel) use (\Winter\Storm\Database\Model $model) { + * if ($relationName === 'permanentRelation') { + * throw new \Exception("Cannot dissociate a permanent relation!"); + * } + * }); + * + */ + $this->parent->fireEvent('model.relation.beforeRemove', [$this->relationName, $model]); + $options = $this->parent->getRelationDefinition($this->relationName); if (array_get($options, 'delete', false)) { @@ -93,6 +138,21 @@ public function remove(Model $model, $sessionKey = null) else { $this->parent->reloadRelations($this->relationName); } + + /** + * @event model.relation.afterRemove + * Called after removing a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations) + * + * Example usage: + * + * $model->bindEvent('model.relation.afterRemove', function (string $relationName, \Winter\Storm\Database\Model $relatedModel) use (\Winter\Storm\Database\Model $model) { + * $relatedClass = get_class($relatedModel); + * $modelClass = get_class($model); + * traceLog("{$relatedClass} was removed from {$modelClass}."); + * }); + * + */ + $this->parent->fireEvent('model.relation.afterRemove', [$this->relationName, $model]); } else { $this->parent->unbindDeferred($this->relationName, $model, $sessionKey); diff --git a/src/Database/Relations/MorphTo.php b/src/Database/Relations/MorphTo.php index 661cdc337..229c70efb 100644 --- a/src/Database/Relations/MorphTo.php +++ b/src/Database/Relations/MorphTo.php @@ -9,6 +9,7 @@ */ class MorphTo extends MorphToBase { + use Concerns\BelongsOrMorphsTo; use Concerns\DeferOneOrMany; use Concerns\DefinedConstraints; diff --git a/src/Database/Traits/HasSortableRelations.php b/src/Database/Traits/HasSortableRelations.php new file mode 100644 index 000000000..c3a65ba3b --- /dev/null +++ b/src/Database/Traits/HasSortableRelations.php @@ -0,0 +1,138 @@ + 'sort_order_column']; + * + * To set orders: + * + * $model->setSortableRelationOrder($relationName, $recordIds, $recordOrders); + * + */ +trait HasSortableRelations +{ + /** + * @var array The array of all sortable relations with their sort_order pivot column. + * + * public $sortableRelations = ['related_model' => 'sort_order']; + */ + + /** + * Initialize the HasSortableRelations trait for this model. + * Sets the sort_order value if a related model has been attached. + */ + public function initializeHasSortableRelations() : void + { + $sortableRelations = $this->getSortableRelations(); + + $this->bindEvent('model.relation.afterAttach', function ($relationName, $attached, $data) use ($sortableRelations) { + // Only for pivot-based relations + if (array_key_exists($relationName, $sortableRelations)) { + $column = $this->getRelationSortOrderColumn($relationName); + + foreach ($attached as $id) { + $this->updateRelationOrder($relationName, $id, $column); + } + } + }); + + $this->bindEvent('model.relation.afterAdd', function ($relationName, $relatedModel) use ($sortableRelations) { + // Only for non pivot-based relations + if (array_key_exists($relationName, $sortableRelations)) { + $column = $this->getRelationSortOrderColumn($relationName); + + $this->updateRelationOrder($relationName, $relatedModel->id, $column); + } + }); + + foreach ($sortableRelations as $relationName => $column) { + $relation = $this->$relationName(); + if (method_exists($relation, 'updateExistingPivot')) { + // Make sure all pivot-based defined sortable relations load the sort_order column as pivot data. + $definition = $this->getRelationDefinition($relationName); + $pivot = array_wrap(array_get($definition, 'pivot', [])); + + if (!in_array($column, $pivot)) { + $pivot[] = $column; + $definition['pivot'] = $pivot; + + $relationType = $this->getRelationType($relationName); + $this->$relationType[$relationName] = $definition; + } + } + } + } + + /** + * Set the sort order of records to the specified orders. If the orders is + * undefined, the record identifier is used. + */ + public function setRelationOrder(string $relationName, string|int|array $itemIds, array $itemOrders = []) : void + { + if (!is_array($itemIds)) { + $itemIds = [$itemIds]; + } + + if (empty($itemOrders)) { + $itemOrders = $itemIds; + } + + if (count($itemIds) != count($itemOrders)) { + throw new Exception('Invalid setRelationOrder call - count of itemIds do not match count of itemOrders'); + } + + $column = $this->getRelationSortOrderColumn($relationName); + + foreach ($itemIds as $index => $id) { + $order = (int)$itemOrders[$index]; + $this->updateRelationOrder($relationName, $id, $column, $order); + } + } + + /** + * Update relation record sort_order. + */ + protected function updateRelationOrder(string $relationName, int $id, string $column, int $order = 0) : void + { + $relation = $this->{$relationName}(); + + if (!$order) { + $order = $relation->count(); + } + if (method_exists($relation, 'updateExistingPivot')) { + $relation->updateExistingPivot($id, [ $column => (int)$order ]); + } else { + $record = $relation->find($id); + $record->{$column} = (int)$order; + $record->save(); + } + } + + /** + * Get the name of the "sort_order" column. + */ + public function getRelationSortOrderColumn(string $relationName) : string + { + return $this->getSortableRelations()[$relationName] ?? 'sort_order'; + } + + /** + * Return all configured sortable relations. + */ + protected function getSortableRelations() : array + { + if (property_exists($this, 'sortableRelations')) { + return $this->sortableRelations; + } + return []; + } +} From 49514acf496328e10cb56728dd2ab87bdb2159b2 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Wed, 27 Jul 2022 11:05:30 -0400 Subject: [PATCH 03/43] regenerate the resizer fixtures (#100) Regenerate resizer fixtures. --- ...stResize0x0_testResizeAutoLandscape1x1.png | Bin 6339 -> 6338 bytes .../target/testResizeAutoExifRotated30x30.jpg | Bin 864 -> 859 bytes .../target/testResizeAutoLandscape1x5.png | Bin 533 -> 437 bytes .../target/testResizeAutoLandscape25.png | Bin 366 -> 310 bytes .../target/testResizeAutoLandscape25x1.png | Bin 366 -> 310 bytes ...toSquare50x50_testResizeFitSquare50x50.jpg | Bin 1825 -> 1818 bytes .../resizer/target/testResizeExact10x15.jpg | Bin 836 -> 839 bytes .../target/testResizeFitLandscape30x30.png | Bin 533 -> 437 bytes tests/fixtures/resizer/target/testSharpen.jpg | Bin 1128 -> 1128 bytes 9 files changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/fixtures/resizer/target/testReset_testResize0x0_testResizeAutoLandscape1x1.png b/tests/fixtures/resizer/target/testReset_testResize0x0_testResizeAutoLandscape1x1.png index 14893fd8ef3ad31244dec0a93eff7cd9898d0efa..8d820ac3226a1d3728252e63a0e29b6b35f77655 100644 GIT binary patch delta 6052 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 eU*K*!^#6gv&gRHW4h^CJ0000yD|2&_ho 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 f^M&rFL;nwcZO-P_kPb`?00000NkvXXu0mjf6_ diff --git a/tests/fixtures/resizer/target/testResizeAutoExifRotated30x30.jpg b/tests/fixtures/resizer/target/testResizeAutoExifRotated30x30.jpg index 96a901283e5fe71fc5e6102074c2e762baea30a2..e4e18394de29bd22610504f93a97e48cfdfcbb88 100644 GIT binary patch delta 188 zcmV;t07L)a2HOUZKLI$gKy3nlOOFvYlP05k{hwiZ{{RT(*wg;ccMq8kskqL*ed_1; z(=ObH;{}-Gnru$mW~FVWl5WnmaTGsox;yS{HW=G|tN#GYHg7WwaB>j`YACBY4jRK$ zDWf)Rt(|AmXSdq%JJdYiC*XneFzxc@g5;1@jZp&N5G~PAlB}8{v<&__tHj6&YZe{xV9_CLuQ>{5g>&Y+Ex@Z5{y%b?5 diff --git a/tests/fixtures/resizer/target/testResizeAutoLandscape1x5.png b/tests/fixtures/resizer/target/testResizeAutoLandscape1x5.png index 7104eae668036919ecfb40049421d7bda4a0657d..76134f2974ad590672103075e45f8afb7eb4fd9f 100644 GIT binary patch delta 389 zcmV;00eb$G1hoT@Ie%wKL_t(26*bb$D@0Kk$MJLTNRlQ=nZ@K~$P}U`vydny6e$Z4 z8x}K@gulUR7P3>CDGN;+FXd%Nl8u*Xq$CtsXwqac3u&5#G~@fzSbXkt&pG!w&pGEF zYcd#WHpXnA1y=}Q3iJ3u7!_#18CFn@UF;x^9A0sb1t{+UK7VA9^jqnOaVr&vM{hB1aZc%UL7G(wZANG@+%UZOV8jl^~|qVZ*D{tool+n-@#FI-sH zKqvO&(TG-gfHovaPD8t>#UeCumc9#U7bj40x1BFs6H9TAAx`m;sJu#S;mvDh+{ABa z#{(+-_wjfz$bPFCgo^n{u7W;(!CJB3NuO^b5GteH2j#xAkgnqR2=SP9D=`Hv@D_H7 z$*ZlxcSu-F)Xs~J+f(eECeGmhU+IXn8y%ni;VPUsL=1b#-~+k?%KO3-nv5xM)YhfT j%Esm$H-7qbUzUCW^~P-*muo~=00000NkvXXu0mjfb)>a6 delta 486 zcmVS|NC}~cpoI`Mkp#3D2yLMh1wk~R z2reX2H@dh9CB*y5kg7Qf{&ddAsZi|B9x*oLKY=x=|Vwl$fCrIzl)1u z9^afZXXewan@^}%RYWg|*as1DMRnOs3ZgO=ld`CV@E7Q5(|_|!MNXUOma4SOtN~_3 zZ(mJ2rdA{$V_(3e@k97_7WBu;U8h%tYsyR?JAiAZ@(SP?QyoD5R?JEOt}@^@@As&k z!^aTVP}MxWam*(z;zxGENz#)bUsBO-%k$#oPl0j*WYDCT|>-SFzXIO=G5eQ z)7$X3dx3~qUw>h%vT;DIPc6#(6FkH^r1q}qc|&S%7~~I+=-qT*;HJa$JsL9iU6-2N0?;?HG+%Hk>*gY;>>+0De_g`VdCQ$7 zzm73TzPBpgBH%vp9sFyAT_zrIG0tqC5#t18GGKcK+g~m{U4`Td4K;?w*lL$^KO}#I zn4+4E$;*%gQ5ol7JH)G47<)-yoj=|t{>c{{f^Q?Ti2noMH{LID(G(o-XXL0}2z;7{ cBLr9J9}>{DDasHj0{{R307*qoM6N<$g38(CLjV8( diff --git a/tests/fixtures/resizer/target/testResizeAutoLandscape25.png b/tests/fixtures/resizer/target/testResizeAutoLandscape25.png index 009b6ec227241148b8e490a6b010b141218ea496..d8afea488dd1db46f9f88d55588b591e911a9315 100644 GIT binary patch delta 262 zcmV+h0r~#!0=5E>IDY`>NklP)ahJ*;(-pW~D4-W6GYD zWHCvTEG#8+lNVrZGkXgQc?D`x%1V>0)D%;M@6T}R^E~I=`}{fgI&Yc&d(E1-0#2yhWki|Dd9MD7OdIAjiEjoV9g zqfQhJJYxzSpx^ZZeaQrh4`@#TTj=6fMDKO+*ZiY{`91@7Oz^CB1(TpPQs0ZBZvX%Q M07*qoM6N<$g1g*wRR910 delta 319 zcmV-F0l@yY0`3BkIDY{kNkl%iQY8(527UB~KA_!I@l1dO%$Z_NE6F+X5{Wr6_r``R7ElVARgYP1w zMLRa6s2uK+>nU_bu@yWk`alPJOR=mk%XQo*dN;vGH11DpH-9f^971U3kOoj24*i7U znWlHFfvq9lq*hPD_^YfzOFRH2eh+M8ALK^xuPJVaeNv0+QqlrG13k>-$e30lRzb)h zR7uHpx@4Lh!Czd;F1hmmIj1cnm}PVhq7wz&K@pQ7+&OZuX{{U%+jzvL^!SK)nW1-t z2&l^@D-`Tz^#|%nWt&^LFGv@d0ea6hyx2MhvO_tDX{Q{5=c1IrL R1oZ#_002ovPDHLkV1k3&|v diff --git a/tests/fixtures/resizer/target/testResizeAutoLandscape25x1.png b/tests/fixtures/resizer/target/testResizeAutoLandscape25x1.png index 009b6ec227241148b8e490a6b010b141218ea496..d8afea488dd1db46f9f88d55588b591e911a9315 100644 GIT binary patch delta 262 zcmV+h0r~#!0=5E>IDY`>NklP)ahJ*;(-pW~D4-W6GYD zWHCvTEG#8+lNVrZGkXgQc?D`x%1V>0)D%;M@6T}R^E~I=`}{fgI&Yc&d(E1-0#2yhWki|Dd9MD7OdIAjiEjoV9g zqfQhJJYxzSpx^ZZeaQrh4`@#TTj=6fMDKO+*ZiY{`91@7Oz^CB1(TpPQs0ZBZvX%Q M07*qoM6N<$g1g*wRR910 delta 319 zcmV-F0l@yY0`3BkIDY{kNkl%iQY8(527UB~KA_!I@l1dO%$Z_NE6F+X5{Wr6_r``R7ElVARgYP1w zMLRa6s2uK+>nU_bu@yWk`alPJOR=mk%XQo*dN;vGH11DpH-9f^971U3kOoj24*i7U znWlHFfvq9lq*hPD_^YfzOFRH2eh+M8ALK^xuPJVaeNv0+QqlrG13k>-$e30lRzb)h zR7uHpx@4Lh!Czd;F1hmmIj1cnm}PVhq7wz&K@pQ7+&OZuX{{U%+jzvL^!SK)nW1-t z2&l^@D-`Tz^#|%nWt&^LFGv@d0ea6hyx2MhvO_tDX{Q{5=c1IrL R1oZ#_002ovPDHLkV1k3&|v diff --git a/tests/fixtures/resizer/target/testResizeAutoSquare25x50_testResizeAutoSquare50x25_testResizeAutoSquare50x50_testResizeFitSquare50x50.jpg b/tests/fixtures/resizer/target/testResizeAutoSquare25x50_testResizeAutoSquare50x25_testResizeAutoSquare50x50_testResizeFitSquare50x50.jpg index d2c6fd58ca1a34b956d99b2db264eb66947ebb57..6175abfcd1084f8bba067832117119e7014ec07b 100644 GIT binary patch delta 1132 zcmV-y1e5!r4w?>-KLI$gKy3nl75G!}Q%Lx^;g9%8G-#)a)9npz7Ug!f-~l`j_p6}s zH;Qzh2x*dP8pfdppJ^B*%R2x^(>3#7?1$pp9|Hc(I=;K9%COp5YIia)C$H|J+JO3# zfnO(lVffFjd}8q|`j)A3(byv}wy_QSxD&A>x%D-6Ii)oenf6bKzi!`u0ch6-D+qNh zbfe(CtN|1cW?}*SDxQz~fA}j=o6Eh@b!{>{{{XF#5x^d#D`viR_+R@Sc-!JWk!z_; zBG7c}h1`-~qZvr-!2t&znIgI`7=FRNHSp%0E!LZ;*z4;O7iiEl5)eAz9D;H0PeL4~ z+4kRoekOQd;#HE~_;*%+hW&Atja%nWs-8t^c(cT|z7O!$qpjWsYg-sh(g^>b zcyGlz{{X{36fM<+w;DdPV=<0sT$RC7gB*$c!LPgz#v5;p-?HzEHF(iry-hB7uPw*$ zNEfK~J!r9NcQgM0;k`Z|ceU2P@6i7M8Y}bKXJ0x`n0@2kigqJ^+Q{~6{{RD zW`him8Pq&Mb1b1x4;*mBe^zDut2_2+@H9R?_{-tlD)KQcj8i?->x}LZ0Qf)bmcaaK z9d}7{@B_k{m<6G?*6;0D_AJptRDK~dU1#iFuEU~yd(x79tM==8?3A(Sb_dVmR}>R_ zw0@I85<-fRph(ewLaG6PpmYMB{{VNtQPPhweQ0L@b*#>nx%v6~cX%S#LGk{N;u}as zxAxV$CCpK-6w4C;&-E%$XZ?}uTqnR!7TsyO7sQPiNKKZZ;pydQSnwo|7%TM+gY>Us z{{Vu8>M+f!_%lyJt#Fat$W$JR%^uVE?LU=xC&4=zF6Z%ogrQr?hr@S{?~VY%<29kq z)uee$%bBD({v$<8EUdCP^12i0MFuC?KL!34AB!Kd7O|v4yR{Y7=DD*Y8CeoIu(>|C z$Kzir+s&(ZE5(schD|eB(=Jt`$N}M%2GL=m6bq$|{3H6rU@YqEx);Y*MgxF*gM(c!j6ZBIhQ1xrrkdMW8r{y{-P+izF>{`U zfWrPY@=m+^8GKOCezOU+-eQ+v3NEFFZ%^$Hclen+i4E<%E{j z@nk66R{&;O!__=U;{Y0I}DNz9Z=t z+LXdA2Tr(M=_UFxl#a|05OL{}E2Hs`>>cA@4r%h;X_}3WzOf;8jRQd;1E>I!3H)lb zArXNEo{SuO8>hqY*R1;$!6VZA=8c@?SR-xAvRKf_v%w|E_`Y+*AC)O#MZtXiGS{{Z-JKZmESuCx#RaUbJFetS;C&XbY%kAHf9DcFx|BiZl#T`r&d zE@_$!F43Js#1}Hk6!7uK3`g~6U&^z8W{(3yf|) z#-i6Wmp=eJC#k3{6~4EBYR9o=iV~yo37YFaV(oS<7vt8Hv+ZBEK|PX|>~;sw;#U+C zd$n`)mI#s*M2!M}Mv4_w3c1xI%aHd$827hu?pwIgt_pTG*CyQ?M?F-^Yj2!)oQ1JBfG%R=$M~oHvhQaz* zvHt+ULiH$R)qEMJqXxK0?qy{MqO(V|{yR_QUJ3A0#qQ^S@qD3Al@EsR9p4-Qg~n?` zovTRliPJMkbJdUYqNbKs5;*d@6W)ppPqKas{475fKV%(aNQHN5E340QXht%!BynMK zeQ}S*zE!uIR`6GgBDg~)o2==VD$(R&;FSpH2d__|uhH+>H{ss7@XO(A=^h}p7MIO` zEx?R8ZLgaO1U^oZ3Jo;Bl;}6>l;jf1@siwBpMz?dfG1}QH zF>{`Uv4#A7E99Mf_A>aPq0Gr`rd#U~D-}s>u2gS-Uc`qZ2D9}40ND%TmZ30;PYx}S zegu~nkp&-JjKuy_XvKMUIDd+N5_La{{x#HmN2>gjU7e9zD`g;vaFMUSLQi`7YvC`# zD~|(y%|8%)L8?H|>YB!xDM;LW(ZUyl?YNxcy6=Ktu@;@++xvZE#kXJB#D5GtOQUJ9qg~!uNpEisLXE|6257jW2dtU-9Y6Z{Ki)siiuTz* zV_%B0NX$Gwm|{rH)PLheGcH+nKTT0WrvMM7NYqhTov|aB4TR&6I5ktuZKvixofK7c bB()cF+|$oAQAq&LCNs%JC+S5LQv?6mB;`#d diff --git a/tests/fixtures/resizer/target/testResizeExact10x15.jpg b/tests/fixtures/resizer/target/testResizeExact10x15.jpg index b18745587d2c2855454d49d9276a1f9bddb72714..dbaf7b4f2e071e247f0286a96cf8763e973819c6 100644 GIT binary patch delta 168 zcmV;Z09XIS2FC`FKLI$gKy3nlQ%&(d#NUX%5%C|yEgQxyq*!=hJT|&Hwpk<8KF<=g z?lYH=+Uu8gcGpa2irGjaBxATB=Q;O)=(;Qss7o^Nt&C(0b;v?*9O04-#qDb6RN|+GI27@H<6q WE8IpI;!}hqZs3pwXsmK-fB)H%TvCq! delta 165 zcmV;W09yaY2E+!CKLI(hKy3nlPfhVZ#NUYC5b+x+pTjqKeShf^^1vb?RDF$D{H4S#cZSz5;5Epr(i(*E47Q^FU4;LXjfkJzW6c_+%Iyc|95&sc_042m{>%O))9&WA)3nGKXVc(zirQDW Tj5EZi2uR(*AOl()uIK;RGL~2i diff --git a/tests/fixtures/resizer/target/testResizeFitLandscape30x30.png b/tests/fixtures/resizer/target/testResizeFitLandscape30x30.png index 7104eae668036919ecfb40049421d7bda4a0657d..76134f2974ad590672103075e45f8afb7eb4fd9f 100644 GIT binary patch delta 389 zcmV;00eb$G1hoT@Ie%wKL_t(26*bb$D@0Kk$MJLTNRlQ=nZ@K~$P}U`vydny6e$Z4 z8x}K@gulUR7P3>CDGN;+FXd%Nl8u*Xq$CtsXwqac3u&5#G~@fzSbXkt&pG!w&pGEF zYcd#WHpXnA1y=}Q3iJ3u7!_#18CFn@UF;x^9A0sb1t{+UK7VA9^jqnOaVr&vM{hB1aZc%UL7G(wZANG@+%UZOV8jl^~|qVZ*D{tool+n-@#FI-sH zKqvO&(TG-gfHovaPD8t>#UeCumc9#U7bj40x1BFs6H9TAAx`m;sJu#S;mvDh+{ABa z#{(+-_wjfz$bPFCgo^n{u7W;(!CJB3NuO^b5GteH2j#xAkgnqR2=SP9D=`Hv@D_H7 z$*ZlxcSu-F)Xs~J+f(eECeGmhU+IXn8y%ni;VPUsL=1b#-~+k?%KO3-nv5xM)YhfT j%Esm$H-7qbUzUCW^~P-*muo~=00000NkvXXu0mjfb)>a6 delta 486 zcmVS|NC}~cpoI`Mkp#3D2yLMh1wk~R z2reX2H@dh9CB*y5kg7Qf{&ddAsZi|B9x*oLKY=x=|Vwl$fCrIzl)1u z9^afZXXewan@^}%RYWg|*as1DMRnOs3ZgO=ld`CV@E7Q5(|_|!MNXUOma4SOtN~_3 zZ(mJ2rdA{$V_(3e@k97_7WBu;U8h%tYsyR?JAiAZ@(SP?QyoD5R?JEOt}@^@@As&k z!^aTVP}MxWam*(z;zxGENz#)bUsBO-%k$#oPl0j*WYDCT|>-SFzXIO=G5eQ z)7$X3dx3~qUw>h%vT;DIPc6#(6FkH^r1q}qc|&S%7~~I+=-qT*;HJa$JsL9iU6-2N0?;?HG+%Hk>*gY;>>+0De_g`VdCQ$7 zzm73TzPBpgBH%vp9sFyAT_zrIG0tqC5#t18GGKcK+g~m{U4`Td4K;?w*lL$^KO}#I zn4+4E$;*%gQ5ol7JH)G47<)-yoj=|t{>c{{f^Q?Ti2noMH{LID(G(o-XXL0}2z;7{ cBLr9J9}>{DDasHj0{{R307*qoM6N<$g38(CLjV8( diff --git a/tests/fixtures/resizer/target/testSharpen.jpg b/tests/fixtures/resizer/target/testSharpen.jpg index c7c93eb51dc1ff45f0f1253d94abc6fe46330ba7..a9b1a1875a8dead1753027fdecbc89d3cc5bd037 100644 GIT binary patch delta 459 zcmV;+0W|*T2wAZxw{8?k+1pdO27G$=EMvO-y5D3M)+WB&jIhyMVNS8MSC%``9Dr^FEHaU=-Zp*tcNQr8+fwbAE4c+WGRLd5Zo zJ~Q&;_x}LDZB&t*rzNF-&(NP6e#xFA_>1r};ctkv8*80E$9gY@>~#Cx8ui-Bd6E?^ z7*>23XNpF0rHCIlBo$hv2kkNYOzFB>=+{3EFRwLakI6cu-VeWbEsSqwiN4NXIpuJj z4l!SPj)t7yaQ!MSZ^_j&;opJZvbT%$m^@eUAIH$n-YdA%wHY)%-7k90c5bdAm1Ve? z#g+3MNXoIMLWvk)90mBV{uGDA-})v#Hva&^a`XQHjo0mzT|eMQ`I;%FX5g%m|JkGX B@3H^@ delta 459 zcmV;+0W|*T2>dxXiaAqqJ*(T=$|ED8SDsl^{{Xzz`lwic4c|9ETD^{C zdG$V9_$B*f$MIjno+Poqv$oK@d!^}DI)o8M8Le+bQJv2kzs!t}v^*?H1CY)0ZTTzy z9vktq{(?Rj{{X=u{{Z9F+WbMYO$+wv@dP?tX#zH?Nl1niwa$*MbbsJRd6@bZCya6N zp4j~l{rhk5sz}aLlG5jY=vR%uWX}?OM)()-x5Qcvwa%a8y%)oFI(_bqdhKOA$r6@~ zD?U7#;*p%GY!92LDz!!*+GF;a(=@fwZhjlzUTVu9l66UZAAasz7~ahjeVo2?%HcQ% z7_YrM52Zc7)t{wB&G~w!d^_+v_EqtIkq?UgBl!9m+r^g}cB3Xfo27?YuFciNGKp>` zF=c$m5;Cl5kfKHy2LXO6{{V#{@jL#BmyN&hoV@=4<8}LI7eoH>{$`e%WIAc1|JlYD B>BIm4 From 71d6e706815a92a687673dd35f76e5d17ae98502 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Wed, 27 Jul 2022 12:57:59 -0400 Subject: [PATCH 04/43] Use extension if provided in resizer options (#99) This is required to allow converting images to other formats (e.g. webp). Otherwise the resulting file will have the correct extension but the wrong image format. Fixes #9 & #74 --- src/Database/Attach/Resizer.php | 10 +++++++++- tests/Database/Attach/ResizerTest.php | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/Database/Attach/Resizer.php b/src/Database/Attach/Resizer.php index f0f199496..3851c10c1 100644 --- a/src/Database/Attach/Resizer.php +++ b/src/Database/Attach/Resizer.php @@ -453,7 +453,7 @@ public function save($savePath) } // Determine the image type from the destination file - $extension = pathinfo($savePath, PATHINFO_EXTENSION) ?: $this->extension; + $extension = $this->getExtension($savePath); // Create and save an image based on it's extension switch (strtolower($extension)) { @@ -697,4 +697,12 @@ protected function getSizeByFit($maxWidth, $maxHeight) return [$optimalWidth, $optimalHeight]; } + + /** + * Get the extension from the options, otherwise use the filename extension + */ + protected function getExtension(string $path): string + { + return $this->getOption('extension') ?: (pathinfo($path, PATHINFO_EXTENSION) ?: $this->extension); + } } diff --git a/tests/Database/Attach/ResizerTest.php b/tests/Database/Attach/ResizerTest.php index b2a14d95c..d7792939d 100644 --- a/tests/Database/Attach/ResizerTest.php +++ b/tests/Database/Attach/ResizerTest.php @@ -72,6 +72,29 @@ public function testReset() $this->assertImageSameAsFixture(self::COMMON_FIXTURES['reset']); } + /** + * Given a Resizer with any image + * Test to see if the getExtension() method properly returns the filename extension for the provided filename + */ + public function testGetExtension() + { + $this->setSource(self::SRC_PORTRAIT); // gif extension + $this->createFixtureResizer(); + + // no extension provided in path, no extension provided in options, should return source extension. + $extension = $this->callProtectedMethod($this->resizer, 'getExtension', ['dummy']); + $this->assertEquals('gif', $extension); + + // no extension provided in options, extension provided in path, should return path extension + $extension = $this->callProtectedMethod($this->resizer, 'getExtension', ['dummy.jpg']); + $this->assertEquals('jpg', $extension); + + // extension provided in options and in path, should return extension from options + $this->resizer->setOptions(['extension' => 'png']); + $extension = $this->callProtectedMethod($this->resizer, 'getExtension', ['dummy.jpg']); + $this->assertEquals('png', $extension); + } + /** * Given a Resizer with any image * When the resize method is called with 0x0 From 87009af328881366259eea1bbef85979178db667 Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Mon, 1 Aug 2022 15:50:53 +0800 Subject: [PATCH 05/43] Switch to external config writer library (#102) Uses the https://github.com/wintercms/laravel-config-writer repo to provide configuration modification. The original classes in Storm have been stubbed so that they continue to work as documented. --- composer.json | 3 +- src/Config/ConfigWriter.php | 2 - src/Parse/EnvFile.php | 240 +----------------- src/Parse/PHP/ArrayFile.php | 430 +-------------------------------- src/Parse/PHP/ArrayPrinter.php | 316 ------------------------ src/Parse/PHP/PHPConstant.php | 24 +- src/Parse/PHP/PHPFunction.php | 46 +--- tests/Parse/ArrayFileTest.php | 2 +- 8 files changed, 14 insertions(+), 1049 deletions(-) delete mode 100644 src/Parse/PHP/ArrayPrinter.php diff --git a/composer.json b/composer.json index 163a5a278..5eb7d5ab6 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,8 @@ "symfony/yaml": "^6.0", "twig/twig": "~3.0", "wikimedia/less.php": "~3.0", - "wikimedia/minify": "~2.2" + "wikimedia/minify": "~2.2", + "winter/laravel-config-writer": "^1.0.0" }, "require-dev": { "phpunit/phpunit": "^9.5.8", diff --git a/src/Config/ConfigWriter.php b/src/Config/ConfigWriter.php index 75f40831a..a609a498a 100644 --- a/src/Config/ConfigWriter.php +++ b/src/Config/ConfigWriter.php @@ -1,7 +1,5 @@ filePath = $filePath; - - list($this->env, $this->map) = $this->parse($filePath); - } - - /** - * Return a new instance of `EnvFile` ready for modification of the file. - */ - public static function open(?string $filePath = null): static - { - if (!$filePath) { - $filePath = base_path('.env'); - } - - return new static($filePath); - } - - /** - * Set a property within the env. Passing an array as param 1 is also supported. - * - * ```php - * $env->set('APP_PROPERTY', 'example'); - * // or - * $env->set([ - * 'APP_PROPERTY' => 'example', - * 'DIF_PROPERTY' => 'example' - * ]); - * ``` - */ - public function set(array|string $key, $value = null): static - { - if (is_array($key)) { - foreach ($key as $item => $value) { - $this->set($item, $value); - } - return $this; - } - - if (!isset($this->map[$key])) { - $this->env[] = [ - 'type' => 'var', - 'key' => $key, - 'value' => $value - ]; - - $this->map[$key] = count($this->env) - 1; - - return $this; - } - - $this->env[$this->map[$key]]['value'] = $value; - - return $this; - } - - /** - * Push a newline onto the end of the env file - */ - public function addEmptyLine(): EnvFile - { - $this->env[] = [ - 'type' => 'nl' - ]; - - return $this; - } - - /** - * Write the current env lines to a fileh - */ - public function write(string $filePath = null): void - { - if (!$filePath) { - $filePath = $this->filePath; - } - - file_put_contents($filePath, $this->render()); - } - - /** - * Get the env lines data as a string - */ - public function render(): string - { - $out = ''; - foreach ($this->env as $env) { - switch ($env['type']) { - case 'comment': - $out .= $env['value']; - break; - case 'var': - $out .= $env['key'] . '=' . $this->escapeValue($env['value']); - break; - } - - $out .= PHP_EOL; - } - - return $out; - } - - /** - * Wrap a value in quotes if needed - * - * @param mixed $value - */ - protected function escapeValue($value): string - { - if (is_numeric($value)) { - return $value; - } - - if ($value === true) { - return 'true'; - } - - if ($value === false) { - return 'false'; - } - - if ($value === null) { - return 'null'; - } - - switch ($value) { - case 'true': - case 'false': - case 'null': - return $value; - default: - // addslashes() wont work as it'll escape single quotes and they will be read literally - return '"' . Str::replace('"', '\"', $value) . '"'; - } - } - - /** - * Parse a .env file, returns an array of the env file data and a key => position map - */ - protected function parse(string $filePath): array - { - if (!is_file($filePath)) { - return [[], []]; - } - - $contents = file($filePath); - if (empty($contents)) { - return [[], []]; - } - - $env = []; - $map = []; - - foreach ($contents as $line) { - $type = !($line = trim($line)) - ? 'nl' - : ( - Str::startsWith($line, '#') - ? 'comment' - : 'var' - ); - - $entry = [ - 'type' => $type - ]; - - if ($type === 'var') { - if (strpos($line, '=') === false) { - // if we cannot split the string, handle it the same as a comment - // i.e. inject it back into the file as is - $entry['type'] = $type = 'comment'; - } else { - list($key, $value) = explode('=', $line); - $entry['key'] = trim($key); - $entry['value'] = trim($value, '"'); - } - } - - if ($type === 'comment') { - $entry['value'] = $line; - } - - $env[] = $entry; - } - - foreach ($env as $index => $item) { - if ($item['type'] !== 'var') { - continue; - } - $map[$item['key']] = $index; - } - - return [$env, $map]; - } - - /** - * Get the variables from the current env lines data as an associative array - */ - public function getVariables(): array - { - $env = []; - - foreach ($this->env as $item) { - if ($item['type'] !== 'var') { - continue; - } - $env[$item['key']] = $item['value']; - } - - return $env; - } } diff --git a/src/Parse/PHP/ArrayFile.php b/src/Parse/PHP/ArrayFile.php index c0b2e7a18..5a9deb43b 100644 --- a/src/Parse/PHP/ArrayFile.php +++ b/src/Parse/PHP/ArrayFile.php @@ -1,435 +1,9 @@ astReturnIndex = $this->getAstReturnIndex($ast); - - if (is_null($this->astReturnIndex)) { - throw new \InvalidArgumentException('ArrayFiles must start with a return statement'); - } - - $this->ast = $ast; - $this->lexer = $lexer; - $this->filePath = $filePath; - $this->printer = $printer ?? new ArrayPrinter(); - } - - /** - * Return a new instance of `ArrayFile` ready for modification of the file. - * - * @throws \InvalidArgumentException if the provided path doesn't exist and $throwIfMissing is true - * @throws SystemException if the provided path is unable to be parsed - */ - public static function open(string $filePath, bool $throwIfMissing = false): static - { - $exists = file_exists($filePath); - - if (!$exists && $throwIfMissing) { - throw new \InvalidArgumentException('file not found'); - } - - $lexer = new Lexer\Emulative([ - 'usedAttributes' => [ - 'comments', - 'startTokenPos', - 'startLine', - 'endTokenPos', - 'endLine' - ] - ]); - $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7, $lexer); - - try { - $ast = $parser->parse( - $exists - ? file_get_contents($filePath) - : sprintf('set('property.key.value', 'example'); - * // or - * $config->set([ - * 'property.key1.value' => 'example', - * 'property.key2.value' => 'example' - * ]); - * ``` - */ - public function set(string|array $key, $value = null): static - { - if (is_array($key)) { - foreach ($key as $name => $value) { - $this->set($name, $value); - } - - return $this; - } - - // try to find a reference to ast object - list($target, $remaining) = $this->seek(explode('.', $key), $this->ast[$this->astReturnIndex]->expr); - - $valueType = $this->getType($value); - - // part of a path found - if ($target && $remaining) { - $target->value->items[] = $this->makeArrayItem(implode('.', $remaining), $valueType, $value); - return $this; - } - - // path to not found - if (is_null($target)) { - $this->ast[$this->astReturnIndex]->expr->items[] = $this->makeArrayItem($key, $valueType, $value); - return $this; - } - - if (!isset($target->value)) { - return $this; - } - - // special handling of function objects - if (get_class($target->value) === FuncCall::class && $valueType !== 'function') { - if ($target->value->name->parts[0] !== 'env' || !isset($target->value->args[0])) { - return $this; - } - if (isset($target->value->args[0]) && !isset($target->value->args[1])) { - $target->value->args[1] = new Arg($this->makeAstNode($valueType, $value)); - } - $target->value->args[1]->value = $this->makeAstNode($valueType, $value); - return $this; - } - - // default update in place - $target->value = $this->makeAstNode($valueType, $value); - - return $this; - } - - /** - * Creates either a simple array item or a recursive array of items - */ - protected function makeArrayItem(string $key, string $valueType, $value): ArrayItem - { - return (str_contains($key, '.')) - ? $this->makeAstArrayRecursive($key, $valueType, $value) - : new ArrayItem( - $this->makeAstNode($valueType, $value), - $this->makeAstNode($this->getType($key), $key) - ); - } - - /** - * Generate an AST node, using `PhpParser` classes, for a value - * - * @throws \RuntimeException If $type is not one of 'string', 'boolean', 'integer', 'function', 'const', 'null', or 'array' - * @return ConstFetch|LNumber|String_|Array_|FuncCall - */ - protected function makeAstNode(string $type, $value) - { - switch (strtolower($type)) { - case 'string': - return new String_($value); - case 'boolean': - return new ConstFetch(new Name($value ? 'true' : 'false')); - case 'integer': - return new LNumber($value); - case 'function': - return new FuncCall( - new Name($value->getName()), - array_map(function ($arg) { - return new Arg($this->makeAstNode($this->getType($arg), $arg)); - }, $value->getArgs()) - ); - case 'const': - return new ConstFetch(new Name($value->getName())); - case 'null': - return new ConstFetch(new Name('null')); - case 'array': - return $this->castArray($value); - default: - throw new \RuntimeException("An unimlemented replacement type ($type) was encountered"); - } - } - - /** - * Cast an array to AST - */ - protected function castArray(array $array): Array_ - { - return ($caster = function ($array, $ast) use (&$caster) { - $useKeys = []; - foreach (array_keys($array) as $i => $key) { - $useKeys[$key] = (!is_numeric($key) || $key !== $i); - } - foreach ($array as $key => $item) { - if (is_array($item)) { - $ast->items[] = new ArrayItem( - $caster($item, new Array_()), - ($useKeys[$key] ? $this->makeAstNode($this->getType($key), $key) : null) - ); - continue; - } - $ast->items[] = new ArrayItem( - $this->makeAstNode($this->getType($item), $item), - ($useKeys[$key] ? $this->makeAstNode($this->getType($key), $key) : null) - ); - } - - return $ast; - })($array, new Array_()); - } - - /** - * Returns type of var passed - * - * @param mixed $var - */ - protected function getType($var): string - { - if ($var instanceof PHPFunction) { - return 'function'; - } - - if ($var instanceof PHPConstant) { - return 'const'; - } - - return gettype($var); - } - - /** - * Returns an ArrayItem generated from a dot notation path - * - * @param string $key - * @param string $valueType - * @param mixed $value - */ - protected function makeAstArrayRecursive(string $key, string $valueType, $value): ArrayItem - { - $path = array_reverse(explode('.', $key)); - - $arrayItem = $this->makeAstNode($valueType, $value); - - foreach ($path as $index => $pathKey) { - if (is_numeric($pathKey)) { - $pathKey = (int) $pathKey; - } - $arrayItem = new ArrayItem($arrayItem, $this->makeAstNode($this->getType($pathKey), $pathKey)); - - if ($index !== array_key_last($path)) { - $arrayItem = new Array_([$arrayItem]); - } - } - - return $arrayItem; - } - - /** - * Find the return position within the ast, returns null on encountering an unsupported ast stmt. - * - * @param array $ast - * @return int|null - */ - protected function getAstReturnIndex(array $ast): ?int - { - foreach ($ast as $index => $item) { - switch (get_class($item)) { - case Stmt\Use_::class: - case Stmt\Expression::class: - break; - case Stmt\Return_::class: - return $index; - default: - return null; - } - } - - return null; - } - - /** - * Attempt to find the parent object of the targeted path. - * If the path cannot be found completely, return the nearest parent and the remainder of the path - * - * @param array $path - * @param mixed $pointer - * @param int $depth - * @throws SystemException if trying to set a position that is already occupied by a value - */ - protected function seek(array $path, &$pointer, int $depth = 0): array - { - if (!$pointer) { - return [null, $path]; - } - - $key = array_shift($path); - - if (isset($pointer->value) && !($pointer->value instanceof ArrayItem || $pointer->value instanceof Array_)) { - throw new SystemException(sprintf( - 'Illegal offset, you are trying to set a position occupied by a value (%s)', - get_class($pointer->value) - )); - } - - foreach (($pointer->items ?? $pointer->value->items) as $index => &$item) { - // loose checking to allow for int keys - if ($item->key->value == $key) { - if (!empty($path)) { - return $this->seek($path, $item, ++$depth); - } - - return [$item, []]; - } - } - - array_unshift($path, $key); - - return [($depth > 0) ? $pointer : null, $path]; - } - - /** - * Sort the config, supports: ArrayFile::SORT_ASC, ArrayFile::SORT_DESC, callable - * - * @param string|callable $mode - * @throws \InvalidArgumentException if the provided sort type is not a callable or one of static::SORT_ASC or static::SORT_DESC - */ - public function sort($mode = self::SORT_ASC): ArrayFile - { - if (is_callable($mode)) { - usort($this->ast[0]->expr->items, $mode); - return $this; - } - - switch ($mode) { - case static::SORT_ASC: - case static::SORT_DESC: - $this->sortRecursive($this->ast[0]->expr->items, $mode); - break; - default: - throw new \InvalidArgumentException('Requested sort type is invalid'); - } - - return $this; - } - - /** - * Recursive sort an Array_ item array - */ - protected function sortRecursive(array &$array, string $mode): void - { - foreach ($array as &$item) { - if (isset($item->value) && $item->value instanceof Array_) { - $this->sortRecursive($item->value->items, $mode); - } - } - - usort($array, function ($a, $b) use ($mode) { - return $mode === static::SORT_ASC - ? $a->key->value <=> $b->key->value - : $b->key->value <=> $a->key->value; - }); - } - - /** - * Write the current config to a file - */ - public function write(string $filePath = null): void - { - if (!$filePath && $this->filePath) { - $filePath = $this->filePath; - } - - file_put_contents($filePath, $this->render()); - } - - /** - * Returns a new instance of PHPFunction - */ - public function function(string $name, array $args): PHPFunction - { - return new PHPFunction($name, $args); - } - - /** - * Returns a new instance of PHPConstant - */ - public function constant(string $name): PHPConstant - { - return new PHPConstant($name); - } - - /** - * Get the printed AST as PHP code - */ - public function render(): string - { - return $this->printer->render($this->ast, $this->lexer) . "\n"; - } - - /** - * Get currently loaded AST - * - * @return Stmt[]|null - */ - public function getAst() - { - return $this->ast; - } } diff --git a/src/Parse/PHP/ArrayPrinter.php b/src/Parse/PHP/ArrayPrinter.php deleted file mode 100644 index 81f967daf..000000000 --- a/src/Parse/PHP/ArrayPrinter.php +++ /dev/null @@ -1,316 +0,0 @@ -lexer = $lexer; - - $p = "prettyPrint($stmts); - - if ($stmts[0] instanceof Stmt\InlineHTML) { - $p = preg_replace('/^<\?php\s+\?>\n?/', '', $p); - } - if ($stmts[count($stmts) - 1] instanceof Stmt\InlineHTML) { - $p = preg_replace('/<\?php$/', '', rtrim($p)); - } - - $this->lexer = null; - - return $p; - } - - /** - * @param array $nodes - * @param bool $trailingComma - * @return string - */ - protected function pMaybeMultiline(array $nodes, bool $trailingComma = false) - { - if ($this->hasNodeWithComments($nodes) || (isset($nodes[0]) && $nodes[0] instanceof Expr\ArrayItem)) { - return $this->pCommaSeparatedMultiline($nodes, $trailingComma) . $this->nl; - } else { - return $this->pCommaSeparated($nodes); - } - } - - /** - * Pretty prints a comma-separated list of nodes in multiline style, including comments. - * - * The result includes a leading newline and one level of indentation (same as pStmts). - * - * @param array $nodes Array of Nodes to be printed - * @param bool $trailingComma Whether to use a trailing comma - * - * @return string Comma separated pretty printed nodes in multiline style - */ - protected function pCommaSeparatedMultiline(array $nodes, bool $trailingComma): string - { - $this->indent(); - - $result = ''; - $lastIdx = count($nodes) - 1; - foreach ($nodes as $idx => $node) { - if ($node !== null) { - $comments = $node->getComments(); - - if ($comments) { - $result .= $this->pComments($comments); - } - - $result .= $this->nl . $this->p($node); - } else { - $result = trim($result) . "\n"; - } - if ($trailingComma || $idx !== $lastIdx) { - $result .= ','; - } - } - - $this->outdent(); - return $result; - } - - /** - * Render an array expression - * - * @param Expr\Array_ $node Array expression node - * - * @return string Comma separated pretty printed nodes in multiline style - */ - protected function pExpr_Array(Expr\Array_ $node): string - { - $default = $this->options['shortArraySyntax'] - ? Expr\Array_::KIND_SHORT - : Expr\Array_::KIND_LONG; - - $ops = $node->getAttribute('kind', $default) === Expr\Array_::KIND_SHORT - ? ['[', ']'] - : ['array(', ')']; - - if (!count($node->items) && $comments = $this->getNodeComments($node)) { - // the array has no items, we can inject whatever we want - return sprintf( - '%s%s%s%s%s', - // opening control char - $ops[0], - // indent and add nl string - $this->indent(), - // join all comments with nl string - implode($this->nl, $comments), - // outdent and add nl string - $this->outdent(), - // closing control char - $ops[1] - ); - } - - if ($comments = $this->getCommentsNotInArray($node)) { - // array has items, we have detected comments not included within the array, therefore we have found - // trailing comments and must append them to the end of the array - return sprintf( - '%s%s%s%s%s%s', - // opening control char - $ops[0], - // render the children - $this->pMaybeMultiline($node->items, true), - // add 1 level of indentation - str_repeat(' ', 4), - // join all comments with the current indentation - implode($this->nl . str_repeat(' ', 4), $comments), - // add a trailing nl - $this->nl, - // closing control char - $ops[1] - ); - } - - // default return - return $ops[0] . $this->pMaybeMultiline($node->items, true) . $ops[1]; - } - - /** - * Increase indentation level. - * Proxied to allow for nl return - * - * @return string - */ - protected function indent(): string - { - $this->indentLevel += 4; - $this->nl .= ' '; - return $this->nl; - } - - /** - * Decrease indentation level. - * Proxied to allow for nl return - * - * @return string - */ - protected function outdent(): string - { - assert($this->indentLevel >= 4); - $this->indentLevel -= 4; - $this->nl = "\n" . str_repeat(' ', $this->indentLevel); - return $this->nl; - } - - /** - * Get all comments that have not been attributed to a node within a node array - * - * @param Expr\Array_ $nodes Array of nodes - * - * @return array Comments found - */ - protected function getCommentsNotInArray(Expr\Array_ $nodes): array - { - if (!$comments = $this->getNodeComments($nodes)) { - return []; - } - - return array_filter($comments, function ($comment) use ($nodes) { - return !$this->commentInNodeList($nodes->items, $comment); - }); - } - - /** - * Recursively check if a comment exists in an array of nodes - * - * @param Node[] $nodes Array of nodes - * @param string $comment The comment to search for - * - * @return bool - */ - protected function commentInNodeList(array $nodes, string $comment): bool - { - foreach ($nodes as $node) { - if ($node->value instanceof Expr\Array_ && $this->commentInNodeList($node->value->items, $comment)) { - return true; - } - if ($nodeComments = $node->getAttribute('comments')) { - foreach ($nodeComments as $nodeComment) { - if ($nodeComment->getText() === $comment) { - return true; - } - } - } - } - - return false; - } - - /** - * Check the lexer tokens for comments within the node's start & end position - * - * @param Node $node Node to check - * - * @return ?array - */ - protected function getNodeComments(Node $node): ?array - { - $tokens = $this->lexer->getTokens(); - $pos = $node->getAttribute('startTokenPos'); - $end = $node->getAttribute('endTokenPos'); - $endLine = $node->getAttribute('endLine'); - $content = []; - - while (++$pos < $end) { - if (!isset($tokens[$pos]) || (!is_array($tokens[$pos]) && $tokens[$pos] !== ',')) { - break; - } - - if ($tokens[$pos][0] === T_WHITESPACE || $tokens[$pos] === ',') { - continue; - } - - list($type, $string, $line) = $tokens[$pos]; - - if ($line > $endLine) { - break; - } - - if ($type === T_COMMENT || $type === T_DOC_COMMENT) { - $content[] = $string; - } elseif ($content) { - break; - } - } - - return empty($content) ? null : $content; - } - - /** - * Prints reformatted text of the passed comments. - * - * @param array $comments List of comments - * - * @return string Reformatted text of comments - */ - protected function pComments(array $comments): string - { - $formattedComments = []; - - foreach ($comments as $comment) { - $formattedComments[] = str_replace("\n", $this->nl, $comment->getReformattedText()); - } - - $padding = $comments[0]->getStartLine() !== $comments[count($comments) - 1]->getEndLine() ? $this->nl : ''; - - return "\n" . $this->nl . trim($padding . implode($this->nl, $formattedComments)) . "\n"; - } - - protected function pExpr_Include(Expr\Include_ $node) - { - static $map = [ - Expr\Include_::TYPE_INCLUDE => 'include', - Expr\Include_::TYPE_INCLUDE_ONCE => 'include_once', - Expr\Include_::TYPE_REQUIRE => 'require', - Expr\Include_::TYPE_REQUIRE_ONCE => 'require_once', - ]; - - return $map[$node->type] . '(' . $this->p($node->expr) . ')'; - } -} diff --git a/src/Parse/PHP/PHPConstant.php b/src/Parse/PHP/PHPConstant.php index 887a9eac5..5304f7dfe 100644 --- a/src/Parse/PHP/PHPConstant.php +++ b/src/Parse/PHP/PHPConstant.php @@ -1,25 +1,7 @@ name = $name; - } +use Winter\LaravelConfig\Parser\PHPConstant as BasePHPConstant; - /** - * Get the const name - */ - public function getName(): string - { - return $this->name; - } +class PHPConstant extends BasePHPConstant +{ } diff --git a/src/Parse/PHP/PHPFunction.php b/src/Parse/PHP/PHPFunction.php index 1874b1e26..cd606a92d 100644 --- a/src/Parse/PHP/PHPFunction.php +++ b/src/Parse/PHP/PHPFunction.php @@ -1,47 +1,7 @@ name = $name; - $this->args = $args; - } +use Winter\LaravelConfig\Parser\PHPFunction as BasePHPFunction; - /** - * Get the function name - * - * @return string - */ - public function getName(): string - { - return $this->name; - } - - /** - * Get the function arguments - * - * @return array - */ - public function getArgs(): array - { - return $this->args; - } +class PHPFunction extends BasePHPFunction +{ } diff --git a/tests/Parse/ArrayFileTest.php b/tests/Parse/ArrayFileTest.php index bb7a85090..12411badb 100644 --- a/tests/Parse/ArrayFileTest.php +++ b/tests/Parse/ArrayFileTest.php @@ -438,7 +438,7 @@ public function testWriteIllegalOffset() $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; $arrayFile = ArrayFile::open($file); - $this->expectException(\Winter\Storm\Exception\SystemException::class); + $this->expectException(\Winter\LaravelConfig\Exceptions\ConfigWriterException::class); $arrayFile->set([ 'w.i.n.t.e.r' => 'Winter CMS', From 4ef95b4924ddf0fb52349fd06ff27cfdcc732aa0 Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Mon, 1 Aug 2022 22:32:24 +0800 Subject: [PATCH 06/43] Update config writer files with correct namespace --- composer.json | 2 +- src/Parse/EnvFile.php | 2 +- src/Parse/PHP/ArrayFile.php | 2 +- src/Parse/PHP/PHPConstant.php | 2 +- src/Parse/PHP/PHPFunction.php | 2 +- tests/Parse/ArrayFileTest.php | 2 +- tests/tmp/.gitignore | 2 -- 7 files changed, 6 insertions(+), 8 deletions(-) delete mode 100644 tests/tmp/.gitignore diff --git a/composer.json b/composer.json index 5eb7d5ab6..e323f54eb 100644 --- a/composer.json +++ b/composer.json @@ -48,7 +48,7 @@ "twig/twig": "~3.0", "wikimedia/less.php": "~3.0", "wikimedia/minify": "~2.2", - "winter/laravel-config-writer": "^1.0.0" + "winter/laravel-config-writer": "^1.0.1" }, "require-dev": { "phpunit/phpunit": "^9.5.8", diff --git a/src/Parse/EnvFile.php b/src/Parse/EnvFile.php index 964dd8c82..14fcc200b 100644 --- a/src/Parse/EnvFile.php +++ b/src/Parse/EnvFile.php @@ -1,6 +1,6 @@ expectException(\Winter\LaravelConfig\Exceptions\ConfigWriterException::class); + $this->expectException(\Winter\LaravelConfigWriter\Exceptions\ConfigWriterException::class); $arrayFile->set([ 'w.i.n.t.e.r' => 'Winter CMS', diff --git a/tests/tmp/.gitignore b/tests/tmp/.gitignore deleted file mode 100644 index d6b7ef32c..000000000 --- a/tests/tmp/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore From 444c099629d6ea3bf425aaac573038de2ea8b38f Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Thu, 4 Aug 2022 09:49:31 +0800 Subject: [PATCH 07/43] Remove Input::all() override. Laravel's "Illuminate\Http\Concerns\InteractsWithInput::all()" method provides all values (query and request) as well as files. The override prevented files from being included. --- src/Support/Facades/Input.php | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Support/Facades/Input.php b/src/Support/Facades/Input.php index 1f997e254..2b29ac67b 100644 --- a/src/Support/Facades/Input.php +++ b/src/Support/Facades/Input.php @@ -21,18 +21,6 @@ public static function get($key = null, $default = null) return static::$app['request']->input($key, $default); } - /** - * Gets all input data items. - * - * This method is used for all request verbs (GET, POST, PUT, and DELETE) - * - * @return array|null - */ - public static function all() - { - return static::$app['request']->input(); - } - /** * Get the registered name of the component. * From 411695b3e7c7c3b32f702a625d6612319cc4052d Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Thu, 4 Aug 2022 11:25:00 -0600 Subject: [PATCH 08/43] Allow for ** wildcard to trust all proxies in a chain of proxies This reinstates the behaviour originally present in fideveloper/trustedproxy where setting ** as the value for app.trustedProxies would allow all proxies vs * which would only allow the most recent one in a chain of proxies (as determined by $_SERVER['REMOTE_ADDR']). See https://github.com/fideloper/TrustedProxy/commit/6018dfb8168c6a4696edc4997f7c20e1b88ac8cd for when & why it was originally added. The '**' wildcard was removed in v4 of that package (https://github.com/fideloper/TrustedProxy/commit/1d095914578a1c63374cec75fb94a1b197a3c5d6) with no explanation and was never added back in when Laravel merged it into the core in https://github.com/laravel/framework/pull/38295. This causes problems for environments where you have multiple proxies in a chain (i.e. Amazon CloudFront in front of Amazon ELB). These problems are documented in https://github.com/fideloper/TrustedProxy/issues/115 & https://github.com/fideloper/TrustedProxy/issues/107, and spawned https://github.com/fideloper/TrustedProxy/pull/142 & https://github.com/ge-tracker/laravel-vapor-trusted-proxies to resolve them. Ultimately, this commit serves to reintroduce the original behaviour of fideveloper/trustproxies v3 and make it so that you can use `**` as the value for app.trustProxies in order to get the correct client IP address when running Winter on Laravel Vapor. --- src/Foundation/Http/Middleware/CheckForTrustedProxies.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Foundation/Http/Middleware/CheckForTrustedProxies.php b/src/Foundation/Http/Middleware/CheckForTrustedProxies.php index d8f445d05..b4efdf887 100644 --- a/src/Foundation/Http/Middleware/CheckForTrustedProxies.php +++ b/src/Foundation/Http/Middleware/CheckForTrustedProxies.php @@ -98,6 +98,12 @@ protected function setTrustedProxies(Request $request) ]); return; } + + // If all proxies are allowed, open the floodgates + if ($proxies === '**') { + $this->allowProxies($request, ['0.0.0.0/0', '2000:0:0:0:0:0:0:0/3']); + return; + } // Support comma-separated strings as well as arrays $proxies = (is_string($proxies)) From d98af36d91284cdfd4a2a05adc0aefa0d66e3f05 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Tue, 9 Aug 2022 20:14:30 -0400 Subject: [PATCH 09/43] Add relatedModel to dissociate events (#101) --- src/Database/Relations/Concerns/BelongsOrMorphsTo.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Database/Relations/Concerns/BelongsOrMorphsTo.php b/src/Database/Relations/Concerns/BelongsOrMorphsTo.php index 8c51f88ed..0a1033df1 100644 --- a/src/Database/Relations/Concerns/BelongsOrMorphsTo.php +++ b/src/Database/Relations/Concerns/BelongsOrMorphsTo.php @@ -58,14 +58,14 @@ public function dissociate() * * Example usage: * - * $model->bindEvent('model.relation.beforeDissociate', function (string $relationName) use (\Winter\Storm\Database\Model $model) { + * $model->bindEvent('model.relation.beforeDissociate', function (string $relationName, Model $relatedModel) { * if ($relationName === 'permanentRelation') { * throw new \Exception("Cannot dissociate a permanent relation!"); * } * }); * */ - $this->parent->fireEvent('model.relation.beforeDissociate', [$this->relationName]); + $this->parent->fireEvent('model.relation.beforeDissociate', [$this->relationName, $this->getRelated()]); $result = parent::dissociate(); @@ -75,13 +75,13 @@ public function dissociate() * * Example usage: * - * $model->bindEvent('model.relation.afterDissociate', function (string $relationName) use (\Winter\Storm\Database\Model $model) { + * $model->bindEvent('model.relation.afterDissociate', function (string $relationName, Model $relatedModel) use (\Winter\Storm\Database\Model $model) { * $modelClass = get_class($model); * traceLog("{$relationName} was dissociated from {$modelClass}."); * }); * */ - $this->parent->fireEvent('model.relation.afterDissociate', [$this->relationName]); + $this->parent->fireEvent('model.relation.afterDissociate', [$this->relationName, $this->getRelated()]); return $result; } From a2253ef0339526558fe673df43bda853b4091b8f Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Wed, 10 Aug 2022 00:25:51 -0600 Subject: [PATCH 10/43] Automatically purge dynamic properties added to models When adding dynamically created properties to models, you have two choices: 1. You can either do it through direct assignment which eventually passes through Laravel's attribute system and stores the dynamic property in the $attributes property, thus flagging it for insertion into the database like a regular property; or 2. You can add it through `addDynamicProperty()` which currently ultimately ends up doing the same thing as 1 except that it also gets added to the list of dynamic properties on the model through the ExtendableTrait. The key difference in choosing to use dynamic properties instead of model attributes usually comes down to whether or not you want your newly added property to be stored in the database. Most commonly, you would choose to use addDynamicProperty() when adding a logical property that's used for performing logic on the model (i.e. adding a ModelBehavior to the model with extendClassWith() that requires properties to be present in order to configure itself). This assumption is present in the PurgeableBehavior itself currently (see https://github.com/wintercms/storm/blob/c972c1f50cce9c5c150278facb86e1caf9dd69ed/src/Database/Behaviors/Purgeable.php#L34-L36) which automatically adds any existing defined dynamic properties to the purgeable array during the process of booting the PurgeableBehavior when it is added to a class (a key part of the original design considerations: https://github.com/octobercms/library/pull/324#issuecomment-395419369). In addition to that, the Winter.Translate plugin has to go out of its way to explicitly add the Purgeable behavior whenever it adds it's own TranslateableModel behavior (see https://github.com/wintercms/wn-translate-plugin/blob/344711704f8ec0556295a02714cb2a6f48f9dd27/Plugin.php#L79-L81). It has also popped up in issues (https://github.com/octobercms/october/issues/3433) before as a source of developer confusion. Given all of the above information, it makes sense to have the Model class itself automatically add the PurgeableBehavior if not already present as soon as a single dynamic property is defined so that all dynamic properties can be automatically added to the list of purgeable properties as soon as they are defined. In the future, it might make sense to just make the Purgeable trait itself included by default in the base model in order to not have to dynamically & automatically add the Purgeable behavior under these conditions, but I leave that as a task for later. --- src/Database/Model.php | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/Database/Model.php b/src/Database/Model.php index 05403aa2a..9b07a2e83 100644 --- a/src/Database/Model.php +++ b/src/Database/Model.php @@ -24,7 +24,9 @@ class Model extends EloquentModel implements ModelInterface use Concerns\HasRelationships; use Concerns\HidesAttributes; use \Winter\Storm\Support\Traits\Emitter; - use \Winter\Storm\Extension\ExtendableTrait; + use \Winter\Storm\Extension\ExtendableTrait { + addDynamicProperty as protected extendableAddDynamicProperty; + } use \Winter\Storm\Database\Traits\DeferredBinding; /** @@ -670,6 +672,30 @@ public function newCollection(array $models = []) // Magic // + /** + * Programmatically adds a property to the extendable class + * + * @param string $dynamicName The name of the property to add + * @param mixed $value The value of the property + * @return void + */ + public function addDynamicProperty($dynamicName, $value = null) + { + if (array_key_exists($dynamicName, $this->getDynamicProperties())) { + return; + } + + // Ensure that dynamic properties are automatically purged + if (!$this->methodExists('addPurgeable')) { + $this->extendableAddDynamicProperty('purgeable', []); + $this->extendClassWith(\Winter\Storm\Database\Behaviors\Purgeable::class); + }; + $this->addPurgeable($dynamicName); + + // Add the dynamic property + return $this->extendableAddDynamicProperty($dynamicName, $value); + } + public function __get($name) { return $this->extendableGet($name); From 7e57a08c6fa0a654e172c28157e40f0d6c9688f1 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Wed, 10 Aug 2022 01:04:33 -0600 Subject: [PATCH 11/43] Deprecate Purgeable model behavior and include Purgeable trait in the core model Follow up to https://github.com/wintercms/storm/commit/a2253ef0339526558fe673df43bda853b4091b8f, @bennothommo pointed out that it made more sense to just include the Purgeable trait on the base model itself. --- src/Database/Behaviors/Purgeable.php | 116 +-------------------- src/Database/Model.php | 10 +- tests/Database/Behaviors/PurgeableTest.php | 77 -------------- 3 files changed, 10 insertions(+), 193 deletions(-) delete mode 100644 tests/Database/Behaviors/PurgeableTest.php diff --git a/src/Database/Behaviors/Purgeable.php b/src/Database/Behaviors/Purgeable.php index dd1c84d68..a7fc0e4d3 100644 --- a/src/Database/Behaviors/Purgeable.php +++ b/src/Database/Behaviors/Purgeable.php @@ -1,117 +1,9 @@ model = $parent; - $this->bootPurgeable(); - } - - /** - * @var array List of original attribute values before they were purged. - */ - protected $originalPurgeableValues = []; - - /** - * Boot the purgeable trait for a model. - * @return void - */ - public function bootPurgeable() - { - if (!$this->model->propertyExists('purgeable')) { - $this->model->addDynamicProperty('purgeable', []); - } - - $this->model->purgeable[] = 'purgeable'; - $dynPropNames = array_keys(array_diff_key($this->model->getDynamicProperties(), ['purgeable' => 0])); - $this->model->purgeable = array_merge($this->model->purgeable, $dynPropNames); - - /* - * Remove any purge attributes from the data set - */ - $model = $this->model; - $model->bindEvent('model.saveInternal', function () use ($model) { - $model->purgeAttributes(); - }); - } - - /** - * Adds an attribute to the purgeable attributes list - * @param array|string|null $attributes - * @return \Winter\Storm\Database\Model - */ - public function addPurgeable($attributes = null) - { - $attributes = is_array($attributes) ? $attributes : func_get_args(); - - $this->model->purgeable = array_merge($this->model->purgeable, $attributes); - - return $this->model; - } - - /** - * Removes purged attributes from the dataset, used before saving. - * @param string|array|null $attributesToPurge Attribute(s) to purge. If unspecified, $purgable property is used - * @return array Current attribute set - */ - public function purgeAttributes($attributesToPurge = null) - { - if ($attributesToPurge !== null) { - $purgeable = is_array($attributesToPurge) ? $attributesToPurge : [$attributesToPurge]; - } - else { - $purgeable = $this->getPurgeableAttributes(); - } - - $attributes = $this->model->getAttributes(); - $cleanAttributes = array_diff_key($attributes, array_flip($purgeable)); - $originalAttributes = array_diff_key($attributes, $cleanAttributes); - - $this->originalPurgeableValues = array_merge($this->originalPurgeableValues, $originalAttributes); - - return $this->model->attributes = $cleanAttributes; - } - - /** - * Returns a collection of fields that will be hashed. - */ - public function getPurgeableAttributes() - { - return $this->model->purgeable; - } - - /** - * Returns the original values of any purged attributes. - */ - public function getOriginalPurgeValues() - { - return $this->originalPurgeableValues; - } - - /** - * Returns the original values of any purged attributes. - */ - public function getOriginalPurgeValue($attribute) - { - return $this->originalPurgeableValues[$attribute] ?? null; - } - - /** - * Restores the original values of any purged attributes. - * - * @return \Winter\Storm\Database\Model - */ - public function restorePurgedValues() - { - $this->model->attributes = array_merge($this->model->getAttributes(), $this->originalPurgeableValues); - return $this->model; - } } diff --git a/src/Database/Model.php b/src/Database/Model.php index 9b07a2e83..210590eb8 100644 --- a/src/Database/Model.php +++ b/src/Database/Model.php @@ -23,6 +23,7 @@ class Model extends EloquentModel implements ModelInterface use Concerns\GuardsAttributes; use Concerns\HasRelationships; use Concerns\HidesAttributes; + use Traits\Purgeable; use \Winter\Storm\Support\Traits\Emitter; use \Winter\Storm\Extension\ExtendableTrait { addDynamicProperty as protected extendableAddDynamicProperty; @@ -49,6 +50,11 @@ class Model extends EloquentModel implements ModelInterface */ protected $dates = []; + /** + * @var array List of attributes which should not be saved to the database. + */ + protected $purgeable = []; + /** * @var bool Indicates if duplicate queries from this model should be cached in memory. */ @@ -686,10 +692,6 @@ public function addDynamicProperty($dynamicName, $value = null) } // Ensure that dynamic properties are automatically purged - if (!$this->methodExists('addPurgeable')) { - $this->extendableAddDynamicProperty('purgeable', []); - $this->extendClassWith(\Winter\Storm\Database\Behaviors\Purgeable::class); - }; $this->addPurgeable($dynamicName); // Add the dynamic property diff --git a/tests/Database/Behaviors/PurgeableTest.php b/tests/Database/Behaviors/PurgeableTest.php deleted file mode 100644 index 7b9a34c38..000000000 --- a/tests/Database/Behaviors/PurgeableTest.php +++ /dev/null @@ -1,77 +0,0 @@ -assertEquals(['Winter.Storm.Database.Behaviors.Purgeable'], $model->implement); - $this->assertEquals(['purgeable'], $model->purgeable); - } - - public function testDirectImplementationWithoutProperty() - { - $model = new TestModelDirectWithoutProperty(); - $this->assertEquals(['Winter.Storm.Database.Behaviors.Purgeable'], $model->implement); - $this->assertEquals(['purgeable'], $model->purgeable); - } - - public function testDynamicImplementation() - { - TestModelDynamic::extend(function ($model) { - $model->implement[] = 'Winter.Storm.Database.Behaviors.Purgeable'; - $model->addDynamicProperty('purgeable', []); - }); - $model = new TestModelDynamic(); - $this->assertEquals(['Winter.Storm.Database.Behaviors.Purgeable'], $model->implement); - $this->assertEquals(['purgeable'], $model->purgeable); - } - - public function testDynamicImplementationWithoutProperty() - { - TestModelDynamicWithoutProperty::extend(function ($model) { - $model->implement[] = 'Winter.Storm.Database.Behaviors.Purgeable'; - }); - $model = new TestModelDynamicWithoutProperty(); - $this->assertEquals(['Winter.Storm.Database.Behaviors.Purgeable'], $model->implement); - $this->assertEquals(['purgeable'], $model->purgeable); - } -} - -/* - * Class with implementation in the class itself - */ -class TestModelDirect extends Model -{ - public $implement = [ - 'Winter.Storm.Database.Behaviors.Purgeable' - ]; - - public $purgeable = []; -} - -/* - * Class with implementation in the class itself but without property - */ -class TestModelDirectWithoutProperty extends Model -{ - public $implement = [ - 'Winter.Storm.Database.Behaviors.Purgeable' - ]; -} - - -/* - * Class with no implementation that can be extended - */ -class TestModelDynamic extends Model -{ - -} - -class TestModelDynamicWithoutProperty extends Model -{ - -} From 6ce6951e91ec0a06863edf02e36e0eb199429749 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Fri, 12 Aug 2022 21:13:25 -0600 Subject: [PATCH 12/43] Improve reliability of extension detection logic in File->fromUrl() --- src/Database/Attach/File.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Database/Attach/File.php b/src/Database/Attach/File.php index 9f09cd69f..a3c4dc65c 100644 --- a/src/Database/Attach/File.php +++ b/src/Database/Attach/File.php @@ -185,14 +185,19 @@ public function fromUrl($url, $filename = null) // Attempt to detect the extension from the reported Content-Type, fall back to the original path extension // if not able to guess $mimesToExt = array_flip($this->autoMimeTypes); - if (!empty($data->headers['Content-Type']) && isset($mimesToExt[$data->headers['Content-Type']])) { - $ext = $mimesToExt[$data->headers['Content-Type']]; + $headers = array_change_key_case($data->headers, CASE_LOWER); + if (!empty($headers['content-type']) && isset($mimesToExt[$headers['content-type']])) { + $ext = $mimesToExt[$headers['content-type']]; } else { - $ext = pathinfo($filePath)['extension']; + $ext = pathinfo($filePath)['extension'] ?? ''; + } + + if (!empty($ext)) { + $ext = '.' . $ext; } // Generate the filename - $filename = "{$filename}.{$ext}"; + $filename = "{$filename}{$ext}"; } return $this->fromData($data, $filename); From c603360c43c3470dc9619bba6507c784d80a9374 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Fri, 12 Aug 2022 23:16:11 -0600 Subject: [PATCH 13/43] Reorder Str helper methods alphabetically --- src/Support/Str.php | 94 ++++++++++++++++++++++----------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/src/Support/Str.php b/src/Support/Str.php index 69afb4a0a..cec7a551a 100644 --- a/src/Support/Str.php +++ b/src/Support/Str.php @@ -9,53 +9,6 @@ */ class Str extends StrHelper { - /** - * Converts number to its ordinal English form. - * - * This method converts 13 to 13th, 2 to 2nd ... - * - * @param integer $number Number to get its ordinal value - * @return string Ordinal representation of given string. - */ - public static function ordinal($number) - { - if (in_array($number % 100, range(11, 13))) { - return $number.'th'; - } - - switch ($number % 10) { - case 1: - return $number.'st'; - case 2: - return $number.'nd'; - case 3: - return $number.'rd'; - default: - return $number.'th'; - } - } - - /** - * Converts line breaks to a standard \r\n pattern. - */ - public static function normalizeEol($string) - { - return preg_replace('~\R~u', "\r\n", $string); - } - - /** - * Removes the starting slash from a class namespace \ - */ - public static function normalizeClassName($name) - { - if (is_object($name)) { - $name = get_class($name); - } - - $name = '\\'.ltrim($name, '\\'); - return $name; - } - /** * Generates a class ID from either an object or a string of the class name. */ @@ -92,4 +45,51 @@ public static function getPrecedingSymbols($string, $symbol) { return strlen($string) - strlen(ltrim($string, $symbol)); } + + /** + * Converts line breaks to a standard \r\n pattern. + */ + public static function normalizeEol($string) + { + return preg_replace('~\R~u', "\r\n", $string); + } + + /** + * Removes the starting slash from a class namespace \ + */ + public static function normalizeClassName($name) + { + if (is_object($name)) { + $name = get_class($name); + } + + $name = '\\'.ltrim($name, '\\'); + return $name; + } + + /** + * Converts number to its ordinal English form. + * + * This method converts 13 to 13th, 2 to 2nd ... + * + * @param integer $number Number to get its ordinal value + * @return string Ordinal representation of given string. + */ + public static function ordinal($number) + { + if (in_array($number % 100, range(11, 13))) { + return $number.'th'; + } + + switch ($number % 10) { + case 1: + return $number.'st'; + case 2: + return $number.'nd'; + case 3: + return $number.'rd'; + default: + return $number.'th'; + } + } } From 3067d7d5e5490aa197037d60980e92a464da1826 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Fri, 12 Aug 2022 23:17:09 -0600 Subject: [PATCH 14/43] Adds Str::join() helper See https://github.com/laravel/framework/pull/43559 for justification. --- src/Support/Str.php | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/Support/Str.php b/src/Support/Str.php index cec7a551a..abe58f5ed 100644 --- a/src/Support/Str.php +++ b/src/Support/Str.php @@ -46,6 +46,45 @@ public static function getPrecedingSymbols($string, $symbol) return strlen($string) - strlen(ltrim($string, $symbol)); } + /** + * Join items into a human readable list (e.g. "one, two, three, and four") + * Uses different glue strings when there are only two elements and for + * the final element. Defaults to joining using the Oxford comma. + * + * 1 item will return: $item + * 2 items will return: $item1 . $dyadicGlue . $item2 + * 3+ items will return: $item1 . $glue . $item2 . $lastGlue . $item3 + */ + public static function join(iterable $items, string $glue = ', ', string $lastGlue = ', and ', $dyadicGlue = ' and '): string + { + $result = ''; + $i = 0; + $total = count($items); + foreach ($items as $item) { + $i++; + + // Only add glue if we're not on the first item + if ($i !== 1) { + // Add diadic glue between the first and last item + if ($i === 2 && $total === 2) { + $result .= $dyadicGlue; + + // Add the last glue if we're on the last item + } elseif ($i === $total) { + $result .= $lastGlue; + + // Add the normal glue otherwise + } else { + $result .= $glue; + } + } + + $result .= $item; + } + + return $result; + } + /** * Converts line breaks to a standard \r\n pattern. */ From 35b3bbb993d558c9fe6f94cd0d22748ccd7ec356 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Fri, 12 Aug 2022 23:19:04 -0600 Subject: [PATCH 15/43] Add test for new Str::join() helper See https://github.com/laravel/framework/pull/43559 --- tests/Support/StrTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/Support/StrTest.php diff --git a/tests/Support/StrTest.php b/tests/Support/StrTest.php new file mode 100644 index 000000000..eba82d57a --- /dev/null +++ b/tests/Support/StrTest.php @@ -0,0 +1,18 @@ +assertSame('', Str::join([])); + $this->assertSame('bob', Str::join(['bob'])); + $this->assertSame('bob and joe', Str::join(['bob', 'joe'])); + $this->assertSame('bob, joe, and sally', Str::join(['bob', 'joe', 'sally'])); + $this->assertSame('bob, joe and sally', Str::join(['bob', 'joe', 'sally'], ', ', ' and ')); + $this->assertSame('bob, joe, and sally', Str::join(['bob', 'joe', 'sally'])); + $this->assertSame('bob or joe', Str::join(['bob', 'joe'], ', ', ', or ', ' or ')); + $this->assertSame('bob; joe; or sally', Str::join(['bob', 'joe', 'sally'], '; ', '; or ')); + } +} From f6be9c5aed5e930941c23b01b4951a219950531c Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Mon, 15 Aug 2022 16:37:47 +0800 Subject: [PATCH 16/43] Re-do image resizer tests Port of https://github.com/wintercms/storm/commit/ee91a88e43577b302625746d85fa7de301efd556 from the 1.1 branch to 1.2. --- .github/workflows/tests.yml | 8 + .gitignore | 1 + tests/Database/Attach/ResizerTest.php | 160 +++++++++--------- .../resizer/source/landscape_rotated.jpg | Bin 2779 -> 22992 bytes .../resizer/source/landscape_transparent.png | Bin 5577 -> 5880 bytes tests/fixtures/resizer/source/portrait.gif | Bin 248 -> 1292 bytes tests/fixtures/resizer/source/square.jpg | Bin 1172 -> 28409 bytes .../fixtures/resizer/target/testCrop10x15.gif | Bin 66 -> 0 bytes .../fixtures/resizer/target/testCrop30x45.gif | Bin 0 -> 208 bytes ...stResize0x0_testResizeAutoLandscape1x1.png | Bin 6338 -> 5880 bytes .../resizer/target/testResize0x20.gif | Bin 96 -> 0 bytes .../resizer/target/testResize0x50.gif | Bin 0 -> 218 bytes .../resizer/target/testResize20x0.gif | Bin 118 -> 0 bytes .../resizer/target/testResize50x0.gif | Bin 0 -> 330 bytes .../target/testResizeAutoExifRotated30x30.jpg | Bin 859 -> 0 bytes .../target/testResizeAutoLandscape100x1.png | Bin 0 -> 2178 bytes .../target/testResizeAutoLandscape125.png | Bin 0 -> 3076 bytes .../target/testResizeAutoLandscape1x5.png | Bin 437 -> 0 bytes .../target/testResizeAutoLandscape1x50.png | Bin 0 -> 3362 bytes .../target/testResizeAutoLandscape25.png | Bin 310 -> 0 bytes .../target/testResizeAutoLandscape25x1.png | Bin 310 -> 0 bytes .../target/testResizeAutoPortrait50.gif | Bin 237 -> 218 bytes ...toSquare50x50_testResizeFitSquare50x50.jpg | Bin 1818 -> 0 bytes ...uare100x100_testResizeFitSquare100x100.jpg | Bin 0 -> 6312 bytes .../resizer/target/testResizeExact10x15.jpg | Bin 839 -> 0 bytes .../resizer/target/testResizeExact50x75.jpg | Bin 0 -> 3705 bytes .../target/testResizeFitLandscape150x150.png | Bin 0 -> 3362 bytes .../target/testResizeFitLandscape30x30.png | Bin 437 -> 0 bytes .../target/testResizeFitPortrait150x150.gif | Bin 0 -> 1210 bytes .../target/testResizeFitPortrait30x30.gif | Bin 129 -> 0 bytes .../target/testResizeLandscape100x1.gif | Bin 0 -> 959 bytes .../target/testResizeLandscape10x1.gif | Bin 71 -> 0 bytes .../resizer/target/testResizePortrait1x10.gif | Bin 57 -> 0 bytes .../target/testResizePortrait1x100.gif | Bin 0 -> 651 bytes .../testResizeSaveBackgroundColor32x32.gif | Bin 63 -> 0 bytes .../testResizeSaveBackgroundColor75x75.gif | Bin 0 -> 115 bytes tests/fixtures/resizer/target/testSharpen.jpg | Bin 1128 -> 7703 bytes 37 files changed, 85 insertions(+), 84 deletions(-) delete mode 100644 tests/fixtures/resizer/target/testCrop10x15.gif create mode 100644 tests/fixtures/resizer/target/testCrop30x45.gif delete mode 100644 tests/fixtures/resizer/target/testResize0x20.gif create mode 100644 tests/fixtures/resizer/target/testResize0x50.gif delete mode 100644 tests/fixtures/resizer/target/testResize20x0.gif create mode 100644 tests/fixtures/resizer/target/testResize50x0.gif delete mode 100644 tests/fixtures/resizer/target/testResizeAutoExifRotated30x30.jpg create mode 100644 tests/fixtures/resizer/target/testResizeAutoLandscape100x1.png create mode 100644 tests/fixtures/resizer/target/testResizeAutoLandscape125.png delete mode 100644 tests/fixtures/resizer/target/testResizeAutoLandscape1x5.png create mode 100644 tests/fixtures/resizer/target/testResizeAutoLandscape1x50.png delete mode 100644 tests/fixtures/resizer/target/testResizeAutoLandscape25.png delete mode 100644 tests/fixtures/resizer/target/testResizeAutoLandscape25x1.png delete mode 100644 tests/fixtures/resizer/target/testResizeAutoSquare25x50_testResizeAutoSquare50x25_testResizeAutoSquare50x50_testResizeFitSquare50x50.jpg create mode 100644 tests/fixtures/resizer/target/testResizeAutoSquare50x100_testResizeAutoSquare100x50_testResizeAutoSquare100x100_testResizeFitSquare100x100.jpg delete mode 100644 tests/fixtures/resizer/target/testResizeExact10x15.jpg create mode 100644 tests/fixtures/resizer/target/testResizeExact50x75.jpg create mode 100644 tests/fixtures/resizer/target/testResizeFitLandscape150x150.png delete mode 100644 tests/fixtures/resizer/target/testResizeFitLandscape30x30.png create mode 100644 tests/fixtures/resizer/target/testResizeFitPortrait150x150.gif delete mode 100644 tests/fixtures/resizer/target/testResizeFitPortrait30x30.gif create mode 100644 tests/fixtures/resizer/target/testResizeLandscape100x1.gif delete mode 100644 tests/fixtures/resizer/target/testResizeLandscape10x1.gif delete mode 100644 tests/fixtures/resizer/target/testResizePortrait1x10.gif create mode 100644 tests/fixtures/resizer/target/testResizePortrait1x100.gif delete mode 100644 tests/fixtures/resizer/target/testResizeSaveBackgroundColor32x32.gif create mode 100644 tests/fixtures/resizer/target/testResizeSaveBackgroundColor75x75.gif diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 59d2e7f9d..f7b540a6b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -71,3 +71,11 @@ jobs: - name: Run tests run: ./vendor/bin/phpunit ./tests + + - name: Upload test artifacts on failure + uses: actions/upload-artifact@v3 + if: ${{ failure() }} + with: + name: ResizerTest-${{ matrix.operatingSystem }}-PHP${{ matrix.phpVersion }} + path: tests/artifacts/ResizerTest/ + if-no-files-found: error diff --git a/.gitignore b/.gitignore index 50fa968a2..bdee57b0e 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ php_errors.log tests/.phpunit.result.cache .phpunit.result.cache tests/tmp +tests/artifacts/* diff --git a/tests/Database/Attach/ResizerTest.php b/tests/Database/Attach/ResizerTest.php index d7792939d..e9dbb4abe 100644 --- a/tests/Database/Attach/ResizerTest.php +++ b/tests/Database/Attach/ResizerTest.php @@ -15,6 +15,7 @@ class ResizerTest extends TestCase const FIXTURE_SRC_BASE_PATH = self::FIXTURE_PATH . 'resizer/source/'; const FIXTURE_TARGET_PATH = self::FIXTURE_PATH . 'resizer/target/'; const TMP_TEST_FILE_PATH = self::FIXTURE_PATH . 'tmp/'; + const ARTIFACTS_PATH = __DIR__ . '/../../artifacts/ResizerTest/'; // Source image filenames const SRC_LANDSCAPE_ROTATED = 'landscape_rotated.jpg'; @@ -29,7 +30,7 @@ class ResizerTest extends TestCase */ const COMMON_FIXTURES = [ 'reset' => 'testReset_testResize0x0_testResizeAutoLandscape1x1', - 'square' => 'testResizeAutoSquare25x50_testResizeAutoSquare50x25_testResizeAutoSquare50x50_testResizeFitSquare50x50' + 'square' => 'testResizeAutoSquare50x100_testResizeAutoSquare100x50_testResizeAutoSquare100x100_testResizeFitSquare100x100' ]; /** @var string The path to the source image */ @@ -52,6 +53,14 @@ class ResizerTest extends TestCase */ protected function tearDown(): void { + if (!is_dir(self::ARTIFACTS_PATH)) { + @mkdir(self::ARTIFACTS_PATH, 0777, true); + } + + if (!empty($this->tmpTarget)) { + copy($this->tmpTarget, self::ARTIFACTS_PATH . basename($this->tmpTarget)); + } + @unlink($this->tmpTarget); @rmdir(self::TMP_TEST_FILE_PATH); parent::tearDown(); @@ -67,7 +76,7 @@ public function testReset() { $this->setSource(self::SRC_LANDSCAPE_TRANSPARENT); $this->createFixtureResizer(); - $this->resizer->resize(5, 5, ['mode' => 'crop']); + $this->resizer->resize(200, 200, ['mode' => 'crop']); $this->resizer->reset(); $this->assertImageSameAsFixture(self::COMMON_FIXTURES['reset']); } @@ -111,29 +120,29 @@ public function testResize0x0() /** * Given a Resizer with any image - * When the resize method is called with 20x0 - * Then the saved image should have a width of 20 and its height set automatically + * When the resize method is called with 50x0 + * Then the saved image should have a width of 50 and its height set automatically * @throws Exception */ - public function testResize20x0() + public function testResize50x0() { $this->setSource(self::SRC_PORTRAIT); $this->createFixtureResizer(); - $this->resizer->resize(20, 0); + $this->resizer->resize(50, 0); $this->assertImageSameAsFixture(__METHOD__); } /** * Given a Resizer with any image - * When the resize method is called with 0x20 - * Then the saved image should have a height of 20 and its width set automatically + * When the resize method is called with 0x50 + * Then the saved image should have a height of 50 and its width set automatically * @throws Exception */ - public function testResize0x20() + public function testResize0x50() { $this->setSource(self::SRC_PORTRAIT); $this->createFixtureResizer(); - $this->resizer->resize(0, 20); + $this->resizer->resize(0, 50); $this->assertImageSameAsFixture(__METHOD__); } @@ -153,57 +162,57 @@ public function testResizeAutoPortrait50() /** * Given a Resizer with a landscape image - * When the resize method is called with the auto parameter and 25x50 dimensions - * Then the saved image should have a width of 25 and its height set automatically + * When the resize method is called with the auto parameter and 125x50 dimensions + * Then the saved image should have a width of 125 and its height set automatically * @throws Exception */ - public function testResizeAutoLandscape25() + public function testResizeAutoLandscape125() { $this->setSource(self::SRC_LANDSCAPE_TRANSPARENT); $this->createFixtureResizer(); - $this->resizer->resize(25, 50, ['mode' => 'auto']); + $this->resizer->resize(125, 50, ['mode' => 'auto']); $this->assertImageSameAsFixture(__METHOD__); } /** * Given a Resizer with a square image - * When the resize method is called with the auto parameter and a 25x50 dimension - * Then the saved image should have be 50x50 (largest dimension takes over) + * When the resize method is called with the auto parameter and a 50x100 dimension + * Then the saved image should have be 100x100 (largest dimension takes over) * @throws Exception */ - public function testResizeAutoSquare25x50() + public function testResizeAutoSquare50x100() { $this->setSource(self::SRC_SQUARE); $this->createFixtureResizer(); - $this->resizer->resize(25, 50, ['mode' => 'auto']); + $this->resizer->resize(50, 100, ['mode' => 'auto']); $this->assertImageSameAsFixture(self::COMMON_FIXTURES['square']); } /** * Given a Resizer with a square image - * When the resize method is called with the auto parameter and a 50x25 dimension - * Then the saved image should have be 50x50 (largest dimension takes over) + * When the resize method is called with the auto parameter and a 100x50 dimension + * Then the saved image should have be 100x100 (largest dimension takes over) * @throws Exception */ - public function testResizeAutoSquare50x25() + public function testResizeAutoSquare100x50() { $this->setSource(self::SRC_SQUARE); $this->createFixtureResizer(); - $this->resizer->resize(50, 25, ['mode' => 'auto']); + $this->resizer->resize(100, 50, ['mode' => 'auto']); $this->assertImageSameAsFixture(self::COMMON_FIXTURES['square']); } /** * Given a Resizer with a square image - * When the resize method is called with the auto parameter and a 50x50 dimension - * Then the saved image should have be 50x50 + * When the resize method is called with the auto parameter and a 100x100 dimension + * Then the saved image should have be 100x100 * @throws Exception */ - public function testResizeAutoSquare50x50() + public function testResizeAutoSquare100x100() { $this->setSource(self::SRC_SQUARE); $this->createFixtureResizer(); - $this->resizer->resize(50, 50, ['mode' => 'auto']); + $this->resizer->resize(100, 100, ['mode' => 'auto']); $this->assertImageSameAsFixture(self::COMMON_FIXTURES['square']); } @@ -223,86 +232,86 @@ public function testResizeAutoLandscape1x1() /** * Given a Resizer with a transparent landscape image - * When the resize method is called with the auto parameter and 1x5 dimensions - * Then the saved image should be have a height of 5 and an automatic width + * When the resize method is called with the auto parameter and 1x50 dimensions + * Then the saved image should be have a height of 50 and an automatic width * @throws Exception */ - public function testResizeAutoLandscape1x5() + public function testResizeAutoLandscape1x50() { $this->setSource(self::SRC_LANDSCAPE_TRANSPARENT); $this->createFixtureResizer(); - $this->resizer->resize(1, 5); + $this->resizer->resize(1, 50); $this->assertImageSameAsFixture(__METHOD__); } /** * Given a Resizer with a transparent landscape image - * When the resize method is called with the auto parameter and 50x1 dimensions - * Then the saved image should have a width a 25 and an automatic height + * When the resize method is called with the auto parameter and 100x1 dimensions + * Then the saved image should have a width a 100 and an automatic height * @throws Exception */ - public function testResizeAutoLandscape25x1() + public function testResizeAutoLandscape100x1() { $this->setSource(self::SRC_LANDSCAPE_TRANSPARENT); $this->createFixtureResizer(); - $this->resizer->resize(25, 1); + $this->resizer->resize(100, 1); $this->assertImageSameAsFixture(__METHOD__); } /** * Given a Resizer with a square image - * When the resize method is called with the exact mode and 10x1=15 dimensions - * Then the saved image should be 10x15 and distorted + * When the resize method is called with the exact mode and 50x75 dimensions + * Then the saved image should be 50x75 and distorted * @throws Exception */ - public function testResizeExact10x15() + public function testResizeExact50x75() { $this->setSource(self::SRC_SQUARE); $this->createFixtureResizer(); - $this->resizer->resize(10, 15, ['mode' => 'exact']); + $this->resizer->resize(50, 75, ['mode' => 'exact']); $this->assertImageSameAsFixture(__METHOD__); } /** * Given a Resizer with any image - * When the resize method is called with the landscape mode and 10x1 dimensions - * Then the saved image should have a width of 10 and an automatic height + * When the resize method is called with the landscape mode and 100x1 dimensions + * Then the saved image should have a width of 100 and an automatic height * @throws Exception */ - public function testResizeLandscape10x1() + public function testResizeLandscape100x1() { $this->setSource(self::SRC_PORTRAIT); $this->createFixtureResizer(); - $this->resizer->resize(10, 1, ['mode' => 'landscape']); + $this->resizer->resize(100, 1, ['mode' => 'landscape']); $this->assertImageSameAsFixture(__METHOD__); } /** * Given a Resizer with any image * When the resize method is called with the portrait mode and 1x10 dimensions - * Then the saved image should have a width of 10 and an automatic height + * Then the saved image should have a width of 100 and an automatic height * @throws Exception */ - public function testResizePortrait1x10() + public function testResizePortrait1x100() { $this->setSource(self::SRC_PORTRAIT); $this->createFixtureResizer(); - $this->resizer->resize(1, 10, ['mode' => 'portrait']); + $this->resizer->resize(1, 100, ['mode' => 'portrait']); $this->assertImageSameAsFixture(__METHOD__); } /** * Given a Resizer with a white background gif image - * When the resize method is called with the auto parameter and 32x32 dimensions - * Then the saved image should have be 32x32 and with white background + * When the resize method is called with the auto parameter and 75x75 dimensions + * Then the saved image should have be 75x75 and with white background * Tests if white color is preserved/saved after resize operation * @throws Exception */ - public function testResizeSaveBackgroundColor32x32() + public function testResizeSaveBackgroundColor75x75() { $this->setSource(self::SRC_GIF_BG); $this->createFixtureResizer(); - $this->resizer->resize(32, 32); + $this->resizer->resize(75, 75); $this->assertImageSameAsFixture(__METHOD__); } @@ -322,64 +331,46 @@ public function testResizeIndex300x255() /** * Given a Resizer with a landscape image - * When the resize method is called with the fit mode and 30x30 dimensions - * Then the saved image should have a width of 30 and an automatic height + * When the resize method is called with the fit mode and 150x150 dimensions + * Then the saved image should have a width of 150 and an automatic height * @throws Exception */ - public function testResizeFitLandscape30x30() + public function testResizeFitLandscape150x150() { $this->setSource(self::SRC_LANDSCAPE_TRANSPARENT); $this->createFixtureResizer(); - $this->resizer->resize(30, 30, ['mode' => 'fit']); + $this->resizer->resize(150, 150, ['mode' => 'fit']); $this->assertImageSameAsFixture(__METHOD__); } /** * Given a Resizer with a landscape image - * When the resize method is called with the fit mode and 30x30 dimensions - * Then the saved image should have a height of 30 and an automatic width + * When the resize method is called with the fit mode and 150x150 dimensions + * Then the saved image should have a height of 150 and an automatic width * @throws Exception */ - public function testResizeFitPortrait30x30() + public function testResizeFitPortrait150x150() { $this->setSource(self::SRC_PORTRAIT); $this->createFixtureResizer(); - $this->resizer->resize(30, 30, ['mode' => 'fit']); + $this->resizer->resize(150, 150, ['mode' => 'fit']); $this->assertImageSameAsFixture(__METHOD__); } /** * Given a Resizer with a square image - * When the resize method is called with the fit mode and 50x50 dimensions - * Then the saved image should have be 50x50 + * When the resize method is called with the fit mode and 100x100 dimensions + * Then the saved image should have be 100x100 * @throws Exception */ - public function testResizeFitSquare50x50() + public function testResizeFitSquare100x100() { $this->setSource(self::SRC_SQUARE); $this->createFixtureResizer(); - $this->resizer->resize(50, 50, ['mode' => 'fit']); + $this->resizer->resize(100, 100, ['mode' => 'fit']); $this->assertImageSameAsFixture(self::COMMON_FIXTURES['square']); } - /** - * Given a Resizer with a JPG image which has an EXIF tag (Rotation=8) - * When the resize method is called with the auto mode and 30x30 dimensions - * Then the saved image should have the EXIF rotation applied and - * @throws Exception - */ - public function testResizeAutoExifRotated30x30() - { - if (!function_exists('exif_read_data')) { - $this->markTestSkipped('Missing exif extension'); - } - - $this->setSource(self::SRC_LANDSCAPE_ROTATED); - $this->createFixtureResizer(); - $this->resizer->resize(30, 30); - $this->assertImageSameAsFixture(__METHOD__); - } - /** * Given a Resizer with any image * When the sharpen method is called with a valid value @@ -390,7 +381,7 @@ public function testSharpen() { $this->setSource(self::SRC_SQUARE); $this->createFixtureResizer(); - $this->resizer->resize(25, 25, ['sharpen' => 50]); + $this->resizer->resize(100, 100, ['sharpen' => 50]); $this->assertImageSameAsFixture(__METHOD__); } @@ -400,11 +391,11 @@ public function testSharpen() * Then the saved image should be cropped as expected * @throws Exception */ - public function testCrop10x15() + public function testCrop30x45() { $this->setSource(self::SRC_PORTRAIT); $this->createFixtureResizer(); - $this->resizer->crop(3, 5, 10, 15); + $this->resizer->crop(10, 50, 30, 45); $this->assertImageSameAsFixture(__METHOD__); } @@ -461,12 +452,13 @@ protected function assertImageSameAsFixture(string $methodName) // Save resizer result to temp file $this->resizer->save($this->tmpTarget); - // Assert file is the same as expected output with 1% error permitted to account for library updates and whatnot + // Assert file is the same as expected output with 5% margin of error permitted to account for library + // updates and differences between OS image manipulation libraries. $this->assertSimilarGD( $this->tmpTarget, $this->target, $methodName . ' result did not match ' . $this->target, - 0.01 + 0.05 ); } } diff --git a/tests/fixtures/resizer/source/landscape_rotated.jpg b/tests/fixtures/resizer/source/landscape_rotated.jpg index cd28b252ae2ce6f2ffd86d8436ada346cce4380f..6145aa9c28a4bc9550ea60db2b5a6cf59ffac9fc 100644 GIT binary patch literal 22992 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 Z5{(x3G0!>k6;??^ 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 Xm{Q@}Qz~J>00000NkvXXu0mjfDMO4_ diff --git a/tests/fixtures/resizer/source/portrait.gif b/tests/fixtures/resizer/source/portrait.gif index 9de1b0980eb7fa848ab93da71f6a9ce3cd1f8292..5d287635064c22b21c12f48c63f8f4afce54e2ad 100644 GIT binary patch literal 1292 zcmV+n1@rnxNk%w1VQv7C0Q3L=0000hr^hU%#Vn`AETzXRrp7I%#w@4CEvLpTr^hX) z$1bMEE~m#WsKza($StSHE~m*ZsK@{S00000000000000000000000000000000000 z000000000000000A^8LW00000A^!_WZDD6+O<`wgV`~j(VQp<;JuogbH8eFf04x9i z003?PkpKV%{~#caWNDsgs;+G7zHlthbZy^wuJ3&B|G=PdNGuwU$fR<~Y&xINsB}uL zTCdowcFXO0zu>T#sTd~(3W52ER(<}zYP|wf|1=rk#-%rIikL=;lRo*!5=rIZi zR`&AlGY<4U`zip&(DXuGL4PX92{f_r1wj`LNfc8spoXxB0LZzi@sJrp5$vkzi->Wd zrjj4M|6znd3E{#B_m)X|R1rag6b*|Z5ZOZ|y@dp>0RWbWCV+T2d6r;zPe?lxk$H5*0$ZZS%J$dc@#*^G{A|{B-Wl#iBoLfozv%yb2`!9 zXcK@%Y&{hUs?fxHQzYn~yu}0d3BCt>WL@A)Mj>SsXlYM(w#Fk(gu;Do<#U5im>qH1 z|A6H!m7gjL%F-Zk>}*8Ha_w~2%7n4~_6Ba&C3cetczHL%0qmUz25%;!5Z)aVN%dTY z38vGVj4NEk)PpP>rx_LqAmAJ{KgvKIDLS&!h=4?T_yTNMXour`iWTEih$~>o6c_6i zX+nWbNyZLN-C-BTei`~F!bCjj5f=&XwG$s62+Fykg-5wH&y;=yHKv4kCZ=L1%-J^~ zg^!H5o0BrcXlS5;X?TN{HKKsxo-#bB7=w_4rel-#44DIebB*<9MuXmRsCX(e%21i| z<=~^2BQT(oQqqC9kOEjj_v(#F)VW<+`|P?iSZi zJKNqSXcJ(TlEPsC-_@Wy{}M;6qDREI6qY5B6|ky!hO2=*=_B?V6K@4qfo#-Gt+Dv0^+XekIOln3lg~|ts2-lE9`3nkEY?! z(F#EoT?(&LM?9a^P9IYvI}{hhGt@U+y#l_X2k6m!!cZKfg@;Wk%yi~U;O z4ki#4xc~kAUo~+MJ_9+{08XJucPNhc6nh8OW_gnP?F_~cZ7Q&ZnRhOSY+pi)5PB_X zEjGyMs*rx?39MV+h5=P^~z^2{`lmVZ~pn{r?39{?6>d!`|!sv|NQjVZ@𓢜F CB2j1n literal 248 zcmZ?wbhEHblwwe3c+AHTo|+z!21FURGu3Wosol;}y$vGO?_{ao$x^$MrG6`0?KTLh z{|5ubpDc`A4Ezi_3_t)fgMnq~hm)T3S6YN0-u*8~U5Y2d@Ug{=%QF@v9adpt4n5o zo4Qmg!YTNh+(}nYqayz76gJk-25uL2#ZFm{{sv*@lzO(NNo>5qW>vFXr!TNe@DXmE pY*r{aRcT_gf-+aHD=jUj~4dy^uB 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 literal 1172 zcmex=pHlUm+$QP)9 z@&7Fb9%e=cCIMyv1_s8m79$apR--0^1&1ZwUhU-AIr*Y)>!TxzEdAGe4vAPUELgrE zn9FCSvZ%mTl?`u$7eAf4Dn#)pKhSJ>0R|>U7G#?lnV1<^gp36Q6@^8Bwr~LLW@Ke@ z7vjq9I9j)4$wc|KxuR=p+A@1umD3mJDy$E=eRi(@ZoRc3DF;|Z%jPaJ)3h}LPwm{(GqblExZjwle6vfQcjjt~z&jJC z8i!jZf7+!Cbdiz(F#H*rfq@5$8%72}LjlD=pk)giCvN-z3q5?mXXsgZV=<@LieLjT%S#SR6&u=;HKBT6Q2d2J}qRJI28osSG_CEpy} z`!`S`{@niA--D)n_I{D; z7+BbVX$uN~VFiqtL;=Ub#)}^oZk%}VAw0MQk{h0A%SUiDNUT0(*UT4I^HkJj*5We< zSc((=$#L24$~sbf#Osr$l+wF(Z?=a>1v}mgcDrVO!sWn!2AAYTA~zR5tFjin9<+La z_*a=dg4?EZ1iav57SPZQ|Nh-LSk=^We#hJX5SI%qzj6hP65n+cI@TM%c#v`D%`ZjH zZEaihA3d`6VlQGd;CU0?`$Ir_MarHgRoUsAzU@Bmo>a5_qF(Qe(kmYY6Osits@+Kp u;`YvpW8Skl@@)N+FJ-C{J1?wb5BHPMI`XPQfq{W};=xbHvO?$ozX05AVXNk%w1VIBZ20FeLyET+UPs?02=%m4rYA^8LW00093EC2ui03HA>0007^ zoDBkSed?l1OOC*-#wo6>S)AZtVF+O4QJtugneI6*>l|Vn?k+MfNl_?3M1mt5^%T06 zL$C`TKQl_e0BeGH6(OVu4vWXdEtzr)+nVBObo?gEHN?50=9D*K_JJ_f#}YW`0h30i z#a9Fsq1Dk7Hvwgn_rPIBhZjQ?wr4ZvCO1_HHtB=O6-A1pvh@RWI)RF!QaBq_EAdME KX|ZZh0028_F;Nr% literal 0 HcmV?d00001 diff --git a/tests/fixtures/resizer/target/testReset_testResize0x0_testResizeAutoLandscape1x1.png b/tests/fixtures/resizer/target/testReset_testResize0x0_testResizeAutoLandscape1x1.png index 8d820ac3226a1d3728252e63a0e29b6b35f77655..77db38b6bd1b99d12b6ba0496af5228e79a67471 100644 GIT binary patch literal 5880 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 Eg2ludZU6uP diff --git a/tests/fixtures/resizer/target/testResize0x20.gif b/tests/fixtures/resizer/target/testResize0x20.gif deleted file mode 100644 index 8b6014585ec41367f14ce3fd85b11060e07866b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 96 zcmZ?wbhEHb4 y=EI?- z-h8j7>0eEPa-W;#l*Y)e)idWETD|Z%*A2G4j5dpcZ`8FfXm88XtvI$QN4)!4-gyqg zZ$A~5i^MDa4l)(DduyDaUG02CCg_@5^pwpTm%KUC8F`&Y?p0Vy`STESarg9ZTiukN z2Sv0TD^+nZPcEFws_7|Fs+sij#eo?tod=E>Op!9YvE$a(DMDA+yIG5WUU|isa;ixC S%WqK|pO~DQo1Ld++~Z%_g(BrC~yo%G^}7B|BSv VG%_|P3n)zCVN4M0)#GBY1^}F5D_;Nr diff --git a/tests/fixtures/resizer/target/testResize50x0.gif b/tests/fixtures/resizer/target/testResize50x0.gif new file mode 100644 index 0000000000000000000000000000000000000000..a1881a317dc53bdaa848b53d213e733b62c13c34 GIT binary patch literal 330 zcmV-Q0k!@|Nk%w1VKD$f0FeLyET+UPs?02=%m4rYA^8LW00093EC2ui05JeT00091 zoR6u??GK}zwAzca-n{z{hT?buBxRmbszPjg07CK(^(Z6nP434GiwOrn5zinnnTkPO z0B}UMa*pVN$GkqURp!n2qtUVqs!f;Fg~5hEh5B-{QYo*&jGBRg%B9$?@uVgN#fR`@ z@ z(aIUQquG;cbD9U51}XWF+;szpx%4G`Tnu>@Hu||~5!#4^nBBIzB{v9-U89GQ{IWhu z1P|X#X<0)H>32i=J0!@HWB>pF literal 0 HcmV?d00001 diff --git a/tests/fixtures/resizer/target/testResizeAutoExifRotated30x30.jpg b/tests/fixtures/resizer/target/testResizeAutoExifRotated30x30.jpg deleted file mode 100644 index e4e18394de29bd22610504f93a97e48cfdfcbb88..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 859 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 diff --git a/tests/fixtures/resizer/target/testResizeAutoLandscape1x50.png b/tests/fixtures/resizer/target/testResizeAutoLandscape1x50.png new file mode 100644 index 0000000000000000000000000000000000000000..be0ed2b7f9098220d6fcb70aa3bd1aa2e1d2460b GIT binary patch literal 3362 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 G&;$S^a(UnY diff --git a/tests/fixtures/resizer/target/testResizeAutoLandscape25x1.png b/tests/fixtures/resizer/target/testResizeAutoLandscape25x1.png deleted file mode 100644 index d8afea488dd1db46f9f88d55588b591e911a9315..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 310 zcmeAS@N?(olHy`uVBq!ia0vp^l0eMD!3HF&7fw+FQk(@Ik;M!Qd`Cc-ajG_-G*Iw` zr;B5Vgrw;O|8B=bk>ekWTaC;%1$Y_Fz8U$EFKww#O7f|BQ&S8*C+p~_7EcyW$ThC! z=B{MT3OeOAdCe7(;FkC23toM;d4A{jAItAn`Adt=op}}TMbMos-Q=Ppzp2Kq3%}$= zkH`t?OgL(umis>CXpHO8(+`wd?R7Tp46shi{KQq#z~|>=c${kS z_rEgLo@D9b&+uoAe(@;W<$dYL>wgwE|FmN+^ZBr*jCJw?)1dp^i}HbfX7F_Nb6Mw< G&;$S^a(UnY diff --git a/tests/fixtures/resizer/target/testResizeAutoPortrait50.gif b/tests/fixtures/resizer/target/testResizeAutoPortrait50.gif index 74b53003eb03d485c559f1685278c52e9bc17fe0..163dc86560a600c254b825fe3a70c4b8ebcee90a 100644 GIT binary patch literal 218 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 diff --git a/tests/fixtures/resizer/target/testResizeExact50x75.jpg b/tests/fixtures/resizer/target/testResizeExact50x75.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4fd8bd080adf481f4dbd73d85a728f721a6d65eb GIT binary patch literal 3705 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/resizer/target/testResizeFitLandscape150x150.png b/tests/fixtures/resizer/target/testResizeFitLandscape150x150.png new file mode 100644 index 0000000000000000000000000000000000000000..be0ed2b7f9098220d6fcb70aa3bd1aa2e1d2460b GIT binary patch literal 3362 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 diff --git a/tests/fixtures/resizer/target/testResizeFitPortrait150x150.gif b/tests/fixtures/resizer/target/testResizeFitPortrait150x150.gif new file mode 100644 index 0000000000000000000000000000000000000000..134aae42b119c5d6a0c7722fc5433f8fa644772c GIT binary patch literal 1210 zcmV;r1V#HtNk%w1VR8VL0HOc@ET+UPs>Cd&%q*(R000000000000000A^8LW000C4 zEC2ui0CE79000C2NV?qqFv>}*y*TU5yZ>M)j$~<`XsWJk>%MR-&vb3yc&_h!@BhG{ za7Zi~kI1BQ$!xX&&KLk7fJR-@=@m=0ZdcRlmzDL7QDO7hlwPOGZge+(YSS-N3Vb5I z2l^3qfq8cXbAwtDRRe_!e`*X}kPB4;jgf#2f|>w~mkf^&dkvMJ4u_bD42z}@p^>Nx zVOy~mud-RIogAt;0fZXAmbiem7@0T%e;u=ixdc@K9>6-rwqk9~8`d`f%gmBhos$;> zy*lC_=Hlxg-orfF;Opea=R5Zr;?f_=@L>Nce4EA%gFHR;CP=z~Y}+#r{w%yps3%=T ziU=EOD7gM$q6l-;ApD}B5f^}ZC3Wd(N5UV)l;#NB(qz#=I&XmjI(USUSHXd&P8!m9 zk|@rU1Y$xE*Yg8UQ7QY(GzqDcvz(L0@pA!E=SY$8igH3mWot-S4(QDo7f|R@GazBP zRZvrAJ*6F$HDyybDM~ZrQUS&btzJlBd+UfBHupg$R7JhiZJbteSsnBcM*wP<@kuKu zx6(Dd_?KvWdUfXAnJ02#uYWy)<#666rF?Wcp38-fCw0}lQ}xh^VsjMb`R!?g z+OpPtwF&7Rzp|B*!{}lzt}Ck@4Qprpyl+Q(978N0FTu5}lI~9xi)JA;`+g_8&!#{e zy8biD;nDR+ZxyihnQ?eH=hR~4#7D|F0B+zCf;4Dl+-D6gl2{X67^P7(NYMf!Vrnq; zAWw^}!QFVgh4+|tzjPI$5zFa?6=3IYvj#diOcerb}a~ z5zc@~KG~Hhagsuulo~?dpe_UU5Kahu?uVhFT7s~s2Qf;r;b?A!=^{DGyf-6B782OO zfGHLjV|bhfN|P6V7Fd?5By_=NeW;j6OoMZssicN$5;Ww6vz|8yOn}*nsfL$I{)(PO zUXDg(k^q((*_fUBVjnAjLLgm9nkoQNqsd|stbD;i$jztOvH2TkF}7HQ?-^sl|P1-HPMdC~-m z!`1m)2TQ98Jep3AKDe5p5idS#7lDVqXOhfded>1yekM(r@)?PU|rd~5{T@vJ5z$%t1zPKv7;;D%9ZwcdwJc`+9$(Z znA|70%^$KLz+4T}w906;p=VdfwYIp0P37M%2m_8Zj~Wgr;w)ghjZ*1@7EBNiOiENt zppI7-kI{sS=3t$!ZUJ$`CAXqUZ8eilXjWQx{)Km|W)-*_`LPV&;8$QA)TgpDaeBwY zN*Q>{SQT<)%|WTIJKdik@BBu=Pk*cR4uOt+P`q#7wD$&S4gT}v8&v-J=%=s#`s}yw Y{`>I9FaP}X*Khy*_~);`>InbyBPfW;)Erq`$pOfZ>eC=|v0-)&SJ-G1mY9 diff --git a/tests/fixtures/resizer/target/testResizeLandscape100x1.gif b/tests/fixtures/resizer/target/testResizeLandscape100x1.gif new file mode 100644 index 0000000000000000000000000000000000000000..e3fc1a2085ea7d732fbd85b7727f2fd17d74e49b GIT binary patch literal 959 zcmV;w13>&oNk%w1VPpV<0HOc@ET+UPs>Cd&%q*(R000000000000000A^8LW000C4 zEC2ui0Av7z000C2NV?qqFv>}*y*TU5yZ>M)j$~<`XsWJk>%MR-&vb3yc&_h!@BhG{ za7Zi;0RSTlAW*K5P8Bnnf>KY@tI4V*WxFFTnDT{)AA;nVz*IlVj(~d|Xqx8@_*$*Hkpzdb%a)+sCjRG*tK_)rSLbNf&t^q25k|dSVB(IdZ zYUpUKGROYljDUXx5_ZHh?!~J=skWUQ#6XT4bPLMhx+uaT%R7shJ}qe2wUtR7f6GX zA9=_+c1hrD4;Ynpk~N}1yL;SP?pP_PS$evDl#k7#89XZ-3HO-aa^{^3Rd~4Jv0Lh{ag~e`lhR1e8@KVWOj*Q8-##@o>Xb z1U;=N(V^-|>KZVpZi<$t7N|Ppr|%57$)4Plddq;LV)V>gq@W5>2V=VMkq7yWhbC-I zyq4ou2T;a9<1<+T+o%oJ!t&vV#jZ-jv7kT`C#>gS%Ne7V7@9$}K=_xUx{)0B8feP~ zfoVnA5?AQF@tVo6w_~6ysEvZW*K0rZ;xMKyvc~(a!?QFC@xT*nQ*p2tTb%I)4R5TX h$FYJe^2j8YZ1Tw{r>yeIEVu0P%P_|*a})^x06Xp=%UJ*b literal 0 HcmV?d00001 diff --git a/tests/fixtures/resizer/target/testResizeLandscape10x1.gif b/tests/fixtures/resizer/target/testResizeLandscape10x1.gif deleted file mode 100644 index 30565a41e7d7fef8f228651d0954820f9211c18d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71 zcmZ?wbhEHbJQ YF!sOlyI|Vv>J^PVPdOPfWmy@l0kpyttN;K2 diff --git a/tests/fixtures/resizer/target/testResizePortrait1x10.gif b/tests/fixtures/resizer/target/testResizePortrait1x10.gif deleted file mode 100644 index 9055138bd48951afc1bf43d1eba4c04920c3922d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57 zcmZ?wbhEHbWM|-FXkcKtlcnCd&%q*(R000000000000000A^8LW000C4 zEC2ui080R5000C2NV?qqFv>}*y*TU5yZ>M)j$~<`XsWJk>%MR-&vb1+z%2rT9s)Vv zhbJ5ciyR^{d0ax83?%?)@3bRi93@Ul&2A!0( zkd*;>4RR`i4FXv}hHHx;jSF99Toi#Uj|)5j7;Hn6Qx|X+ni7q8q??xtf}DP=QJ$C% zp^y-xvWOdf4!Tiep0fmUwzRjY#jmmhV@+azw|d5;tXFqORh0y=aAw0hY+#(#3SU94 zaXF?aS%I+F+2~sCyqKBM8eiIz0A}b-=Gp8lCA#t}lc#R`6u}w+L=rq^Wq|o3gNWQh z4M4thdu9Fy3LQ0w^5C(TkdTB#s`RuQ*NET05(c3;AlGJ@t664D`WX4f&YN%+YcvF~ zktQHu{OoxkY5^ZGH#Gw(+tMoCgr|i@M$`mSqDG-ff2y)$G7{4;UojQbSwv&OnOI4T zToiJg%t2!-Sd8W{Przj=WtEGYaAT6az9tm~!|WTmeJR+@OW6RWp2}too6w3(Em3p8 zUT)?$waYnQluuXYd;;H?2ZVs;Rh5r`Wx4LO1u+@w&9ku=G3umK>E^H0kUR$W!}gu5 zV9KArz6|#;n8~pMOW&?C732usmw$R9p7vD?aUm?no&;5f@;@-RytD`A1#1A`6_1NjOJ3`}w@{VPwu080P>0#byJsmtvTqnxzbi?iOm z`wxcVNS5Y_rs~SJ?hD8AOxN~}=lag~{tpZahs2`sh)gP%%%<}RjY_A~s`ZM^YPa03 V_X`e-$K^86f06UlXIu8H< literal 0 HcmV?d00001 diff --git a/tests/fixtures/resizer/target/testSharpen.jpg b/tests/fixtures/resizer/target/testSharpen.jpg index a9b1a1875a8dead1753027fdecbc89d3cc5bd037..630b009d4724bbbd01cf9ab3539555be89d21140 100644 GIT binary patch delta 7097 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 delta 470 zcmV;{0V)2MJm?6JKLI$AK%oX102u=TBC+Si0e||R#Qy-cI(#_z58xedRkyX)ym{hX z5)D9DM1IR{7El>3W#x=<1~?1JVh98P%)V=B@uT*%*R=TjS!3Y@{=$(KWVVM!j7K66 z2*tbHeXYR+a=mhKit)=>mtXkN@P5Nw`Lw}d;QJV(kvAjSy}hi0B03d$<&{sa=C9R4 zz<+1v8T$0A*ydN8-1%?dpY4+$#cv9DlE(hd+d}c~m!(~55JeDXwY?EWcRXtU05UQ* z(D1P&4nsH1x8$$*cxT4X`UrSq{{RGs{{W9yYw-fjG%wqy#1QFmBnaA}J0ciT*BUyt z(dR#S&oiGw#PNJ_1emLk`*l&R(u#|ibis!h#xm36AG6z*FO#~uQg?l$vULo55IRU zjBjR%zRq7c<#3%2F<*L)hMeDU{VFbR$<;IA-+|w(ZmuDfWw@BdmGc}(%CV+Gi5OrU1^BQ26o Date: Mon, 15 Aug 2022 18:41:34 -0600 Subject: [PATCH 17/43] Respect session config when setting the auth manager cookie See https://github.com/laravel/framework/blob/b52ced2934f1c61af94dd02ea8032dd67c64ce86/src/Illuminate/Session/Middleware/StartSession.php#L210-L226 --- src/Auth/Manager.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Auth/Manager.php b/src/Auth/Manager.php index 12aa75e21..c398386ad 100644 --- a/src/Auth/Manager.php +++ b/src/Auth/Manager.php @@ -1,9 +1,11 @@ ipAddress = Request::ip(); + $this->sessionManager = App::make(SessionManager::class); } // @@ -479,7 +482,19 @@ protected function setPersistCodeInSession($user, $remember = true) Session::put($this->sessionKey, $toPersist); if ($remember) { - Cookie::queue(Cookie::forever($this->sessionKey, json_encode($toPersist))); + $config = $this->sessionManager->getSessionConfig(); + Cookie::queue( + Cookie::forever( + $this->sessionKey, + json_encode($toPersist), + $config['path'], + $config['domain'], + $config['secure'] ?? false, + $config['http_only'] ?? true, + false, + $config['same_site'] ?? null + ) + ); } } From c64d9ca46c1388a3cd2334c1163142837420c26d Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Mon, 22 Aug 2022 15:28:20 -0600 Subject: [PATCH 18/43] Improve accuracy of docblock message --- src/Database/Attach/File.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Attach/File.php b/src/Database/Attach/File.php index a3c4dc65c..e3ac5a0d5 100644 --- a/src/Database/Attach/File.php +++ b/src/Database/Attach/File.php @@ -123,7 +123,7 @@ public function fromPost($uploadedFile) } /** - * Creates a file object from a file on the disk. + * Creates a file object from a file on the local filesystem. * * @param string $filePath The path to the file. * @return static From c15ec51e8be9665f620536adfcfbac4c31462aa0 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson <31214002+jaxwilko@users.noreply.github.com> Date: Mon, 22 Aug 2022 22:31:01 +0100 Subject: [PATCH 19/43] Support creating File model from file on disk (#106) Adds support for creating a file model from a file on the disk returned by File->getDisk() via the fromStorage() method. Useful for processing file uploads to remote disks where the file is uploaded directly to the remote disk instead of first touching the application server. --- src/Database/Attach/File.php | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/Database/Attach/File.php b/src/Database/Attach/File.php index e3ac5a0d5..04ad3f753 100644 --- a/src/Database/Attach/File.php +++ b/src/Database/Attach/File.php @@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Storage; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\File as FileObj; +use Winter\Storm\Exception\ApplicationException; /** * File attachment model @@ -141,6 +142,29 @@ public function fromFile($filePath, $filename = null) return $this; } + /** + * Creates a file object from a file on the disk returned by $this->getDisk() + */ + public function fromStorage(string $filePath): static + { + $disk = $this->getDisk(); + + if (!$disk->exists($filePath)) { + throw new \InvalidArgumentException(sprintf('File `%s` was not found on the storage disk', $filePath)); + } + + $this->file_name = basename($filePath); + $this->file_size = $disk->size($filePath); + $this->content_type = $disk->mimeType($filePath); + $this->disk_name = $this->getDiskName(); + + if (!$disk->copy($filePath, $this->getDiskPath())) { + throw new ApplicationException(sprintf('Unable to copy `%s` to `%s`', $filePath, $this->getDiskPath())); + } + + return $this; + } + /** * Creates a file object from raw data. * @@ -535,8 +559,10 @@ public function beforeSave() if ($this->data !== null) { if ($this->data instanceof UploadedFile) { $this->fromPost($this->data); - } else { + } elseif (file_exists($this->data)) { $this->fromFile($this->data); + } else { + $this->fromStorage($this->data); } $this->data = null; From 583b7e1e39868beec1c2e3d7de449d4dd21b97db Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Wed, 24 Aug 2022 20:06:31 -0600 Subject: [PATCH 20/43] Allow the file_name & content_type to be set when using FileModel->fromStorage() The file path provided could be a UUID string inside of a temporary directory so we need to allow for the file_name and content_type to be explicitly set. --- src/Database/Attach/File.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Database/Attach/File.php b/src/Database/Attach/File.php index 04ad3f753..bec7c6d16 100644 --- a/src/Database/Attach/File.php +++ b/src/Database/Attach/File.php @@ -153,9 +153,14 @@ public function fromStorage(string $filePath): static throw new \InvalidArgumentException(sprintf('File `%s` was not found on the storage disk', $filePath)); } - $this->file_name = basename($filePath); + if (empty($this->file_name)) { + $this->file_name = basename($filePath); + } + if (empty($this->content_type)) { + $this->content_type = $disk->mimeType($filePath); + } + $this->file_size = $disk->size($filePath); - $this->content_type = $disk->mimeType($filePath); $this->disk_name = $this->getDiskName(); if (!$disk->copy($filePath, $this->getDiskPath())) { From a25db1067b119aa1fb86fa6364925577ab7273ed Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Sat, 27 Aug 2022 04:02:46 +0800 Subject: [PATCH 21/43] Add SVG utility (#61) Documented by https://github.com/wintercms/docs/commit/f73ee927a30b4324e27e472df8c8cd74d623858e --- composer.json | 1 + src/Foundation/Bootstrap/RegisterWinter.php | 3 + src/Support/Facades/Svg.php | 21 +++++++ src/Support/Svg.php | 60 +++++++++++++++++++ tests/Support/SvgTest.php | 22 +++++++ tests/fixtures/svg/extracted/winter-dirty.svg | 1 + tests/fixtures/svg/extracted/winter.svg | 1 + tests/fixtures/svg/winter-dirty.svg | 59 ++++++++++++++++++ tests/fixtures/svg/winter.svg | 56 +++++++++++++++++ 9 files changed, 224 insertions(+) create mode 100644 src/Support/Facades/Svg.php create mode 100644 src/Support/Svg.php create mode 100644 tests/Support/SvgTest.php create mode 100644 tests/fixtures/svg/extracted/winter-dirty.svg create mode 100644 tests/fixtures/svg/extracted/winter.svg create mode 100644 tests/fixtures/svg/winter-dirty.svg create mode 100644 tests/fixtures/svg/winter.svg diff --git a/composer.json b/composer.json index e323f54eb..6d7597aa2 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "assetic/framework": "~3.0", "doctrine/dbal": "^2.6", + "enshrined/svg-sanitize": "^0.15", "erusev/parsedown-extra": "~0.7", "laravel/framework": "^9.1", "laravel/tinker": "^2.7", diff --git a/src/Foundation/Bootstrap/RegisterWinter.php b/src/Foundation/Bootstrap/RegisterWinter.php index c9ff12699..073597b83 100644 --- a/src/Foundation/Bootstrap/RegisterWinter.php +++ b/src/Foundation/Bootstrap/RegisterWinter.php @@ -25,6 +25,9 @@ public function bootstrap(Application $app): void $app->singleton('string', function () { return new \Winter\Storm\Support\Str; }); + $app->singleton('svg', function () { + return new \Winter\Storm\Support\Svg; + }); /* * Change paths based on config diff --git a/src/Support/Facades/Svg.php b/src/Support/Facades/Svg.php new file mode 100644 index 000000000..c6e97a49e --- /dev/null +++ b/src/Support/Facades/Svg.php @@ -0,0 +1,21 @@ +removeRemoteReferences(true); + $sanitizer->removeXMLTag(true); + + if ($minify) { + $sanitizer->minify(true); + } + + return trim($sanitizer->sanitize($svg)); + } +} diff --git a/tests/Support/SvgTest.php b/tests/Support/SvgTest.php new file mode 100644 index 000000000..7bc723271 --- /dev/null +++ b/tests/Support/SvgTest.php @@ -0,0 +1,22 @@ +assertEquals($fixture, $svg); + } + + public function testDirtySvg() + { + $svg = Svg::extract(dirname(__DIR__) . '/fixtures/svg/winter-dirty.svg'); + $fixture = trim(file_get_contents(dirname(__DIR__) . '/fixtures/svg/extracted/winter-dirty.svg')); + + $this->assertEquals($fixture, $svg); + } +} diff --git a/tests/fixtures/svg/extracted/winter-dirty.svg b/tests/fixtures/svg/extracted/winter-dirty.svg new file mode 100644 index 000000000..31a09caba --- /dev/null +++ b/tests/fixtures/svg/extracted/winter-dirty.svg @@ -0,0 +1 @@ + link diff --git a/tests/fixtures/svg/extracted/winter.svg b/tests/fixtures/svg/extracted/winter.svg new file mode 100644 index 000000000..6386f7eb1 --- /dev/null +++ b/tests/fixtures/svg/extracted/winter.svg @@ -0,0 +1 @@ + diff --git a/tests/fixtures/svg/winter-dirty.svg b/tests/fixtures/svg/winter-dirty.svg new file mode 100644 index 000000000..55656a44c --- /dev/null +++ b/tests/fixtures/svg/winter-dirty.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + shouldn't be here + + + + + + link + + + diff --git a/tests/fixtures/svg/winter.svg b/tests/fixtures/svg/winter.svg new file mode 100644 index 000000000..6c7ddced0 --- /dev/null +++ b/tests/fixtures/svg/winter.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 66c48000f910dc3feed685182f0b8fc36537ec84 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson <31214002+jaxwilko@users.noreply.github.com> Date: Thu, 1 Sep 2022 21:24:50 +0100 Subject: [PATCH 22/43] Added support for including anon classes multiple times per execution (#109) --- src/Database/Updater.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Database/Updater.php b/src/Database/Updater.php index da497fd64..70edd56ee 100644 --- a/src/Database/Updater.php +++ b/src/Database/Updater.php @@ -13,6 +13,11 @@ */ class Updater { + /** + * @var array Local cache of migration file paths to support anonymous migrations [$path => $anonInstance || $className] + */ + protected static $migrationCache = []; + /** * Sets up a migration or seed file. */ @@ -69,19 +74,26 @@ public function packDown($file) * @param string $file * @return object|null */ - public function resolve($file) + public function resolve($file): ?object { if (!File::isFile($file)) { return null; } + if (isset(static::$migrationCache[$file])) { + return is_object(static::$migrationCache[$file]) + ? static::$migrationCache[$file] + : new static::$migrationCache[$file]; + } + $instance = require_once $file; if (is_object($instance)) { - return $instance; + return static::$migrationCache[$file] = $instance; } + if ($class = $this->getClassFromFile($file)) { - return new $class; + return new (static::$migrationCache[$file] = $class); } } From 87470d7f253f12d440fd356b2308a460ff2d4998 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Fri, 2 Sep 2022 08:35:58 -0600 Subject: [PATCH 23/43] Fix Updater->resolve() Type hint requires that a value is always returned. --- src/Database/Updater.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Database/Updater.php b/src/Database/Updater.php index 70edd56ee..10f5b488e 100644 --- a/src/Database/Updater.php +++ b/src/Database/Updater.php @@ -95,6 +95,8 @@ public function resolve($file): ?object if ($class = $this->getClassFromFile($file)) { return new (static::$migrationCache[$file] = $class); } + + return null; } /** From 39656d30c99a1d1b9bcc1ba80d35ae79f3fc0f09 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Sun, 4 Sep 2022 00:17:55 -0600 Subject: [PATCH 24/43] Add Winter\Storm\Console\Traits\HandlesCleanup trait Enables easily performing cleanup tasks on CLI commands through a cross platform interface by implementing the trait and a handleCleanup() method. Also reorgs the base command logic into traits as applicable. --- src/Console/Command.php | 67 +--------------- src/Console/Traits/ConfirmsWithInput.php | 2 +- src/Console/Traits/HandlesCleanup.php | 74 ++++++++++++++++++ src/Console/Traits/ProvidesAutocompletion.php | 76 +++++++++++++++++++ 4 files changed, 154 insertions(+), 65 deletions(-) create mode 100644 src/Console/Traits/HandlesCleanup.php create mode 100644 src/Console/Traits/ProvidesAutocompletion.php diff --git a/src/Console/Command.php b/src/Console/Command.php index b3a3c3af8..feb886b17 100644 --- a/src/Console/Command.php +++ b/src/Console/Command.php @@ -1,9 +1,6 @@ newLine(); } - - /** - * Provide autocompletion for this command's input - */ - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - $inputs = [ - 'arguments' => $input->getArguments(), - 'options' => $input->getOptions(), - ]; - - foreach ($inputs as $type => $data) { - switch ($type) { - case 'arguments': - $dataType = 'Argument'; - $suggestionType = 'Values'; - break; - case 'options': - $dataType = 'Option'; - $suggestionType = 'Options'; - break; - default: - // This should not be possible to ever be triggered given the type is hardcoded above - throw new \Exception('Invalid input type being parsed during completion'); - } - if (!empty($data)) { - foreach ($data as $name => $value) { - // Skip the command argument since that's handled by Artisan directly - if ( - $type === 'arguments' - && in_array($name, ['command']) - ) { - continue; - } - - $inputRoutingMethod = "mustSuggest{$dataType}ValuesFor"; - $suggestionValuesMethod = Str::camel('suggest ' . $name) . $suggestionType; - $suggestionsMethod = 'suggest' . $suggestionType; - - if ( - method_exists($this, $suggestionValuesMethod) - && $input->{$inputRoutingMethod}($name) - ) { - $values = $this->$suggestionValuesMethod($value, $inputs); - $suggestions->{$suggestionsMethod}($values); - } - } - } - } - } - - /** - * Example implementation of a suggestion method - */ - // public function suggestMyArgumentValues(string $value = null, array $allInput): array - // { - // if ($allInput['arguments']['dependent'] === 'matches') { - // return ['some', 'suggested', 'values']; - // } - // return ['all', 'values']; - // } } diff --git a/src/Console/Traits/ConfirmsWithInput.php b/src/Console/Traits/ConfirmsWithInput.php index 8cc25d3e1..115e6fcf1 100644 --- a/src/Console/Traits/ConfirmsWithInput.php +++ b/src/Console/Traits/ConfirmsWithInput.php @@ -4,7 +4,7 @@ * Console Command Trait that provides confirmation step that requires set * input to be provided in order to act as confirmation for an action * - * @package winter\wn-system-module + * @package winter\storm * @author Luke Towers */ trait ConfirmsWithInput diff --git a/src/Console/Traits/HandlesCleanup.php b/src/Console/Traits/HandlesCleanup.php new file mode 100644 index 000000000..b71b208fc --- /dev/null +++ b/src/Console/Traits/HandlesCleanup.php @@ -0,0 +1,74 @@ +handleCleanup(); + } + + // Exit cleanly at this point if this was a user termination + if (in_array($signal, [SIGINT, SIGQUIT])) { + exit(0); + } + } + + /** + * Handle the provided Windows process singal. + */ + public function handleWindowsSignal(int $event): void + { + // Remove the handler + sapi_windows_set_ctrl_handler([$this, 'handleWindowsSignal'], false); + + // Handle the signal + if ( + method_exists($this, 'handleCleanup') + && ( + $event === PHP_WINDOWS_EVENT_CTRL_C + || $event === PHP_WINDOWS_EVENT_CTRL_BREAK + ) + ) { + $this->handleCleanup(); + + // Exit cleanly at this point if this was a user termination + exit(0); + } + } +} diff --git a/src/Console/Traits/ProvidesAutocompletion.php b/src/Console/Traits/ProvidesAutocompletion.php new file mode 100644 index 000000000..c7b230d09 --- /dev/null +++ b/src/Console/Traits/ProvidesAutocompletion.php @@ -0,0 +1,76 @@ + $input->getArguments(), + 'options' => $input->getOptions(), + ]; + + foreach ($inputs as $type => $data) { + switch ($type) { + case 'arguments': + $dataType = 'Argument'; + $suggestionType = 'Values'; + break; + case 'options': + $dataType = 'Option'; + $suggestionType = 'Options'; + break; + default: + // This should not be possible to ever be triggered given the type is hardcoded above + throw new \Exception('Invalid input type being parsed during completion'); + } + if (!empty($data)) { + foreach ($data as $name => $value) { + // Skip the command argument since that's handled by Artisan directly + if ( + $type === 'arguments' + && in_array($name, ['command']) + ) { + continue; + } + + $inputRoutingMethod = "mustSuggest{$dataType}ValuesFor"; + $suggestionValuesMethod = Str::camel('suggest ' . $name) . $suggestionType; + $suggestionsMethod = 'suggest' . $suggestionType; + + if ( + method_exists($this, $suggestionValuesMethod) + && $input->{$inputRoutingMethod}($name) + ) { + $values = $this->$suggestionValuesMethod($value, $inputs); + $suggestions->{$suggestionsMethod}($values); + } + } + } + } + } + + /** + * Example implementation of a suggestion method + */ + // public function suggestMyArgumentValues(string $value = null, array $allInput): array + // { + // if ($allInput['arguments']['dependent'] === 'matches') { + // return ['some', 'suggested', 'values']; + // } + // return ['all', 'values']; + // } +} From 3500516bb677e7142fccefbab3e5a3dbd6369784 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Sun, 4 Sep 2022 13:04:04 -0600 Subject: [PATCH 25/43] Add missing implementation for HandlesCleanup CLI trait to function on the base command class --- src/Console/Command.php | 3 ++- src/Console/Traits/HandlesCleanup.php | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Console/Command.php b/src/Console/Command.php index feb886b17..66f3e84a9 100644 --- a/src/Console/Command.php +++ b/src/Console/Command.php @@ -1,6 +1,7 @@ **NOTE:** This trait requires the implementing class to implement the + * Symfony\Component\Console\Command\SignalableCommandInterface interface + * * @package winter\storm * @author Luke Towers */ From 993562d0a35bebd5d0ba4657a9641d157d6ab774 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Sun, 4 Sep 2022 13:05:15 -0600 Subject: [PATCH 26/43] Use modern constant for clarity --- src/Console/Traits/HandlesCleanup.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Console/Traits/HandlesCleanup.php b/src/Console/Traits/HandlesCleanup.php index 1a58e784c..223705436 100644 --- a/src/Console/Traits/HandlesCleanup.php +++ b/src/Console/Traits/HandlesCleanup.php @@ -22,7 +22,7 @@ public function getSubscribedSignals(): array $signals = []; if (method_exists($this, 'handleCleanup')) { // Handle Windows OS - if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + if (PHP_OS_FAMILY === 'Windows') { // Attach to Windows Ctrl+C & Ctrl+Break events if (function_exists('sapi_windows_set_ctrl_handler')) { sapi_windows_set_ctrl_handler([$this, 'handleWindowsSignal'], true); From 6112214ea7217af95fc51f81df1aeb2295fbc20f Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Wed, 7 Sep 2022 16:00:02 -0400 Subject: [PATCH 27/43] add dummy fireEvent() method --- tests/Extension/ExtendableTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/Extension/ExtendableTest.php b/tests/Extension/ExtendableTest.php index 4e7a6efb7..f6b6f94e8 100644 --- a/tests/Extension/ExtendableTest.php +++ b/tests/Extension/ExtendableTest.php @@ -355,6 +355,12 @@ public function getProtectedFooAttribute() { return $this->protectedFoo; } + + // dummy method to appease test case on mock object + public function fireEvent($event) + { + return $event; + } } /* From 8dab2d78e0a31b6c5b98591148086f885d7372d9 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Wed, 7 Sep 2022 16:01:26 -0400 Subject: [PATCH 28/43] Revert "add dummy fireEvent() method" This reverts commit 6112214ea7217af95fc51f81df1aeb2295fbc20f. --- tests/Extension/ExtendableTest.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/Extension/ExtendableTest.php b/tests/Extension/ExtendableTest.php index f6b6f94e8..4e7a6efb7 100644 --- a/tests/Extension/ExtendableTest.php +++ b/tests/Extension/ExtendableTest.php @@ -355,12 +355,6 @@ public function getProtectedFooAttribute() { return $this->protectedFoo; } - - // dummy method to appease test case on mock object - public function fireEvent($event) - { - return $event; - } } /* From 09005bcc44c765340a58d07934b2f9934b704d9c Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Mon, 12 Sep 2022 16:06:51 +0800 Subject: [PATCH 29/43] Update testing of trusted proxies Includes testing of the chained proxies introduced in https://github.com/wintercms/storm/commit/411695b3e7c7c3b32f702a625d6612319cc4052d, and adjusts the scenarios to more closely test according to how the TrustedProxy library tested. Fixes https://github.com/wintercms/winter/issues/637 --- .../Middleware/CheckForTrustedProxiesTest.php | 225 +++++++++++++++--- 1 file changed, 193 insertions(+), 32 deletions(-) diff --git a/tests/Foundation/Http/Middleware/CheckForTrustedProxiesTest.php b/tests/Foundation/Http/Middleware/CheckForTrustedProxiesTest.php index 3c6b76e40..cbde74f1f 100644 --- a/tests/Foundation/Http/Middleware/CheckForTrustedProxiesTest.php +++ b/tests/Foundation/Http/Middleware/CheckForTrustedProxiesTest.php @@ -20,7 +20,7 @@ public function testUntrusted() { $request = $this->createProxiedRequest(); - $this->assertEquals('173.174.200.38', $request->getClientIp()); + $this->assertEquals('192.168.10.10', $request->getClientIp()); $this->assertEquals('http', $request->getScheme()); $this->assertEquals('root.host', $request->getHost()); $this->assertEquals(8000, $request->getPort()); @@ -34,9 +34,9 @@ public function testUntrusted() public function testTrustedProxy() { $request = $this->createProxiedRequest(); - $request->setTrustedProxies(['173.174.200.38'], Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO); + $request->setTrustedProxies(['192.168.10.10'], Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO); - $this->assertEquals('192.168.10.10', $request->getClientIp()); + $this->assertEquals('173.174.200.38', $request->getClientIp()); $this->assertEquals('https', $request->getScheme()); $this->assertEquals('proxy.host', $request->getHost()); $this->assertEquals(443, $request->getPort()); @@ -53,7 +53,7 @@ public function testTrustedProxyMiddlewareWithWildcard() $request = $this->createProxiedRequest(); $middleware->handle($request, function ($request) { - $this->assertEquals('192.168.10.10', $request->getClientIp()); + $this->assertEquals('173.174.200.38', $request->getClientIp()); $this->assertEquals('https', $request->getScheme()); $this->assertEquals('proxy.host', $request->getHost()); $this->assertEquals(443, $request->getPort()); @@ -67,15 +67,26 @@ public function testTrustedProxyMiddlewareWithWildcard() */ public function testTrustedProxyMiddlewareWithStringIp() { - $middleware = $this->createTrustedProxyMock('173.174.200.38', 'HEADER_X_FORWARDED_ALL'); + $middleware = $this->createTrustedProxyMock('192.168.10.10', 'HEADER_X_FORWARDED_ALL'); $request = $this->createProxiedRequest(); $middleware->handle($request, function ($request) { - $this->assertEquals('192.168.10.10', $request->getClientIp()); + $this->assertEquals('173.174.200.38', $request->getClientIp()); $this->assertEquals('https', $request->getScheme()); $this->assertEquals('proxy.host', $request->getHost()); $this->assertEquals(443, $request->getPort()); }); + + // Failure state + $middleware = $this->createTrustedProxyMock('192.168.12.12', 'HEADER_X_FORWARDED_ALL'); + $request = $this->createProxiedRequest(); + + $middleware->handle($request, function ($request) { + $this->assertEquals('192.168.10.10', $request->getClientIp()); + $this->assertEquals('http', $request->getScheme()); + $this->assertEquals('root.host', $request->getHost()); + $this->assertEquals(8000, $request->getPort()); + }); } /** @@ -85,15 +96,26 @@ public function testTrustedProxyMiddlewareWithStringIp() */ public function testTrustedProxyMiddlewareWithStringCsv() { - $middleware = $this->createTrustedProxyMock('173.174.200.38, 173.174.200.38', 'HEADER_X_FORWARDED_ALL'); + $middleware = $this->createTrustedProxyMock('192.168.10.10, 192.168.11.11', 'HEADER_X_FORWARDED_ALL'); $request = $this->createProxiedRequest(); $middleware->handle($request, function ($request) { - $this->assertEquals('192.168.10.10', $request->getClientIp()); + $this->assertEquals('173.174.200.38', $request->getClientIp()); $this->assertEquals('https', $request->getScheme()); $this->assertEquals('proxy.host', $request->getHost()); $this->assertEquals(443, $request->getPort()); }); + + // Failure state + $middleware = $this->createTrustedProxyMock('192.168.12.12, 192.168.13.13', 'HEADER_X_FORWARDED_ALL'); + $request = $this->createProxiedRequest(); + + $middleware->handle($request, function ($request) { + $this->assertEquals('192.168.10.10', $request->getClientIp()); + $this->assertEquals('http', $request->getScheme()); + $this->assertEquals('root.host', $request->getHost()); + $this->assertEquals(8000, $request->getPort()); + }); } /** @@ -103,29 +125,22 @@ public function testTrustedProxyMiddlewareWithStringCsv() */ public function testTrustedProxyMiddlewareWithArray() { - $middleware = $this->createTrustedProxyMock(['173.174.200.38', '173.174.200.38'], 'HEADER_X_FORWARDED_ALL'); + $middleware = $this->createTrustedProxyMock(['192.168.10.10', '192.168.11.11'], 'HEADER_X_FORWARDED_ALL'); $request = $this->createProxiedRequest(); $middleware->handle($request, function ($request) { - $this->assertEquals('192.168.10.10', $request->getClientIp()); + $this->assertEquals('173.174.200.38', $request->getClientIp()); $this->assertEquals('https', $request->getScheme()); $this->assertEquals('proxy.host', $request->getHost()); $this->assertEquals(443, $request->getPort()); }); - } - /** - * Test an untrusted connection through a proxy through middleware with an array of IP addresses. - * - * @return void - */ - public function testUntrustedProxyMiddlewareWithArray() - { - $middleware = $this->createTrustedProxyMock(['173.174.100.1', '173.174.100.2'], 'HEADER_X_FORWARDED_ALL'); + // Failure state + $middleware = $this->createTrustedProxyMock(['192.168.12.12', '192.168.13.13'], 'HEADER_X_FORWARDED_ALL'); $request = $this->createProxiedRequest(); $middleware->handle($request, function ($request) { - $this->assertEquals('173.174.200.38', $request->getClientIp()); + $this->assertEquals('192.168.10.10', $request->getClientIp()); $this->assertEquals('http', $request->getScheme()); $this->assertEquals('root.host', $request->getHost()); $this->assertEquals(8000, $request->getPort()); @@ -139,11 +154,11 @@ public function testUntrustedProxyMiddlewareWithArray() */ public function testUntrustedHeaders() { - $middleware = $this->createTrustedProxyMock('173.174.200.38', Request::HEADER_FORWARDED); + $middleware = $this->createTrustedProxyMock('192.168.10.10', Request::HEADER_FORWARDED); $request = $this->createProxiedRequest(); $middleware->handle($request, function ($request) { - $this->assertEquals('173.174.200.38', $request->getClientIp()); + $this->assertEquals('192.168.10.10', $request->getClientIp()); $this->assertEquals('http', $request->getScheme()); $this->assertEquals('root.host', $request->getHost()); $this->assertEquals(8000, $request->getPort()); @@ -157,11 +172,11 @@ public function testUntrustedHeaders() */ public function testTrustOnlyForwardedFor() { - $middleware = $this->createTrustedProxyMock('173.174.200.38', Request::HEADER_X_FORWARDED_FOR); + $middleware = $this->createTrustedProxyMock('192.168.10.10', Request::HEADER_X_FORWARDED_FOR); $request = $this->createProxiedRequest(); $middleware->handle($request, function ($request) { - $this->assertEquals('192.168.10.10', $request->getClientIp()); + $this->assertEquals('173.174.200.38', $request->getClientIp()); $this->assertEquals('http', $request->getScheme()); $this->assertEquals('root.host', $request->getHost()); $this->assertEquals(8000, $request->getPort()); @@ -175,11 +190,11 @@ public function testTrustOnlyForwardedFor() */ public function testTrustOnlyForwardedPort() { - $middleware = $this->createTrustedProxyMock('173.174.200.38', Request::HEADER_X_FORWARDED_PORT); + $middleware = $this->createTrustedProxyMock('192.168.10.10', Request::HEADER_X_FORWARDED_PORT); $request = $this->createProxiedRequest(); $middleware->handle($request, function ($request) { - $this->assertEquals('173.174.200.38', $request->getClientIp()); + $this->assertEquals('192.168.10.10', $request->getClientIp()); $this->assertEquals('http', $request->getScheme()); $this->assertEquals('root.host', $request->getHost()); $this->assertEquals(443, $request->getPort()); @@ -195,11 +210,11 @@ public function testTrustOnlyForwardedPort() */ public function testTrustOnlyForwardedProto() { - $middleware = $this->createTrustedProxyMock('173.174.200.38', 'HEADER_X_FORWARDED_PROTO'); + $middleware = $this->createTrustedProxyMock('192.168.10.10', Request::HEADER_X_FORWARDED_PROTO); $request = $this->createProxiedRequest(); $middleware->handle($request, function ($request) { - $this->assertEquals('173.174.200.38', $request->getClientIp()); + $this->assertEquals('192.168.10.10', $request->getClientIp()); $this->assertEquals('https', $request->getScheme()); $this->assertEquals('root.host', $request->getHost()); $this->assertEquals(8000, $request->getPort()); @@ -214,19 +229,165 @@ public function testTrustOnlyForwardedProto() public function testTrustOnlyForwardedHostPortAndProto() { $middleware = $this->createTrustedProxyMock( - '173.174.200.38', + '192.168.10.10', Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO ); $request = $this->createProxiedRequest(); $middleware->handle($request, function ($request) { - $this->assertEquals('173.174.200.38', $request->getClientIp()); + $this->assertEquals('192.168.10.10', $request->getClientIp()); $this->assertEquals('https', $request->getScheme()); $this->assertEquals('proxy.host', $request->getHost()); $this->assertEquals(443, $request->getPort()); }); } + /** + * Tests getting client IP from the "X-Forwarded-For" header with multiple IP addresses + * + * @return void + */ + public function testGetClientIps() + { + $middleware = $this->createTrustedProxyMock( + '192.168.10.10', + Request::HEADER_X_FORWARDED_FOR + ); + + $forwardedFor = [ + '192.0.2.2', + '192.0.2.2, 192.0.2.199', + '192.0.2.2, 192.0.2.199, 99.99.99.99', + '192.0.2.2,192.0.2.199', + ]; + + foreach ($forwardedFor as $forwardedForHeader) { + $request = $this->createProxiedRequest(['HTTP_X_FORWARDED_FOR' => $forwardedForHeader]); + + $middleware->handle($request, function ($request) { + $ips = $request->getClientIps(); + $this->assertEquals('192.0.2.2', end($ips)); + }); + } + } + + /** + * Tests getting client IP from the "X-Forwarded-For" header with multiple IP addresses where some + * proxies are trusted. + * + * @return void + */ + public function testGetClientIpSomeProxiesTrusted() + { + $middleware = $this->createTrustedProxyMock( + ['192.168.10.10', '192.0.2.199'], + Request::HEADER_X_FORWARDED_FOR + ); + + $forwardedFor = [ + '192.0.2.2', + '192.0.2.2, 192.0.2.199', + '99.99.99.99, 192.0.2.2, 192.0.2.199', + '192.0.2.2,192.0.2.199', + ]; + + foreach ($forwardedFor as $forwardedForHeader) { + $request = $this->createProxiedRequest(['HTTP_X_FORWARDED_FOR' => $forwardedForHeader]); + + $middleware->handle($request, function ($request) { + $this->assertEquals('192.0.2.2', $request->getClientIp()); + }); + } + } + + /** + * Tests getting client IP from the "X-Forwarded-For" header with multiple IP addresses where all + * proxies are trusted through the wildcard character. + * + * @return void + */ + public function testGetClientIpAllProxiesTrusted() + { + $middleware = $this->createTrustedProxyMock( + '*', + Request::HEADER_X_FORWARDED_FOR + ); + + $forwardedFor = [ + '192.0.2.2', + '192.0.2.199, 192.0.2.2', + '192.0.2.199,192.0.2.2', + '99.99.99.99,192.0.2.199,192.0.2.2', + ]; + + foreach ($forwardedFor as $forwardedForHeader) { + $request = $this->createProxiedRequest(['HTTP_X_FORWARDED_FOR' => $forwardedForHeader]); + + $middleware->handle($request, function ($request) { + $this->assertEquals('192.0.2.2', $request->getClientIp()); + }); + } + } + + /** + * Tests getting client IP from the "X-Forwarded-For" header with multiple IP addresses where all + * proxies in the chain are trusted through the double-wildcard ('**') characters. + * + * @return void + */ + public function testGetClientIpAllChainedProxiesTrusted() + { + $middleware = $this->createTrustedProxyMock( + '**', + Request::HEADER_X_FORWARDED_FOR + ); + + $forwardedFor = [ + '192.0.2.2', + '192.0.2.2, 192.0.2.199', + '192.0.2.2, 99.99.99.99, 192.0.2.199', + '192.0.2.2, 2001:0db8:0a0b:12f0:0000:0000:0000:0001, 192.0.2.199', + '192.0.2.2, 2c01:0db8:0a0b:12f0:0000:0000:0000:0001, 192.0.2.199', + '192.0.2.2,192.0.2.199', + ]; + + foreach ($forwardedFor as $forwardedForHeader) { + $request = $this->createProxiedRequest(['HTTP_X_FORWARDED_FOR' => $forwardedForHeader]); + + $middleware->handle($request, function ($request) { + $this->assertEquals('192.0.2.2', $request->getClientIp()); + }); + } + + // Failure state + $middleware = $this->createTrustedProxyMock( + '*', + Request::HEADER_X_FORWARDED_FOR + ); + + $forwardedFor = [ + '192.0.2.2', + '192.0.2.2, 192.0.2.199', + '192.0.2.2, 99.99.99.99, 192.0.2.199', + '192.0.2.2, 2001:0db8:0a0b:12f0:0000:0000:0000:0001, 192.0.2.199', + '192.0.2.2, 2c01:0db8:0a0b:12f0:0000:0000:0000:0001, 192.0.2.199', + '192.0.2.2,192.0.2.199', + ]; + + // Items 1 should pass. The rest should fail. + foreach ($forwardedFor as $i => $forwardedForHeader) { + $request = $this->createProxiedRequest(['HTTP_X_FORWARDED_FOR' => $forwardedForHeader]); + + $middleware->handle($request, function ($request) use ($i) { + if ($i === 0) { + $this->assertEquals('192.0.2.2', $request->getClientIp(), 'Index ' . $i . ' failed when it should pass'); + } else { + $this->assertNotEquals('192.0.2.2', $request->getClientIp(), 'Index ' . $i . ' passed when it should fail'); + } + }); + } + } + /** * Create a proxied request for testing. * @@ -236,13 +397,13 @@ public function testTrustOnlyForwardedHostPortAndProto() protected function createProxiedRequest(array $overrides = []) { $defaults = [ - 'HTTP_X_FORWARDED_FOR' => '192.168.10.10', + 'HTTP_X_FORWARDED_FOR' => '173.174.200.38', 'HTTP_X_FORWARDED_HOST' => 'proxy.host', 'HTTP_X_FORWARDED_PORT' => '443', 'HTTP_X_FORWARDED_PROTO' => 'https', 'SERVER_PORT' => 8000, 'HTTP_HOST' => 'root.host', - 'REMOTE_ADDR' => '173.174.200.38', + 'REMOTE_ADDR' => '192.168.10.10', ]; $request = Request::create( From a5e19806e640cf7a51bb5695c5c8426d2e058b60 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Wed, 21 Sep 2022 12:25:52 -0400 Subject: [PATCH 30/43] Fix 'ClassName@method' Event handler format. (#116) Fix for Event::listen('event-name', 'ClassName@method') in L9 when ClassName is not fully qualified but has been bound to the App with $this->app->bind('ClassName', \Path\To\ClassName::class); --- src/Events/Dispatcher.php | 27 +++++++++++++++++ tests/Events/DispatcherTest.php | 53 ++++++++++++++++++++++++++++++++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/Events/Dispatcher.php b/src/Events/Dispatcher.php index 788606829..9cca58a08 100644 --- a/src/Events/Dispatcher.php +++ b/src/Events/Dispatcher.php @@ -265,4 +265,31 @@ protected function callQueueMethodOnHandler($class, $method, $arguments) 'class' => $class, 'method' => $method, 'data' => serialize($arguments), ]); } + + /** + * Create the class based event callable. + * + * @param array|string $listener + * @return callable + */ + protected function createClassCallable($listener) + { + [$class, $method] = is_array($listener) + ? $listener + : $this->parseClassCallable($listener); + + $listener = $this->container->make($class); + + if (! method_exists($listener, $method)) { + $method = '__invoke'; + } + + if ($this->handlerShouldBeQueued($class)) { + return $this->createQueuedHandlerCallable($class, $method); + } + + return $this->handlerShouldBeDispatchedAfterDatabaseTransactions($listener) + ? $this->createCallbackForListenerRunningAfterCommits($listener, $method) + : [$listener, $method]; + } } diff --git a/tests/Events/DispatcherTest.php b/tests/Events/DispatcherTest.php index 2cf08f419..8cbb8648d 100644 --- a/tests/Events/DispatcherTest.php +++ b/tests/Events/DispatcherTest.php @@ -1,4 +1,4 @@ -dispatch(new EventTest()); $this->assertTrue($magic_value); } + + /** + * Test [$classInstance, 'method'] event listener format + */ + public function testInstanceMethodListen() + { + $dispatcher = new Dispatcher(); + $classInstance = new TestClass; + + $dispatcher->listen('test.test', [$classInstance, 'instanceMethodHandler']); + $dispatcher->fire('test.test'); + + $this->assertTrue($classInstance->getMagicValue()); + } + + /** + * Test 'ClassName@method' event listener format + */ + public function testClassMethodListen() + { + $magic_value = false; + $this->app->bind('TestClass', TestClass::class); + + Event::listen('test.test', 'TestClass@classMethodHandler'); + Event::fire('test.test', [&$magic_value]); + + $this->assertTrue($magic_value); + } +} + +class TestClass +{ + protected $magic_value = false; + + public function instanceMethodHandler() + { + $this->magic_value = true; + } + + public function classMethodHandler(&$value) + { + $value = true; + } + + public function getMagicValue() + { + return $this->magic_value; + } } From 5c46aa3ebe96370489ee6e02589582e0ba804dd0 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson <31214002+jaxwilko@users.noreply.github.com> Date: Wed, 21 Sep 2022 17:37:38 +0100 Subject: [PATCH 31/43] Added Str::unique() & Arr::moveKeyToIndex() helpers (#114) --- src/Support/Arr.php | 31 ++++++++++++++++++----- src/Support/Str.php | 33 ++++++++++++++++++++++-- tests/Support/ArrTest.php | 53 +++++++++++++++++++++++++++++++++++++++ tests/Support/StrTest.php | 17 +++++++++++++ 4 files changed, 126 insertions(+), 8 deletions(-) diff --git a/src/Support/Arr.php b/src/Support/Arr.php index ea94efd45..29d05ed71 100644 --- a/src/Support/Arr.php +++ b/src/Support/Arr.php @@ -1,22 +1,19 @@ winter_3 + * winter, [winter_1, winter_3] -> winter + */ + public static function unique(string $str, array $items, string $separator = '_', int $step = 1): string + { + $indexes = []; + + if (!in_array($str, $items)) { + return $str; + } else { + $indexes[] = 0; + } + + foreach ($items as $item) { + if (!preg_match('/(.*?)' . $str . $separator . '(\d*$)/', $item, $matches)) { + continue; + } + + $indexes[] = (int) $matches[2]; + } + + return empty($indexes) + ? $str + : $str . $separator . (max($indexes) + $step); + } } diff --git a/tests/Support/ArrTest.php b/tests/Support/ArrTest.php index b01a37bdb..2acde7bf1 100644 --- a/tests/Support/ArrTest.php +++ b/tests/Support/ArrTest.php @@ -4,6 +4,59 @@ class ArrTest extends TestCase { + public function testMoveKeyToIndex() + { + $array = [ + 'one' => 'a', + 'two' => 'b', + 'three' => 'c', + ]; + + // 0 based index means that index 1 is the second element + $this->assertSame([ + 'one' => 'a', + 'three' => 'c', + 'two' => 'b', + ], Arr::moveKeyToIndex($array, 'three', 1)); + + // 0 index inserts at start + $this->assertSame([ + 'two' => 'b', + 'one' => 'a', + 'three' => 'c', + ], Arr::moveKeyToIndex($array, 'two', 0)); + + // Index out of range inserts at end + $this->assertSame([ + 'one' => 'a', + 'three' => 'c', + 'two' => 'b', + ], Arr::moveKeyToIndex($array, 'two', 10)); + + // Negative index works backwards + $this->assertSame([ + 'two' => 'b', + 'one' => 'a', + 'three' => 'c', + ], Arr::moveKeyToIndex($array, 'two', -2)); + + // Negative index beyond bounds inserting as first element + $this->assertSame([ + 'two' => 'b', + 'one' => 'a', + 'three' => 'c', + ], Arr::moveKeyToIndex($array, 'two', -10)); + + // Elements with null values are correctly able to be sorted + $nullValueArray = $array; + $nullValueArray['two'] = null; + $this->assertSame([ + 'one' => 'a', + 'three' => 'c', + 'two' => null, + ], Arr::moveKeyToIndex($nullValueArray, 'two', 2)); + } + public function testArrClass() { $array = [ diff --git a/tests/Support/StrTest.php b/tests/Support/StrTest.php index eba82d57a..3abd67793 100644 --- a/tests/Support/StrTest.php +++ b/tests/Support/StrTest.php @@ -15,4 +15,21 @@ public function testJoin() $this->assertSame('bob or joe', Str::join(['bob', 'joe'], ', ', ', or ', ' or ')); $this->assertSame('bob; joe; or sally', Str::join(['bob', 'joe', 'sally'], '; ', '; or ')); } + + public function testUnique() + { + // Original returned unmodified when already unique + $this->assertSame('winter_cms', Str::unique('winter_cms', [])); + $this->assertSame('winter_cms', Str::unique('winter_cms', ['winter_cms_1', 'winter_cms_2'])); + + // // String modified to be the default step higher than the highest index identified + $this->assertSame('winter_cms_1', Str::unique('winter_cms', ['winter_cms'])); + $this->assertSame('winter_cms_4', Str::unique('winter_cms', ['winter_cms', 'winter_cms_1', 'test_5', 'winter_cms_3'])); + + // String modified to be the default step higher than the highest index identified with reversed order of items + $this->assertSame('winter_cms_98', Str::unique('winter_cms', ['winter_cms', 'winter_cms_97', 'test_5', 'winter_cms_3'])); + + // String modified to be the provided step higher than the highest index identified with the provided separator + $this->assertSame('winter_cms 5', Str::unique('winter_cms', ['winter_cms', 'winter_cms 1', 'test_5', 'winter_cms 3'], ' ', 2)); + } } From 16f44dc905eed98f1b026f91003c0737ca393a6b Mon Sep 17 00:00:00 2001 From: Jack Wilkinson <31214002+jaxwilko@users.noreply.github.com> Date: Tue, 27 Sep 2022 17:40:39 +0100 Subject: [PATCH 32/43] Added fix to allow pivots to use custom pivot models on attach (#120) --- src/Database/Concerns/HasRelationships.php | 5 +++++ .../Relations/Concerns/BelongsOrMorphsToMany.php | 14 +++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Database/Concerns/HasRelationships.php b/src/Database/Concerns/HasRelationships.php index ecf581b9c..9d57f23df 100644 --- a/src/Database/Concerns/HasRelationships.php +++ b/src/Database/Concerns/HasRelationships.php @@ -326,6 +326,11 @@ protected function handleRelation($relationName) case 'belongsToMany': $relation = $this->validateRelationArgs($relationName, ['table', 'key', 'otherKey', 'parentKey', 'relatedKey', 'pivot', 'timestamps']); $relationObj = $this->$relationType($relation[0], $relation['table'], $relation['key'], $relation['otherKey'], $relation['parentKey'], $relation['relatedKey'], $relationName); + + if (isset($relation['pivotModel'])) { + $relationObj->using($relation['pivotModel']); + } + break; case 'morphTo': diff --git a/src/Database/Relations/Concerns/BelongsOrMorphsToMany.php b/src/Database/Relations/Concerns/BelongsOrMorphsToMany.php index 7e346d3d6..d7dffff39 100644 --- a/src/Database/Relations/Concerns/BelongsOrMorphsToMany.php +++ b/src/Database/Relations/Concerns/BelongsOrMorphsToMany.php @@ -119,9 +119,6 @@ public function create(array $attributes = [], array $pivotData = [], $sessionKe */ public function attach($id, array $attributes = [], $touch = true) { - $insertData = $this->formatAttachRecords($this->parseIds($id), $attributes); - $attachedIdList = array_pluck($insertData, $this->relatedPivotKey); - /** * @event model.relation.beforeAttach * Called before creating a new relation between models (only for BelongsToMany relation) @@ -136,14 +133,21 @@ public function attach($id, array $attributes = [], $touch = true) * }); * */ - if ($this->parent->fireEvent('model.relation.beforeAttach', [$this->relationName, $attachedIdList, $insertData], true) === false) { + if ($this->parent->fireEvent('model.relation.beforeAttach', [$this->relationName, $id, $attributes], true) === false) { return; } + $insertData = $this->formatAttachRecords($this->parseIds($id), $attributes); + $attachedIdList = array_pluck($insertData, $this->relatedPivotKey); + // Here we will insert the attachment records into the pivot table. Once we have // inserted the records, we will touch the relationships if necessary and the // function will return. We can parse the IDs before inserting the records. - $this->newPivotStatement()->insert($insertData); + if ($this->using) { + $this->attachUsingCustomClass($id, $attributes); + } else { + $this->newPivotStatement()->insert($insertData); + } if ($touch) { $this->touchIfTouching(); From 9dea96030b6255d6006252bf3c413658dad4f4d6 Mon Sep 17 00:00:00 2001 From: Damien MATHIEU Date: Wed, 28 Sep 2022 10:25:53 +0200 Subject: [PATCH 33/43] Fix support for queueing emails (#117) See https://github.com/laravel/framework#32187 (comment) --- src/Foundation/Application.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Foundation/Application.php b/src/Foundation/Application.php index 568888f1c..92ee21442 100644 --- a/src/Foundation/Application.php +++ b/src/Foundation/Application.php @@ -470,6 +470,7 @@ public function registerCoreContainerAliases() 'hash' => [\Illuminate\Contracts\Hashing\Hasher::class], 'translator' => [\Illuminate\Translation\Translator::class, \Illuminate\Contracts\Translation\Translator::class], 'log' => [\Illuminate\Log\Logger::class, \Psr\Log\LoggerInterface::class], + 'mail.manager' => [\Illuminate\Mail\MailManager::class, \Illuminate\Contracts\Mail\Factory::class], 'mailer' => [\Illuminate\Mail\Mailer::class, \Illuminate\Contracts\Mail\Mailer::class, \Illuminate\Contracts\Mail\MailQueue::class], 'queue' => [\Illuminate\Queue\QueueManager::class, \Illuminate\Contracts\Queue\Factory::class, \Illuminate\Contracts\Queue\Monitor::class], 'queue.connection' => [\Illuminate\Contracts\Queue\Queue::class], From 623845f446304f3abd2f9d22c20e366659bc5958 Mon Sep 17 00:00:00 2001 From: Romain 'Maz' BILLOIR Date: Wed, 28 Sep 2022 10:39:50 +0200 Subject: [PATCH 34/43] Improve slug generation performance (#118) The `EXISTS` sql statement will return true at the first existing value where the `COUNT` will uselessly continue to loop through the whole table which is inaccurate for this use case. --- src/Database/Traits/Sluggable.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Database/Traits/Sluggable.php b/src/Database/Traits/Sluggable.php index 3d072f924..0a7c35f0b 100644 --- a/src/Database/Traits/Sluggable.php +++ b/src/Database/Traits/Sluggable.php @@ -95,8 +95,8 @@ protected function getSluggableUniqueAttributeValue($name, $value) $separator = $this->getSluggableSeparator(); $_value = $value; while (($this->methodExists('withTrashed') && $this->allowTrashedSlugs) ? - $this->newSluggableQuery()->where($name, $_value)->withTrashed()->count() > 0 : - $this->newSluggableQuery()->where($name, $_value)->count() > 0 + $this->newSluggableQuery()->where($name, $_value)->withTrashed()->exists() : + $this->newSluggableQuery()->where($name, $_value)->exists() ) { $counter++; $_value = $value . $separator . $counter; From a39671fcdec2b3f5a61957ec96c0f7e7b2f6f43c Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Thu, 29 Sep 2022 12:03:04 +0800 Subject: [PATCH 35/43] Fix hidden file glob on systems without GLOB_BRACE support (#121) Fixes wintercms/winter#714 Allows systems without GLOB_BRACE support (ie. Solaris, Alpine Linux) to still correctly include hidden files in a Zip by emulating the brace search using multiple glob passes. --- src/Filesystem/Zip.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Filesystem/Zip.php b/src/Filesystem/Zip.php index d8ca7a62c..835a5e44f 100644 --- a/src/Filesystem/Zip.php +++ b/src/Filesystem/Zip.php @@ -152,7 +152,19 @@ public function add(string $source, array $options = []): self $folders = []; $recursive = false; } else { - $files = glob($source, GLOB_BRACE); + // Workaround for systems that do not support GLOB_BRACE (ie. Solaris, Alpine Linux) + if (!defined('GLOB_BRACE') && $includeHidden) { + $files = array_merge( + glob(dirname($source) . '/*'), + glob(dirname($source) . '/.[!.]*'), + glob(dirname($source) . '/..?*') + ); + } elseif (defined('GLOB_BRACE')) { + $files = glob($source, GLOB_BRACE); + } else { + $files = glob($source); + } + $folders = glob(dirname($source) . '/*', GLOB_ONLYDIR); } From 01a214fb685213546d9f9803e7aa0e008c6fda99 Mon Sep 17 00:00:00 2001 From: Romain 'Maz' BILLOIR Date: Thu, 29 Sep 2022 07:45:53 +0200 Subject: [PATCH 36/43] Readability improvement for Sluggable trait (#119) Co-authored-by: Luke Towers --- src/Database/Traits/Sluggable.php | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Database/Traits/Sluggable.php b/src/Database/Traits/Sluggable.php index 0a7c35f0b..cee887b6a 100644 --- a/src/Database/Traits/Sluggable.php +++ b/src/Database/Traits/Sluggable.php @@ -94,10 +94,7 @@ protected function getSluggableUniqueAttributeValue($name, $value) $counter = 1; $separator = $this->getSluggableSeparator(); $_value = $value; - while (($this->methodExists('withTrashed') && $this->allowTrashedSlugs) ? - $this->newSluggableQuery()->where($name, $_value)->withTrashed()->exists() : - $this->newSluggableQuery()->where($name, $_value)->exists() - ) { + while ($this->newSluggableQuery()->where($name, $_value)->exists()) { $counter++; $_value = $value . $separator . $counter; } @@ -107,13 +104,22 @@ protected function getSluggableUniqueAttributeValue($name, $value) /** * Returns a query that excludes the current record if it exists + * Supports SoftDelete trait * @return Builder */ protected function newSluggableQuery() { - return $this->exists - ? $this->newQuery()->where($this->getKeyName(), '<>', $this->getKey()) - : $this->newQuery(); + $query = $this->newQuery(); + + if ($this->exists) { + $query->where($this->getKeyName(), '<>', $this->getKey()); + } + + if ($this->methodExists('withTrashed') && $this->allowTrashedSlugs) { + $query->withTrashed(); + } + + return $query; } /** From e6e3c5bda3129236cf38ce2b9736df7eeae466cd Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Thu, 29 Sep 2022 14:51:16 -0600 Subject: [PATCH 37/43] Fix BC break in event args for belongsToMany model.relation.afterAttach See https://github.com/wintercms/storm/pull/120#issuecomment-1260806259 --- .../Concerns/BelongsOrMorphsToMany.php | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Database/Relations/Concerns/BelongsOrMorphsToMany.php b/src/Database/Relations/Concerns/BelongsOrMorphsToMany.php index d7dffff39..a48f13c87 100644 --- a/src/Database/Relations/Concerns/BelongsOrMorphsToMany.php +++ b/src/Database/Relations/Concerns/BelongsOrMorphsToMany.php @@ -119,6 +119,17 @@ public function create(array $attributes = [], array $pivotData = [], $sessionKe */ public function attach($id, array $attributes = [], $touch = true) { + $insertData = $this->formatAttachRecords($this->parseIds($id), $attributes); + $attachedIdList = array_pluck($insertData, $this->relatedPivotKey); + + $eventArgs = [$this->relationName]; + + if ($this->using) { + $eventArgs += [$id, $attributes]; + } else { + $eventArgs += [$attachedIdList, $insertData]; + } + /** * @event model.relation.beforeAttach * Called before creating a new relation between models (only for BelongsToMany relation) @@ -132,14 +143,13 @@ public function attach($id, array $attributes = [], $touch = true) * } * }); * + * >**NOTE:** If a custom pivotModel is being used the parameters will actually be `string $relationName, mixed $id, array $attributes` + * */ - if ($this->parent->fireEvent('model.relation.beforeAttach', [$this->relationName, $id, $attributes], true) === false) { + if ($this->parent->fireEvent('model.relation.beforeAttach', $eventArgs, true) === false) { return; } - $insertData = $this->formatAttachRecords($this->parseIds($id), $attributes); - $attachedIdList = array_pluck($insertData, $this->relatedPivotKey); - // Here we will insert the attachment records into the pivot table. Once we have // inserted the records, we will touch the relationships if necessary and the // function will return. We can parse the IDs before inserting the records. @@ -163,8 +173,10 @@ public function attach($id, array $attributes = [], $touch = true) * traceLog("New relation {$relationName} was created", $attachedIdList); * }); * + * >**NOTE:** If a custom pivotModel is being used the parameters will actually be `string $relationName, mixed $id, array $attributes` + * */ - $this->parent->fireEvent('model.relation.afterAttach', [$this->relationName, $attachedIdList, $insertData]); + $this->parent->fireEvent('model.relation.afterAttach', $eventArgs); } /** From e67da1a796dc63dbeb982162a86a0ea1b892c072 Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Tue, 4 Oct 2022 14:56:03 +0800 Subject: [PATCH 38/43] Don't attempt to store array source DBs if storage is disabled --- src/Database/Traits/ArraySource.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Database/Traits/ArraySource.php b/src/Database/Traits/ArraySource.php index 2dc241354..3c2ff5c83 100644 --- a/src/Database/Traits/ArraySource.php +++ b/src/Database/Traits/ArraySource.php @@ -100,11 +100,13 @@ protected static function arraySourceSetDbConnection(string $database): void */ protected function arraySourceCreateDb(): void { - if (File::exists($this->arraySourceGetDbPath())) { - File::delete($this->arraySourceGetDbPath()); + if ($this->arraySourceCanStoreDb()) { + if (File::exists($this->arraySourceGetDbPath())) { + File::delete($this->arraySourceGetDbPath()); + } + // Create SQLite file + File::put($this->arraySourceGetDbPath(), ''); } - // Create SQLite file - File::put($this->arraySourceGetDbPath(), ''); $records = $this->getRecords(); From 57c76693a124b905609001d36eab806ad6aad93a Mon Sep 17 00:00:00 2001 From: Eric Pfeiffer <63664924+ericp-mrel@users.noreply.github.com> Date: Wed, 5 Oct 2022 11:28:44 -0500 Subject: [PATCH 39/43] Merge eventArgs using spread operator instead of array union `+` operator. (#122) This fixes the issue with the arrays not being merged properly with auto-incremented keys. See https://github.com/wintercms/storm/pull/120#issuecomment-1267072421 --- src/Database/Relations/Concerns/BelongsOrMorphsToMany.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Database/Relations/Concerns/BelongsOrMorphsToMany.php b/src/Database/Relations/Concerns/BelongsOrMorphsToMany.php index a48f13c87..7b92bf0bf 100644 --- a/src/Database/Relations/Concerns/BelongsOrMorphsToMany.php +++ b/src/Database/Relations/Concerns/BelongsOrMorphsToMany.php @@ -125,9 +125,9 @@ public function attach($id, array $attributes = [], $touch = true) $eventArgs = [$this->relationName]; if ($this->using) { - $eventArgs += [$id, $attributes]; + $eventArgs = [...$eventArgs, $id, $attributes]; } else { - $eventArgs += [$attachedIdList, $insertData]; + $eventArgs = [...$eventArgs, $attachedIdList, $insertData]; } /** From d7124238a4717fcbb3f5d9b9c387e86899658c8a Mon Sep 17 00:00:00 2001 From: Jack Wilkinson <31214002+jaxwilko@users.noreply.github.com> Date: Sun, 9 Oct 2022 23:03:39 +0100 Subject: [PATCH 40/43] Support source root level paths in Halcyon datasources (#123) Supporting PR for wintercms/winter#726 --- src/Halcyon/Datasource/DbDatasource.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Halcyon/Datasource/DbDatasource.php b/src/Halcyon/Datasource/DbDatasource.php index a91de342f..5d83c4b04 100644 --- a/src/Halcyon/Datasource/DbDatasource.php +++ b/src/Halcyon/Datasource/DbDatasource.php @@ -95,7 +95,7 @@ public function getQuery(bool $ignoreDeleted = true): \Winter\Storm\Database\Que */ protected function makeFilePath(string $dirName, string $fileName, string $extension): string { - return $dirName . '/' . $fileName . '.' . $extension; + return ltrim($dirName . '/' . $fileName . '.' . $extension, '/'); } /** From 6cc669a1bc3bdb83b6081036a0a2086db2e5fc25 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson <31214002+jaxwilko@users.noreply.github.com> Date: Thu, 13 Oct 2022 01:06:04 +0100 Subject: [PATCH 41/43] Fixed bug when dynamically adding hasMany* relations (#124) --- src/Database/Concerns/HasRelationships.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Database/Concerns/HasRelationships.php b/src/Database/Concerns/HasRelationships.php index 9d57f23df..c79e63e1e 100644 --- a/src/Database/Concerns/HasRelationships.php +++ b/src/Database/Concerns/HasRelationships.php @@ -949,7 +949,7 @@ public function addAttachManyRelation(string $name, array $config): void */ public function addHasOneThroughRelation(string $name, array $config): void { - $this->addRelation('HasOneThrough', $name, $config); + $this->addRelation('hasOneThrough', $name, $config); } /** @@ -959,7 +959,7 @@ public function addHasOneThroughRelation(string $name, array $config): void */ public function addHasManyThroughRelation(string $name, array $config): void { - $this->addRelation('HasManyThrough', $name, $config); + $this->addRelation('hasManyThrough', $name, $config); } /** From 302593d340eb928bb78df770c8ecb54bb4d4bfd7 Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Tue, 18 Oct 2022 04:22:44 +0800 Subject: [PATCH 42/43] Reverse order in checking mail config styles (#107) This reverses the order for checking mail configs by searching for the new format first, then checking for the old format. Laravel checks old format first, then new format, which works fine for them, but doesn't work for us as we also populate settings via the Backend, which populates the settings in the new format. Fixes https://github.com/wintercms/winter/issues/626 --- src/Mail/MailManager.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/Mail/MailManager.php b/src/Mail/MailManager.php index ade48d425..45ee036d9 100644 --- a/src/Mail/MailManager.php +++ b/src/Mail/MailManager.php @@ -7,6 +7,7 @@ * Overrides the Laravel MailManager * - Replaces the Laravel Mailer class with the Winter Mailer class * - Fires mailer.beforeRegister & mailer.register events + * - Uses another method to determine old vs. new mail configs */ class MailManager extends BaseMailManager { @@ -71,4 +72,29 @@ protected function resolve($name) return $mailer; } + + /** + * @inheritDoc + */ + protected function getConfig(string $name) + { + // Here we will check if the "mailers" key exists and if it does, we will use that to + // determine the applicable config. Laravel checks if the "drivers" key exists, and while + // that does work for Laravel, it doesn't work in Winter when someone uses the Backend to + // populate mail settings, as these mail settings are populated into the "mailers" key. + return $this->app['config']['mail.mailers'] + ? ($this->app['config']["mail.mailers.{$name}"] ?? $this->app['config']['mail']) + : $this->app['config']['mail']; + } + + /** + * @inheritDoc + */ + public function getDefaultDriver() + { + // We will do the reverse of what Laravel does and check for "default" first, which is + // populated by the Backend or the new "mail" config, before searching for the "driver" + // key that was present in older version of Winter (<1.2). + return $this->app['config']['mail.default'] ?? $this->app['config']['mail.driver']; + } } From 6d2d55288f19eb57938a5619fd77b2f0668998f8 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson <31214002+jaxwilko@users.noreply.github.com> Date: Tue, 18 Oct 2022 15:58:12 +0100 Subject: [PATCH 43/43] Patch alert method to keep compatibility with Laravel (#125) --- src/Console/Command.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Console/Command.php b/src/Console/Command.php index 66f3e84a9..038de5b8a 100644 --- a/src/Console/Command.php +++ b/src/Console/Command.php @@ -35,9 +35,10 @@ public function __construct() * Write a string in an alert box. * * @param string $string + * @param int|string|null $verbosity * @return void */ - public function alert($string) + public function alert($string, $verbosity = null) { $maxLength = 80; $padding = 5; @@ -60,7 +61,7 @@ public function alert($string) $width = $innerLineWidth + ($border * 2); // Top border - $this->comment(str_repeat('*', $width)); + $this->comment(str_repeat('*', $width), $verbosity); // Alert content foreach ($lines as $line) { @@ -68,12 +69,13 @@ public function alert($string) $this->comment( str_repeat('*', $border) . str_pad($line, $innerLineWidth, ' ', STR_PAD_BOTH) - . str_repeat('*', $border) + . str_repeat('*', $border), + $verbosity ); } // Bottom border - $this->comment(str_repeat('*', $width)); + $this->comment(str_repeat('*', $width), $verbosity); $this->newLine(); }