Skip to content

Commit 48cdfbc

Browse files
committed
Merge branch 'release/4.5.6' into main
2 parents 906e011 + 6320ac2 commit 48cdfbc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+556
-206
lines changed

CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,36 @@
11
# Release Notes for Craft CMS 4
22

3+
## 4.5.6 - 2023-09-26
4+
5+
- When slideouts are opened within Live Preview, they now slide up over the editor pane, rather than covering the preview pane. ([#13739](https://github.com/craftcms/cms/pull/13739))
6+
- Cross-site validation now only involves fields which were actually modified in the element save. ([#13675](https://github.com/craftcms/cms/discussions/13675))
7+
- Row headings within Table fields now get statically translated. ([#13703](https://github.com/craftcms/cms/discussions/13703))
8+
- Element condition settings within field layout components now display a warning if the `autosaveDrafts` config setting is disabled. ([#12348](https://github.com/craftcms/cms/issues/12348))
9+
- Added the `resave/addresses` command. ([#13720](https://github.com/craftcms/cms/discussions/13720))
10+
- The `resave/matrix-blocks` command now supports an `--owner-id` option.
11+
- Added `craft\helpers\App::phpExecutable()`.
12+
- Added `craft\helpers\Component::cleanseConfig()`.
13+
- `craft\helpers\Component::createComponent()` now filters out `as X` and `on X` keys from the component config.
14+
- `craft\services\Announcements::push()` now has an `$adminsOnly` argument. ([#13728](https://github.com/craftcms/cms/discussions/13728))
15+
- `Craft.appendHeadHtml()` and `appendBodyHtml()` now load external scripts asynchronously, and return promises.
16+
- Improved the reliability of Composer operations when PHP is running via FastCGI. ([#13681](https://github.com/craftcms/cms/issues/13681))
17+
- Fixed a bug where it wasn’t always possible to create new entries from custom sources which were limited to one section.
18+
- Fixed a bug where relational fields weren’t factoring in cross-site elements when enforcing their “Min Relations”, “Max Relations”, and “Validate related entries” settings. ([#13699](https://github.com/craftcms/cms/issues/13699))
19+
- Fixed a bug where pagination wasn’t working for admin tables, if the `onQueryParams` callback method wasn’t set. ([#13677](https://github.com/craftcms/cms/issues/13677))
20+
- Fixed a bug where relations within Matrix blocks weren’t getting restored when restoring a revision’s content. ([#13626](https://github.com/craftcms/cms/issues/13626))
21+
- Fixed a bug where the filesystem and volume-creation slideouts could keep reappearing if canceled. ([#13707](https://github.com/craftcms/cms/issues/13707))
22+
- Fixed an error that could occur when reattempting to update to Craft 4.5. ([#13714](https://github.com/craftcms/cms/issues/13714))
23+
- Fixed a bug where date and time inputs could be parsed incorrectly, if the user’s formatting locale wasn’t explicitly set, or it changed between page load and form submit. ([#13731](https://github.com/craftcms/cms/issues/13731))
24+
- Fixed JavaScript errors that could occur when control panel resources were being loaded from a different domain. ([#13715](https://github.com/craftcms/cms/issues/13715))
25+
- Fixed a PHP error that occurred if the `CRAFT_DOTENV_PATH` environment variable was set, or a console command was executed with the `--dotenvPath` option. ([#13725](https://github.com/craftcms/cms/issues/13725))
26+
- Fixed a bug where long element titles weren’t always getting truncated in the control panel. ([#13718](https://github.com/craftcms/cms/issues/13718))
27+
- Fixed a bug where checkboxes could be preselected if they had an empty value. ([#13710](https://github.com/craftcms/cms/issues/13710))
28+
- Fixed a bug where links in validation summaries weren’t working if the offending field was in a collapsed Matrix block. ([#13708](https://github.com/craftcms/cms/issues/13708))
29+
- Fixed a bug where cross-site validation could apply even if `craft\services\Elements::saveElement()` was called with `$runValidation` set to `false`.
30+
- Fixed some wonky scrolling behavior on pages where the details pane was shorter than the content pane. ([#13637](https://github.com/craftcms/cms/issues/13637))
31+
- Fixed a division by zero error. ([#13712](https://github.com/craftcms/cms/issues/13712))
32+
- Fixed an RCE vulnerability.
33+
334
## 4.5.5 - 2023-09-14
435

536
- Added the `maxGraphqlBatchSize` config setting. ([#13693](https://github.com/craftcms/cms/issues/13693))

bootstrap/bootstrap.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,14 @@
4141
}
4242
};
4343

44-
$findConfigPath = function($cliName, $envName) use ($createFolder) {
44+
$findConfigPath = function(string $cliName, string $envName, bool $isFile = false) use ($createFolder) {
4545
$path = App::cliOption($cliName, true) ?? App::env($envName);
4646
if (!$path) {
4747
return null;
4848
}
49-
$createFolder($path);
49+
if (!$isFile) {
50+
$createFolder($path);
51+
}
5052
return realpath($path);
5153
};
5254

@@ -76,8 +78,8 @@
7678
// Set the "project root" path that contains config/, storage/, etc. By default assume that it's up a level from vendor/.
7779
$rootPath = $findConfigPath('--basePath', 'CRAFT_BASE_PATH') ?? dirname($vendorPath);
7880

79-
// By default the remaining directories will be in the base directory
80-
$dotenvPath = $findConfigPath('--dotenvPath', 'CRAFT_DOTENV_PATH') ?? "$rootPath/.env";
81+
// By default the remaining files/directories will be in the base directory
82+
$dotenvPath = $findConfigPath('--dotenvPath', 'CRAFT_DOTENV_PATH', true) ?? "$rootPath/.env";
8183
$configPath = $findConfigPath('--configPath', 'CRAFT_CONFIG_PATH') ?? "$rootPath/config";
8284
$contentMigrationsPath = $findConfigPath('--contentMigrationsPath', 'CRAFT_CONTENT_MIGRATIONS_PATH') ?? "$rootPath/migrations";
8385
$storagePath = $findConfigPath('--storagePath', 'CRAFT_STORAGE_PATH') ?? "$rootPath/storage";

packages/craftcms-vue/admintable/App.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,9 @@
610610
};
611611
612612
if (this.onQueryParams instanceof Function) {
613-
params = this.onQueryParams(params);
613+
let callbackParams = this.onQueryParams(params);
614+
// if `callbackParams` is not undefined, use them instead of `params`
615+
params = callbackParams || params;
614616
}
615617
616618
return params;

src/base/FieldLayoutComponent.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,12 @@ public function getSettingsHtml(): string
261261
'instructions' => Craft::t('app', 'Only show when editing {type} that match the following rules:', [
262262
'type' => $elementType::pluralLowerDisplayName(),
263263
]),
264+
'warning' => !Craft::$app->getConfig()->getGeneral()->autosaveDrafts
265+
? sprintf(
266+
'`autosaveDrafts` must be enabled for this condition to take effect automatically when editing %s.',
267+
$elementType::pluralLowerDisplayName(),
268+
)
269+
: null,
264270
]);
265271
}
266272
}

src/config/app.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
return [
44
'id' => 'CraftCMS',
55
'name' => 'Craft CMS',
6-
'version' => '4.5.5',
6+
'version' => '4.5.6',
77
'schemaVersion' => '4.5.3.0',
88
'minVersionRequired' => '3.7.11',
99
'basePath' => dirname(__DIR__), // Defines the @app alias

src/console/controllers/ResaveController.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Craft;
1111
use craft\base\ElementInterface;
1212
use craft\console\Controller;
13+
use craft\elements\Address;
1314
use craft\elements\Asset;
1415
use craft\elements\Category;
1516
use craft\elements\db\ElementQuery;
@@ -184,6 +185,18 @@ final public static function normalizeTo(?string $to): callable
184185
*/
185186
public ?string $field = null;
186187

188+
/**
189+
* @var string|int[]|null Comma-separated list of owner element IDs.
190+
* @since 4.5.6
191+
*/
192+
public string|array|null $ownerId = null;
193+
194+
/**
195+
* @var string|null Comma-separated list of country codes.
196+
* @since 4.5.6
197+
*/
198+
public ?string $countryCode = null;
199+
187200
/**
188201
* @var string|null An attribute name that should be set for each of the elements. The value will be determined by --to.
189202
* @since 3.7.29
@@ -233,6 +246,10 @@ public function options($actionID): array
233246
$options[] = 'touch';
234247

235248
switch ($actionID) {
249+
case 'addresses':
250+
$options[] = 'ownerId';
251+
$options[] = 'countryCode';
252+
break;
236253
case 'assets':
237254
$options[] = 'volume';
238255
break;
@@ -252,6 +269,7 @@ public function options($actionID): array
252269
break;
253270
case 'matrix-blocks':
254271
$options[] = 'field';
272+
$options[] = 'ownerId';
255273
$options[] = 'type';
256274
break;
257275
}
@@ -299,6 +317,24 @@ public function beforeAction($action): bool
299317
return true;
300318
}
301319

320+
/**
321+
* Re-saves user addresses.
322+
*
323+
* @return int
324+
* @since 4.5.6
325+
*/
326+
public function actionAddresses(): int
327+
{
328+
$criteria = [];
329+
if (isset($this->ownerId)) {
330+
$criteria['ownerId'] = array_map(fn(string $id) => (int)$id, explode(',', (string)$this->ownerId));
331+
}
332+
if (isset($this->countryCode)) {
333+
$criteria['countryCode'] = explode(',', (string)$this->countryCode);
334+
}
335+
return $this->resaveElements(Address::class, $criteria);
336+
}
337+
302338
/**
303339
* Re-saves assets.
304340
*
@@ -358,6 +394,9 @@ public function actionMatrixBlocks(): int
358394
if (isset($this->field)) {
359395
$criteria['field'] = explode(',', $this->field);
360396
}
397+
if (isset($this->ownerId)) {
398+
$criteria['ownerId'] = array_map(fn(string $id) => (int)$id, explode(',', (string)$this->ownerId));
399+
}
361400
if (isset($this->type)) {
362401
$criteria['type'] = explode(',', $this->type);
363402
}

src/console/controllers/SetupController.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
use Symfony\Component\Console\Helper\QuestionHelper;
3030
use Symfony\Component\Console\Input\StringInput;
3131
use Symfony\Component\Console\Output\StreamOutput;
32-
use Symfony\Component\Process\PhpExecutableFinder;
3332
use Symfony\Component\Process\Process;
3433
use Throwable;
3534
use yii\base\InvalidConfigException;
@@ -627,7 +626,7 @@ public function actionCloud(): int
627626
}
628627

629628
$process = new Process([
630-
(new PhpExecutableFinder())->find() ?: 'php',
629+
App::phpExecutable() ?? 'php',
631630
$script,
632631
'cloud/setup',
633632
]);

src/console/controllers/UpdateController.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
use craft\models\Updates;
2222
use craft\models\Updates as UpdatesModel;
2323
use Symfony\Component\Process\Exception\ProcessFailedException;
24-
use Symfony\Component\Process\PhpExecutableFinder;
2524
use Symfony\Component\Process\Process;
2625
use Throwable;
2726
use yii\base\InvalidConfigException;
@@ -416,7 +415,7 @@ private function _migrate(): bool
416415

417416
$this->stdout('Applying new migrations ... ', Console::FG_YELLOW);
418417

419-
$php = (new PhpExecutableFinder())->find() ?: 'php';
418+
$php = App::phpExecutable() ?? 'php';
420419
$process = new Process([$php, $script, 'migrate/all', '--no-backup', '--no-content']);
421420
$process->setTimeout(null);
422421
try {
@@ -480,7 +479,7 @@ private function _revertComposerChanges(): void
480479

481480
$this->stdout('Reverting Composer changes ... ', Console::FG_YELLOW);
482481

483-
$php = (new PhpExecutableFinder())->find() ?: 'php';
482+
$php = App::phpExecutable() ?? 'php';
484483
$process = new Process([$php, $script, 'update/composer-install']);
485484
$process->setTimeout(null);
486485
try {

src/fields/BaseOptionsField.php

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -345,19 +345,7 @@ public function normalizeValue(mixed $value, ?ElementInterface $element = null):
345345
$optionLabels = [];
346346
foreach ($this->options() as $option) {
347347
if (!isset($option['optgroup'])) {
348-
// special case for blank options, when $value is null
349-
if ($value === null && $option['value'] === '') {
350-
if (!$selectedBlankOption) {
351-
$selectedValues[] = '';
352-
$selectedBlankOption = true;
353-
$selected = true;
354-
} else {
355-
$selected = false;
356-
}
357-
} else {
358-
$selected = in_array($option['value'], $selectedValues, true);
359-
}
360-
348+
$selected = $this->isOptionSelected($option, $value, $selectedValues, $selectedBlankOption);
361349
$options[] = new OptionData($option['label'], $option['value'], $selected, true);
362350
$optionValues[] = (string)$option['value'];
363351
$optionLabels[] = (string)$option['label'];
@@ -390,6 +378,20 @@ public function normalizeValue(mixed $value, ?ElementInterface $element = null):
390378
return $value;
391379
}
392380

381+
/**
382+
* Check if given option should be marked as selected.
383+
*
384+
* @param array $option
385+
* @param mixed $value
386+
* @param array $selectedValues
387+
* @param bool $selectedBlankOption
388+
* @return bool
389+
*/
390+
protected function isOptionSelected(array $option, mixed $value, array &$selectedValues, bool &$selectedBlankOption): bool
391+
{
392+
return in_array($option['value'], $selectedValues, true);
393+
}
394+
393395
/**
394396
* @inheritdoc
395397
*/

src/fields/BaseRelationField.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,10 @@ public function validateRelationCount(ElementInterface $element): void
439439
/** @var ElementQueryInterface|Collection $value */
440440
$value = $element->getFieldValue($this->handle);
441441

442+
if ($value instanceof ElementQueryInterface) {
443+
$value = $this->_all($value);
444+
}
445+
442446
$arrayValidator = new NumberValidator([
443447
'min' => $this->minRelations,
444448
'max' => $this->maxRelations,
@@ -473,6 +477,11 @@ public function validateRelatedElements(ElementInterface $element): void
473477

474478
/** @var ElementQueryInterface|Collection $value */
475479
$value = $element->getFieldValue($this->handle);
480+
481+
if ($value instanceof ElementQueryInterface) {
482+
$value = $this->_all($value);
483+
}
484+
476485
$errorCount = 0;
477486

478487
foreach ($value->all() as $i => $related) {

src/fields/Dropdown.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,23 @@ protected function optionsSettingLabel(): string
113113
{
114114
return Craft::t('app', 'Dropdown Options');
115115
}
116+
117+
/**
118+
* @inheritdoc
119+
*/
120+
protected function isOptionSelected(array $option, mixed $value, array &$selectedValues, bool &$selectedBlankOption): bool
121+
{
122+
// special case for blank options, when $value is null
123+
if ($value === null && $option['value'] === '') {
124+
if (!$selectedBlankOption) {
125+
$selectedValues[] = '';
126+
$selectedBlankOption = true;
127+
return true;
128+
}
129+
130+
return false;
131+
}
132+
133+
return in_array($option['value'], $selectedValues, true);
134+
}
116135
}

src/fields/Table.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,15 @@ private function _normalizeValueInternal(mixed $value, ?ElementInterface $elemen
440440

441441
$defaults = $this->defaults ?? [];
442442

443+
// Apply static translations
444+
foreach ($defaults as &$row) {
445+
foreach ($this->columns as $colId => $col) {
446+
if ($col['type'] === 'heading' && isset($row[$colId])) {
447+
$row[$colId] = Craft::t('site', $row[$colId]);
448+
}
449+
}
450+
}
451+
443452
if (is_string($value) && !empty($value)) {
444453
$value = Json::decodeIfJson($value);
445454
} elseif ($value === null && $this->isFresh($element)) {

src/helpers/App.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use HTMLPurifier_Encoder;
3535
use ReflectionClass;
3636
use ReflectionProperty;
37+
use Symfony\Component\Process\PhpExecutableFinder;
3738
use yii\base\Event;
3839
use yii\base\Exception;
3940
use yii\base\InvalidArgumentException;
@@ -604,6 +605,35 @@ public static function isPathAllowed(string $path): bool
604605
return false;
605606
}
606607

608+
/**
609+
* Returns the path to a PHP executable which should be used by sub processes.
610+
*
611+
* @return string|null The PHP executable path, or `null` if it can’t be determined.
612+
* @since 4.5.6
613+
*/
614+
public static function phpExecutable(): ?string
615+
{
616+
// If PHP_BINARY was set to $_SERVER, update the environment variable to match
617+
if (isset($_SERVER['PHP_BINARY']) && $_SERVER['PHP_BINARY'] !== getenv('PHP_BINARY')) {
618+
putenv(sprintf('PHP_BINARY=%s', $_SERVER['PHP_BINARY']));
619+
}
620+
621+
if (
622+
getenv('PHP_BINARY') === false &&
623+
PHP_BINARY &&
624+
PHP_SAPI === 'cgi-fcgi' &&
625+
str_ends_with(PHP_BINARY, 'php-cgi')
626+
) {
627+
// See if a `php` file exists alongside `php-cgi`, and if so, use that
628+
$file = dirname(PHP_BINARY) . DIRECTORY_SEPARATOR . 'php';
629+
if (@is_executable($file) && !@is_dir($file)) {
630+
return $file;
631+
}
632+
}
633+
634+
return (new PhpExecutableFinder())->find() ?: null;
635+
}
636+
607637
/**
608638
* Tests whether ini_set() works.
609639
*

src/helpers/Component.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public static function createComponent(string|array $config, ?string $instanceOf
140140

141141
// Instantiate and return
142142
$config['class'] = $class;
143-
return Craft::createObject($config);
143+
return Craft::createObject(static::cleanseConfig($config));
144144
}
145145

146146
/**

0 commit comments

Comments
 (0)