diff --git a/CHANGELOG-v3.md b/CHANGELOG-v3.md index a72acecbaf9..a7c52285328 100644 --- a/CHANGELOG-v3.md +++ b/CHANGELOG-v3.md @@ -3,13 +3,69 @@ Craft CMS 3.0 Working Changelog ## Unreleased +### Added +- Added `craft\helpers\StringHelper::split()`. + +### Changed +- Plugin translation sources are now configured with `forceTranslations` enabled. +- Checkbox and radio groups now have `div.checkbox-group` and `div.radio-group` container elements. ([#1965](https://github.com/craftcms/cms/pull/1965)) +- Performance improvement to `craft\services\Images::getSupportImageFormats()` if you have Imagick installed. ([#1969](https://github.com/craftcms/cms/pull/1969)) +- Performance improvement to `craft\helpers\Image::canManipulateAsImage()`. ([#1969](https://github.com/craftcms/cms/pull/1969)) +- Queue info requests in the Control Panel no longer extend the user session. + +### Deprecated +- Splitting a string on commas via `craft\helpers\ArrayHelper::toArray()` is now deprecated. Use `craft\helpers\StringHelper::split()` instead. + +### Fixed +- Fixed a SQL error that could occur when using the `relatedTo` element query param. ([#1939](https://github.com/craftcms/cms/issues/1939)) +- Fixed a bug where the “Parent” field was showing up on Edit Entry pages in Structure sections that were limited to a single level. ([#1940](https://github.com/craftcms/cms/pull/1940)) +- Fixed a bug where the “Delete Stale Template Cache” task would fail when saving an existing Single section. +- Fixed a bug where it was not possible to use `:notempty:` with PostgreSQL. +- Fixed a bug where the `job` column in the `queue` table wasn’t large enough to store some job data. ([#1948](https://github.com/craftcms/cms/issues/1948)) +- Fixed a JS error that occurred on CP templates that extended `_layouts/cp` but overrode the `body` block to remove the sidebar, if there were any running/waiting jobs in the queue. ([#1950](https://github.com/craftcms/cms/issues/1950)) +- Updated some outdated references to `runPendingTasks()` in the CP JavaScript files, which resulted in JS errors. ([#1951](https://github.com/craftcms/cms/issues/1951)) +- Fixed a bug where the “Translation Method” setting was visible for existing Matrix sub-fields even when there was only one available translation method (e.g. Entries fields). ([#1967](https://github.com/craftcms/cms/issues/1967)) +- Fixed a bug where Craft would get confused whether or not it could manipulate SVG files. ([#1874](https://github.com/craftcms/cms/issues/1874)) +- Fixed a CSRF validation error that would occur when attempting to re-login via the login modal in the Control Panel. ([#1957](https://github.com/craftcms/cms/issues/1957)) +- Fixed a “divide by zero” error when paginating an empty set of elements. ([#1970](https://github.com/craftcms/cms/pull/1970)) + +## 3.0.0-beta.26 - 2017-08-22 + +### Fixed +- Fixed a SQL error that could occur when logging deprecation errors if the line number is unknown. ([#1927](https://github.com/craftcms/cms/issues/1927)) +- Fixed a deprecation error that occurred when using a Tags field. ([#1932](https://github.com/craftcms/cms/issues/1932)) +- Fixed a PHP error that occurred if a volume type class was missing. ([#1934](https://github.com/craftcms/cms/issues/1934)) +- Fixed a PHP error that occurred when saving a new entry in a section with versioning enabled. ([#1935](https://github.com/craftcms/cms/issues/1935)) + +## 3.0.0-beta.25 - 2017-08-17 + +### Added +- Added `craft\elements\Entry::$revisionCreatorId`. +- Added `craft\services\EntryRevisions::doesEntryHaveVersions()`. + +### Changed +- Craft now retroactively creates entry versions when saving entries, for entries that didn’t have any version history yet. ([#1926](https://github.com/craftcms/cms/issues/1926)) + +### Fixed +- Fixed an error that occurred when chaining a custom field method and a normal query method (e.g. `all()`) together on an element query. ([#1887](https://github.com/craftcms/cms/issues/1887)) +- Fixed a PHP error that occurred when processing a user verification request. +- Fixed a bug where newly-created `div.matrixblock` elements weren’t getting a `data-type` attribute like existing blocks had. ([#1925](https://github.com/craftcms/cms/pull/1925)) +- Fixed a bug where you would get a SQL error if you tried to push a job to the queue using PostgreSQL. +- Fixed a bug that would trigger the browser’s JavaScript debugger if you saved a Matrix field that had collapsed blocks. +- Fixed a bug where `craft\helpers\ChartHelper::getRunChartDataFromQuery()` an SQL query instead of the actual results. +- Fixed a bug where `craft\controllers\BaseElementsController::context()` was expecting a`string ` return, while return could also be `null`. + +## 3.0.0-beta.24 - 2017-08-15 + ### Added - Craft’s tasks implementation has been replaced with a queue, based on the [Yii 2 Queue Extension](https://github.com/yiisoft/yii2-queue). ([#1910](https://github.com/craftcms/cms/issues/1910)) - The “Failed” message in the queue HUD in the Control Panel now shows the full error message as alt text. ([#855](https://github.com/craftcms/cms/issues/855)) +- It’s now possible to install Craft from the command line, using the new `install` command. ([#1917](https://github.com/craftcms/cms/pull/1917)) - Added the `instance of()` Twig test. - Added `craft\base\FlysystemVolume`, which replaces `craft\base\Volume` as the new base class for Flysystem-based volumes. - Added `craft\behaviors\SessionBehavior`, making it possible for `config/app.php` to customize the base `session` component while retaining Craft’s custom session methods. - Added `craft\controllers\QueueController`. +- Added `craft\events\UserEvent`. - Added `craft\queue\BaseJob`, a base class for queue jobs that adds support for descriptions and progress. - Added `craft\queue\Command`, which provides `queue/run`, `queue/listen`, and `queue/info` console commands. - Added `craft\queue\InfoAction`. @@ -27,14 +83,15 @@ Craft CMS 3.0 Working Changelog ### Changed - Renamed the `runTasksAutomatically` config setting to `runQueueAutomatically`. - Logs that occur during `queue` requests now get saved in `storage/logs/queue.log`. -- The updater now ensures it can find `composer.json` before putting the system in Maintenance Mode, reducing the liklihood that Craft will mistakingly think that it’s already mid-update later on. ([#1883](https://github.com/craftcms/cms/issues/1883)) -- The updater now ensures that the `COMPOSER_HOME`, `HOME` (\*nix), or `APPDATA` (Windows) environment variable is set before putting the system in Maintenance Mode, reducing the liklihood that Craft will mistakingly think that it’s already mid-update later on. ([#1890](https://github.com/craftcms/cms/issues/1890#issuecomment-319715460)) +- The updater now ensures it can find `composer.json` before putting the system in Maintenance Mode, reducing the likelihood that Craft will mistakingly think that it’s already mid-update later on. ([#1883](https://github.com/craftcms/cms/issues/1883)) +- The updater now ensures that the `COMPOSER_HOME`, `HOME` (\*nix), or `APPDATA` (Windows) environment variable is set before putting the system in Maintenance Mode, reducing the likelihood that Craft will mistakingly think that it’s already mid-update later on. ([#1890](https://github.com/craftcms/cms/issues/1890#issuecomment-319715460)) - `craft\mail\Mailer::send()` now processes Twig code in the email message before parsing it as Markdown, if the message was composed via `craft\mail\Mailer::composeFromKey()`. ([#1895](https://github.com/craftcms/cms/pull/1895)) - `craft\mail\Mailer::send()` no longer catches exceptions thrown by its parent method, or fires a `sendMailFailure` event in the event of a send failure. ([#1896](https://github.com/craftcms/cms/issues/1896)) - Renamed `craft\helpers\Component::applySettings()` to `mergeSettings()`, and it no longer takes the `$config` argument by reference, instead returning a new array. - Renamed `craft\web\twig\nodes\GetAttr` to `GetAttrNode`. - `craft\base\Volume` is now only focussed on things that every volume would need, regardless of whether it will use Flysystem under the hood. - `craft\base\VolumeInterface::createFileByStream()`, `updateFileByStream()`, `deleteFile()`, `renameFile()`, `copyFile()`, `createDir()`, `deleteDir()`, and `renameDir()` no longer require their implementation methods to return a boolean value. +- `div.matrixblock` elements in the Control Panel now have a `data-type` attribute set to the Matrix block type’s handle. ([#1915](https://github.com/craftcms/cms/pull/1915)) ### Deprecated - Looping through element queries directly is now deprecated. Use the `all()` function to fetch the query results before looping over them. ([#1902](https://github.com/craftcms/cms/issues/1902)) @@ -44,9 +101,15 @@ Craft CMS 3.0 Working Changelog - Removed `craft\base\TaskInterface`. - Removed `craft\base\TaskTrait`. - Removed `craft\controllers\TasksController`. +- Removed `craft\controllers\VolumesController::actionLoadVolumeTypeData()`. - Removed `craft\db\TaskQuery`. - Removed `craft\events\MailFailureEvent`. - Removed `craft\events\TaskEvent`. +- Removed `craft\events\UserActivateEvent`. Use `craft\events\UserEvent` instead. +- Removed `craft\events\UserSuspendEvent`. Use `craft\events\UserEvent` instead. +- Removed `craft\events\UserTokenEvent`. Use `craft\events\UserEvent` instead. +- Removed `craft\events\UserUnlockEvent`. Use `craft\events\UserEvent` instead. +- Removed `craft\events\UserUnsuspendEvent`. Use `craft\events\UserEvent` instead. - Removed `craft\mail\Mailer::EVENT_SEND_MAIL_FAILURE`. - Removed `craft\records\Task`. - Removed `craft\services\Tasks`. @@ -68,6 +131,8 @@ Craft CMS 3.0 Working Changelog - Fixed a 404 error for `bootstrap.js` that occurred when the Debug Toolbar was opened. - Fixed some deprecation errors caused by relational and Matrix field inputs. - Fixed a bug where a plugin would get a PHP error if it tried to get the current site in the middle of a Craft update. +- Fixed a bug where the Migrations utility would display an error message even if the migrations were applied successfully. ([#1911](https://github.com/craftcms/cms/issues/1911)) +- Fixed a PHP error that occurred if calling `getMimeType()` on an asset with an extension with an unknown MIME type. ([#1919](https://github.com/craftcms/cms/pull/1919)) ## 3.0.0-beta.23 - 2017-07-28 diff --git a/CHANGELOG.md b/CHANGELOG.md index 843d8e1ca4f..9b1a3873f2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,29 @@ Craft CMS Changelog ## Unreleased +### Changed +- Added support for the `application/font-woff2` MIME type (`.woff2`). ([#1966](https://github.com/craftcms/cms/issues/1966)) +- `div.matrixblock` elements in the Control Panel now have a `data-type` attribute set to the Matrix block type’s handle. ([#1915](https://github.com/craftcms/cms/pull/1915)) + +### Fixed +- Fixed a bug where you could get a PHP error uploading some JPG files on PHP 7.1. +- Fixed a bug where user photos and site logos/icons were not taking into account the [sanitizeSvgUploads](https://craftcms.com/docs/config-settings#sanitizeSvgUploads) config setting. +- Fixed a CSRF validation error that would occur when attempting to re-login via the login modal in the Control Panel. + +### Security +- Fixed an XSS vulnerability. + +## 2.6.2989 2017-08-15 + +### Added +- Added the [onLockUser](https://craftcms.com/docs/plugins/events-reference#users-onLockUser) event, which fires when a user account is locked. + ### Fixed - Fixed a bug where the PHP and DB versions the Craft Support widget passed to GitHub would not escape tildes (`~`), potentially having Markdown confuse them for strikethrough markup delimiters. - Fixed a bug where it was possible for users to be redirected to a 404 in the Control Panel after logging in. ([#1901](https://github.com/craftcms/cms/issues/1901)) +- Fixed a bug where users would get one extra login attempt than the [maxInvalidLogins](https://craftcms.com/docs/config-settings#maxInvalidLogins) config setting was set to. -## 2.6.2988 2017-07-28 +## 2.6.2988 - 2017-07-28 ### Changed - Added `.m2t` to the default [allowedFileExtensions](https://craftcms.com/docs/config-settings#allowedFileExtensions) config setting value. diff --git a/composer.json b/composer.json index cd1b7e944a7..6f34d63d96c 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "craftcms/cms", "description": "Craft CMS", - "version": "3.0.0-beta.23", + "version": "3.0.0-beta.26", "keywords": [ "cms", "craftcms", @@ -28,6 +28,7 @@ "league/flysystem": "~1.0.35", "mikehaertl/php-shellcommand": "~1.2.5", "pixelandtonic/imagine": "0.7.1.1", + "seld/cli-prompt": "^1.0", "twig/twig": "~2.3.0", "yiisoft/yii2": "~2.0.12.0", "yiisoft/yii2-debug": "2.0.8", diff --git a/src/base/ApplicationTrait.php b/src/base/ApplicationTrait.php index 01252f1c5e2..cba4ec5c339 100644 --- a/src/base/ApplicationTrait.php +++ b/src/base/ApplicationTrait.php @@ -261,7 +261,7 @@ public function getIsInstalled(): bool return false; } - return $this->_isInstalled = (bool)($this->getRequest()->getIsConsoleRequest() || $this->getDb()->tableExists('{{%info}}', false)); + return $this->_isInstalled = (bool)($this->getDb()->tableExists('{{%info}}', false)); } /** diff --git a/src/base/Plugin.php b/src/base/Plugin.php index 57ef1c0521f..d6dcb3c3123 100644 --- a/src/base/Plugin.php +++ b/src/base/Plugin.php @@ -70,6 +70,7 @@ public function __construct($id, $parent = null, array $config = []) 'class' => PhpMessageSource::class, 'sourceLanguage' => $this->sourceLanguage, 'basePath' => $this->getBasePath().DIRECTORY_SEPARATOR.'translations', + 'forceTranslation' => true, 'allowOverrides' => true, ]; } diff --git a/src/base/VolumeInterface.php b/src/base/VolumeInterface.php index 4f0ba2b611e..77ad7b07f4f 100644 --- a/src/base/VolumeInterface.php +++ b/src/base/VolumeInterface.php @@ -28,7 +28,7 @@ interface VolumeInterface extends SavableComponentInterface /** * Returns the URL to the source, if it’s accessible via HTTP traffic. * - * @return string|null The root URL, or `false` if there isn’t one + * @return string|false The root URL, or `false` if there isn’t one */ public function getRootUrl(); diff --git a/src/behaviors/ElementQueryBehavior.php.template b/src/behaviors/ElementQueryBehavior.php.template index 30100a46110..27d860b4e26 100644 --- a/src/behaviors/ElementQueryBehavior.php.template +++ b/src/behaviors/ElementQueryBehavior.php.template @@ -26,7 +26,7 @@ class ElementQueryBehavior extends ContentBehavior { if (isset(self::$fieldHandles[$name]) && count($params) === 1) { $this->$name = $params[0]; - return $this; + return $this->owner; } return parent::__call($name, $params); } diff --git a/src/config/GeneralConfig.php b/src/config/GeneralConfig.php index 647d919ecf2..b27ef668e56 100644 --- a/src/config/GeneralConfig.php +++ b/src/config/GeneralConfig.php @@ -7,8 +7,8 @@ namespace craft\config; -use craft\helpers\ArrayHelper; use craft\helpers\ConfigHelper; +use craft\helpers\StringHelper; use yii\base\InvalidConfigException; use yii\base\Object; use yii\base\UnknownPropertyException; @@ -768,10 +768,10 @@ public function init() // Merge extraAllowedFileExtensions into allowedFileExtensions if (is_string($this->allowedFileExtensions)) { - $this->allowedFileExtensions = ArrayHelper::toArray($this->allowedFileExtensions); + $this->allowedFileExtensions = StringHelper::split($this->allowedFileExtensions); } if (is_string($this->extraAllowedFileExtensions)) { - $this->extraAllowedFileExtensions = ArrayHelper::toArray($this->extraAllowedFileExtensions); + $this->extraAllowedFileExtensions = StringHelper::split($this->extraAllowedFileExtensions); } if (is_array($this->extraAllowedFileExtensions)) { $this->allowedFileExtensions = array_merge($this->allowedFileExtensions, $this->extraAllowedFileExtensions); diff --git a/src/config/app/main.php b/src/config/app/main.php index 4da218fab74..3995c61dd23 100644 --- a/src/config/app/main.php +++ b/src/config/app/main.php @@ -3,8 +3,8 @@ return [ 'id' => 'CraftCMS', 'name' => 'Craft CMS', - 'version' => '3.0.0-beta.23', - 'schemaVersion' => '3.0.55', + 'version' => '3.0.0-beta.26', + 'schemaVersion' => '3.0.59', 'minVersionRequired' => '2.6.2788', 'basePath' => dirname(__DIR__, 2), // Defines the @app alias 'runtimePath' => '@storage/runtime', // Defines the @runtime alias diff --git a/src/config/mimeTypes.php b/src/config/mimeTypes.php new file mode 100644 index 00000000000..bce43dae269 --- /dev/null +++ b/src/config/mimeTypes.php @@ -0,0 +1,6 @@ + + * @since 3.0 + */ +class InstallController extends Controller +{ + // Properties + // ========================================================================= + + /** + * @var string|null The default email address for the first user to create during install + */ + public $email; + + /** + * @var string|null The default username for the first user to create during install + */ + public $username; + + /** + * @var string|null The default password for the first user to create during install + */ + public $password; + + /** + * @var string|null The default site name for the first site to create during install + */ + public $siteName; + + /** + * @var string|null The default site url for the first site to create during install + */ + public $siteUrl; + + /** + * @var string|null The default langcode for the first site to create during install + */ + public $language; + + // Public Methods + // ========================================================================= + + /** + * @inheritdoc + */ + public function options($actionID) + { + $options = parent::options($actionID); + $options[] = 'email'; + $options[] = 'username'; + $options[] = 'password'; + $options[] = 'siteName'; + $options[] = 'siteUrl'; + $options[] = 'language'; + return $options; + } + + /** + * Runs the install migration + */ + public function actionIndex() + { + if (Craft::$app->getIsInstalled()) { + $this->stdout("Craft is already installed!\n"); + return; + } + + // Validate the arguments + $errors = []; + if ($this->username && !$this->validateUsername($this->username, $error)) { + $errors[] = $error; + } + if ($this->email && !$this->validateEmail($this->email, $error)) { + $errors[] = $error; + } + if ($this->password && !$this->validatePassword($this->password, $error)) { + $errors[] = $error; + } + if ($this->siteName && !$this->validateSiteName($this->siteName, $error)) { + $errors[] = $error; + } + if ($this->siteUrl && !$this->validateSiteUrl($this->siteUrl, $error)) { + $errors[] = $error; + } + if ($this->language && !$this->validateLanguage($this->language, $error)) { + $errors[] = $error; + } + + if (!empty($errors)) { + $this->stderr("Invalid arguments:\n - ".implode("\n - ", $errors)."\n"); + return; + } + + $username = $this->username ?: $this->prompt('Username:', ['validator' => [$this, 'validateUsername'], 'default' => 'admin']); + $email = $this->email ?: $this->prompt('Email:', ['required' => true, 'validator' => [$this, 'validateEmail']]); + $password = $this->password ?: $this->_passwordPrompt(); + $siteName = $this->siteName ?: $this->prompt('Site Name:', ['required' => true, 'validator' => [$this, 'validateSiteName']]); + $siteUrl = $this->siteUrl ?: $this->prompt('Site URL:', ['required' => true, 'validator' => [$this, 'validateSiteUrl']]); + $language = $this->language ?: $this->prompt('Site Language:', ['validator' => [$this, 'validateLanguage'], 'default' => 'en-US']); + + $site = new Site([ + 'name' => $siteName, + 'handle' => 'default', + 'hasUrls' => true, + 'baseUrl' => $siteUrl, + 'language' => $language, + ]); + + $migration = new Install([ + 'username' => $username, + 'password' => $password, + 'email' => $email, + 'site' => $site, + ]); + + // Run the install migration + $migrator = Craft::$app->getMigrator(); + + if ($migrator->migrateUp($migration) !== false) { + $this->stdout("{$siteName} was successfully installed.\n", Console::FG_GREEN); + + // Mark all existing migrations as applied + foreach ($migrator->getNewMigrations() as $name) { + $migrator->addMigrationHistory($name); + } + } else { + $this->stderr("There was a problem installing {$siteName}.\n", Console::FG_RED); + } + } + + public function validateUsername(string $value, string &$error = null): bool + { + return $this->_validateUserAttribute('username', $value, $error); + } + + public function validateEmail(string $value, string &$error = null): bool + { + return $this->_validateUserAttribute('email', $value, $error); + } + + public function validatePassword(string $value, string &$error = null): bool + { + return $this->_validateUserAttribute('newPassword', $value, $error); + } + + public function validateSiteName(string $value, string &$error = null): bool + { + return $this->_validateSiteAttribute('name', $value, $error); + } + + public function validateSiteUrl(string $value, string &$error = null): bool + { + return $this->_validateSiteAttribute('baseUrl', $value, $error); + } + + public function validateLanguage(string $value, string &$error = null): bool + { + return $this->_validateSiteAttribute('language', $value, $error); + } + + // Private Methods + // ========================================================================= + + private function _validateUserAttribute(string $attribute, $value, &$error): bool + { + $user = new User([$attribute => $value]); + if (!$user->validate([$attribute])) { + $error = $user->getFirstError($attribute); + return false; + } + $error = null; + return true; + } + + private function _validateSiteAttribute(string $attribute, $value, &$error): bool + { + $site = new Site([$attribute => $value]); + if (!$site->validate([$attribute])) { + $error = $site->getFirstError($attribute); + return false; + } + $error = null; + return true; + } + + private function _passwordPrompt(): string + { + // todo: would be nice to replace CliPrompt with a native Yii silent prompt + // (https://github.com/yiisoft/yii2/issues/10551) + top: + $this->stdout('Password: '); + if (($password = CliPrompt::hiddenPrompt()) === '') { + $this->stdout("Invalid input.\n"); + goto top; + } + $this->stdout('Confirm: '); + if (!($matched = ($password === CliPrompt::hiddenPrompt()))) { + $this->stdout("Passwords didn't match, try again.\n"); + goto top; + } + return $password; + } +} diff --git a/src/controllers/BaseElementsController.php b/src/controllers/BaseElementsController.php index 6d68672b2f3..2c464a25413 100644 --- a/src/controllers/BaseElementsController.php +++ b/src/controllers/BaseElementsController.php @@ -72,9 +72,9 @@ protected function elementType(): string /** * Returns the context that this controller is being called in. * - * @return string + * @return string|null */ - protected function context(): string + protected function context() { return Craft::$app->getRequest()->getParam('context'); } diff --git a/src/controllers/CategoriesController.php b/src/controllers/CategoriesController.php index 63b8b14099a..d7b30ef3482 100644 --- a/src/controllers/CategoriesController.php +++ b/src/controllers/CategoriesController.php @@ -270,7 +270,7 @@ public function actionEditCategory(string $groupHandle, int $categoryId = null, // Parent Category selector variables // --------------------------------------------------------------------- - if ($variables['group']->maxLevels != 1) { + if ((int)$variables['group']->maxLevels !== 1) { $variables['elementType'] = Category::class; // Define the parent options criteria diff --git a/src/controllers/EntriesController.php b/src/controllers/EntriesController.php index 1c963bd6d7c..7f941dfc084 100644 --- a/src/controllers/EntriesController.php +++ b/src/controllers/EntriesController.php @@ -129,7 +129,7 @@ public function actionEditEntry(string $sectionHandle, int $entryId = null, int if ( $section->type === Section::TYPE_STRUCTURE && - $section->maxLevels !== 1 + (int)$section->maxLevels !== 1 ) { $variables['elementType'] = Entry::class; @@ -477,6 +477,15 @@ public function actionSaveEntry() } } + // Make sure the entry has at least one version if the section has versioning enabled + $revisionsService = Craft::$app->getEntryRevisions(); + if ($entry->getSection()->enableVersioning && $entry->id && !$revisionsService->doesEntryHaveVersions($entry->id, $entry->siteId)) { + $currentEntry = Craft::$app->getEntries()->getEntryById($entry->id, $entry->siteId); + $currentEntry->revisionCreatorId = $entry->authorId; + $currentEntry->revisionNotes = 'Revision from '.Craft::$app->getFormatter()->asDatetime($entry->dateUpdated); + $revisionsService->saveVersion($currentEntry); + } + // Save the entry (finally!) if (!Craft::$app->getElements()->saveElement($entry)) { if ($request->getAcceptsJson()) { @@ -497,7 +506,7 @@ public function actionSaveEntry() // Should we save a new version? if ($entry->getSection()->enableVersioning) { - Craft::$app->getEntryRevisions()->saveVersion($entry); + $revisionsService->saveVersion($entry); } if ($request->getAcceptsJson()) { diff --git a/src/controllers/RebrandController.php b/src/controllers/RebrandController.php index 5664224df31..17735babab3 100644 --- a/src/controllers/RebrandController.php +++ b/src/controllers/RebrandController.php @@ -78,7 +78,9 @@ public function actionUploadSiteImage(): Response move_uploaded_file($file->tempName, $fileDestination); - Craft::$app->getImages()->loadImage($fileDestination)->scaleToFit(300, 300)->saveAs($fileDestination); + $imagesService = Craft::$app->getImages(); + $imagesService->cleanImage($fileDestination); + $imagesService->loadImage($fileDestination)->scaleToFit(300, 300)->saveAs($fileDestination); $html = $this->getView()->renderTemplate('settings/general/_images/'.$type); return $this->asJson(['html' => $html]); diff --git a/src/controllers/TemplatesController.php b/src/controllers/TemplatesController.php index 4774e8e6e9e..0964ad6a21a 100644 --- a/src/controllers/TemplatesController.php +++ b/src/controllers/TemplatesController.php @@ -48,7 +48,7 @@ class TemplatesController extends Controller public function beforeAction($action) { $actionSegments = Craft::$app->getRequest()->getActionSegments(); - if (isset($actionSegments[0]) && $actionSegments[0] === 'templates') { + if (isset($actionSegments[0]) && strtolower($actionSegments[0]) === 'templates') { throw new ForbiddenHttpException(); } @@ -128,16 +128,16 @@ public function actionRequirementsCheck() } throw new ServerErrorHttpException(Craft::t('app', 'The update can’t be installed :( {message}', ['message' => $message])); - } else { - return $this->renderTemplate('_special/cantrun', [ - 'reqCheck' => $reqCheck - ]); } - } else { - // Cache the base path. - Craft::$app->getCache()->set('basePath', Craft::$app->getBasePath()); + + return $this->renderTemplate('_special/cantrun', [ + 'reqCheck' => $reqCheck + ]); } + // Cache the base path. + Craft::$app->getCache()->set('basePath', Craft::$app->getBasePath()); + return null; } diff --git a/src/controllers/UsersController.php b/src/controllers/UsersController.php index 9ca871aa075..458c9c1dbe3 100644 --- a/src/controllers/UsersController.php +++ b/src/controllers/UsersController.php @@ -13,7 +13,7 @@ use craft\errors\UploadFailedException; use craft\events\LoginFailureEvent; use craft\events\RegisterUserActionsEvent; -use craft\events\UserTokenEvent; +use craft\events\UserEvent; use craft\helpers\Assets; use craft\helpers\DateTimeHelper; use craft\helpers\FileHelper; @@ -1780,7 +1780,7 @@ private function _processTokenRequest() if ($userToProcess) { // Fire a 'beforeVerifyUser' event Craft::$app->getUsers()->trigger(Users::EVENT_BEFORE_VERIFY_EMAIL, - new UserTokenEvent([ + new UserEvent([ 'user' => $userToProcess ])); @@ -1793,7 +1793,7 @@ private function _processTokenRequest() // Fire an 'afterVerifyUser' event Craft::$app->getUsers()->trigger(Users::EVENT_AFTER_VERIFY_EMAIL, - new UserTokenEvent([ + new UserEvent([ 'user' => $userToProcess ])); diff --git a/src/controllers/UtilitiesController.php b/src/controllers/UtilitiesController.php index b7917825f47..09f62634484 100644 --- a/src/controllers/UtilitiesController.php +++ b/src/controllers/UtilitiesController.php @@ -14,6 +14,7 @@ use craft\base\UtilityInterface; use craft\db\Query; use craft\elements\Asset; +use craft\errors\MigrationException; use craft\helpers\FileHelper; use craft\helpers\Path; use craft\queue\jobs\FindAndReplace; @@ -547,9 +548,10 @@ public function actionApplyNewMigrations() $migrator = Craft::$app->getContentMigrator(); - if ($migrator->up(0)) { + try { + $migrator->up(); Craft::$app->getSession()->setNotice(Craft::t('app', 'Applied new migrations successfully.')); - } else { + } catch (MigrationException $e) { Craft::$app->getSession()->setError(Craft::t('app', 'Couldn’t apply new migrations.')); } diff --git a/src/controllers/VolumesController.php b/src/controllers/VolumesController.php index 1e5aa81684a..74404684e0a 100644 --- a/src/controllers/VolumesController.php +++ b/src/controllers/VolumesController.php @@ -243,41 +243,4 @@ public function actionDeleteVolume(): Response return $this->asJson(['success' => true]); } - - /** - * Load Assets VolumeType data. - * - * This is used to, for example, load Amazon S3 bucket list or Rackspace Cloud Storage Containers. - * - * @return Response - */ - public function actionLoadVolumeTypeData(): Response - { - $this->requirePostRequest(); - $this->requireAcceptsJson(); - - $request = Craft::$app->getRequest(); - $volumeType = $request->getRequiredBodyParam('volumeType'); - $dataType = $request->getRequiredBodyParam('dataType'); - $params = $request->getBodyParam('params'); - - $volumeType = 'craft\volumes\\'.$volumeType; - - if (!class_exists($volumeType)) { - return $this->asErrorJson(Craft::t('app', 'The volume type specified does not exist!')); - } - - try { - $result = call_user_func_array( - [ - $volumeType, - 'load'.ucfirst($dataType) - ], - $params); - - return $this->asJson($result); - } catch (\Throwable $exception) { - return $this->asErrorJson($exception->getMessage()); - } - } } diff --git a/src/db/Connection.php b/src/db/Connection.php index c5909f49b4e..0029bc7b34a 100644 --- a/src/db/Connection.php +++ b/src/db/Connection.php @@ -309,7 +309,7 @@ public function getPrimaryKeyName(string $table, $columns): string { $table = $this->_getTableNameWithoutPrefix($table); if (is_string($columns)) { - $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY); + $columns = StringHelper::split($columns); } $name = $this->tablePrefix.$table.'_'.implode('_', $columns).'_pk'; @@ -328,7 +328,7 @@ public function getForeignKeyName(string $table, $columns): string { $table = $this->_getTableNameWithoutPrefix($table); if (is_string($columns)) { - $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY); + $columns = StringHelper::split($columns); } $name = $this->tablePrefix.$table.'_'.implode('_', $columns).'_fk'; @@ -350,7 +350,7 @@ public function getIndexName(string $table, $columns, bool $unique = false, bool { $table = $this->_getTableNameWithoutPrefix($table); if (is_string($columns)) { - $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY); + $columns = StringHelper::split($columns); } $name = $this->tablePrefix.$table.'_'.implode('_', $columns).($unique ? '_unq' : '').($foreignKey ? '_fk' : '_idx'); diff --git a/src/db/mysql/QueryBuilder.php b/src/db/mysql/QueryBuilder.php index c0e5f285caa..f28056bafd7 100644 --- a/src/db/mysql/QueryBuilder.php +++ b/src/db/mysql/QueryBuilder.php @@ -22,6 +22,17 @@ */ class QueryBuilder extends \yii\db\mysql\QueryBuilder { + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + + // Use LONGBLOB for "binary" columns rather than BLOB + $this->typeMap[Schema::TYPE_BINARY] = 'longblob'; + } + /** * @inheritdoc * diff --git a/src/elements/Asset.php b/src/elements/Asset.php index 3117a5f317e..11b8a602119 100644 --- a/src/elements/Asset.php +++ b/src/elements/Asset.php @@ -711,9 +711,11 @@ public function getExtension() } /** - * @return string + * Returns the file’s MIME type, if it can be determined. + * + * @return string|null */ - public function getMimeType(): string + public function getMimeType() { // todo: maybe we should be passing this off to volume types // so Local volumes can call FileHelper::getMimeType() (uses magic file instead of ext) diff --git a/src/elements/Entry.php b/src/elements/Entry.php index cf66fc2cca7..6e3beca9057 100644 --- a/src/elements/Entry.php +++ b/src/elements/Entry.php @@ -476,6 +476,11 @@ protected static function prepElementQueryForTableAttribute(ElementQueryInterfac */ public $newParentId; + /** + * @var int|null Revision creator ID + */ + public $revisionCreatorId; + /** * @var string|null Revision notes */ diff --git a/src/elements/db/AssetQuery.php b/src/elements/db/AssetQuery.php index db1329d5c6c..17b8fb3fec5 100644 --- a/src/elements/db/AssetQuery.php +++ b/src/elements/db/AssetQuery.php @@ -11,8 +11,8 @@ use craft\base\Volume; use craft\db\Query; use craft\elements\Asset; -use craft\helpers\ArrayHelper; use craft\helpers\Db; +use craft\helpers\StringHelper; use yii\db\Connection; /** @@ -81,7 +81,7 @@ class AssetQuery extends ElementQuery public $includeSubfolders = false; /** - * @var array|null The asset transform indexes that should be eager-loaded, if they exist + * @var string|array|null The asset transform indexes that should be eager-loaded, if they exist */ public $withTransforms; @@ -283,7 +283,7 @@ public function includeSubfolders(bool $value = true) /** * Sets the [[withTransforms]] property. * - * @param array|null $value The transforms to include. + * @param string|array|null $value The transforms to include. * * @return self The query object itself */ @@ -302,10 +302,13 @@ public function populate($rows) $elements = parent::populate($rows); // Eager-load transforms? - if ($this->asArray === false && !empty($this->withTransforms)) { - $transforms = ArrayHelper::toArray($this->withTransforms); + if ($this->asArray === false && $this->withTransforms) { + $transforms = $this->withTransforms; + if (!is_array($transforms)) { + $transforms = is_string($transforms) ? StringHelper::split($transforms) : [$transforms]; + } - Craft::$app->getAssetTransforms()->eagerLoadTransforms($elements, $transforms); + Craft::$app->getAssetTransforms()->eagerLoadTransforms($elements, $this->withTransforms); } return $elements; diff --git a/src/elements/db/CategoryQuery.php b/src/elements/db/CategoryQuery.php index c715ca2ccde..86f11fffd5c 100644 --- a/src/elements/db/CategoryQuery.php +++ b/src/elements/db/CategoryQuery.php @@ -11,8 +11,8 @@ use craft\db\Query; use craft\db\QueryAbortedException; use craft\elements\Category; -use craft\helpers\ArrayHelper; use craft\helpers\Db; +use craft\helpers\StringHelper; use craft\models\CategoryGroup; use yii\db\Connection; @@ -201,7 +201,11 @@ private function _applyRefParam() return; } - $refs = ArrayHelper::toArray($this->ref); + $refs = $this->ref; + if (!is_array($refs)) { + $refs = is_string($refs) ? StringHelper::split($refs) : [$refs]; + } + $condition = ['or']; $joinCategoryGroups = false; diff --git a/src/elements/db/ElementQuery.php b/src/elements/db/ElementQuery.php index 97a7601c265..1263de3e1f6 100644 --- a/src/elements/db/ElementQuery.php +++ b/src/elements/db/ElementQuery.php @@ -125,7 +125,7 @@ class ElementQuery extends Query implements ElementQueryInterface /** * @var string|string[]|null The status(es) that the resulting elements must have. */ - public $status = 'enabled'; + public $status = ['enabled']; /** * @var bool Whether to return only archived elements. @@ -1474,7 +1474,11 @@ private function _applyStatusParam(string $class) return; } - $statuses = ArrayHelper::toArray($this->status); + $statuses = $this->status; + if (!is_array($statuses)) { + $statuses = is_string($statuses) ? StringHelper::split($statuses) : [$statuses]; + } + $condition = ['or']; foreach ($statuses as $status) { @@ -1775,7 +1779,10 @@ private function _applyOrderByParams(Connection $db) // Any other empty value means we should set it if (empty($this->orderBy)) { if ($this->fixedOrder) { - $ids = ArrayHelper::toArray($this->id); + $ids = $this->id; + if (!is_array($ids)) { + $ids = is_string($ids) ? StringHelper::split($ids) : [$ids]; + } if (empty($ids)) { throw new QueryAbortedException; diff --git a/src/elements/db/ElementRelationParamParser.php b/src/elements/db/ElementRelationParamParser.php index 4f8d893fac2..c47a49609d0 100644 --- a/src/elements/db/ElementRelationParamParser.php +++ b/src/elements/db/ElementRelationParamParser.php @@ -15,6 +15,7 @@ use craft\fields\BaseRelationField; use craft\fields\Matrix; use craft\helpers\ArrayHelper; +use craft\helpers\StringHelper; use craft\models\Site; /** @@ -68,10 +69,8 @@ class ElementRelationParamParser public function parse($relatedToParam) { // Ensure the criteria is an array - if (is_string($relatedToParam)) { - $relatedToParam = ArrayHelper::toArray($relatedToParam); - } else if (!is_array($relatedToParam)) { - $relatedToParam = [$relatedToParam]; + if (!is_array($relatedToParam)) { + $relatedToParam = is_string($relatedToParam) ? StringHelper::split($relatedToParam) : [$relatedToParam]; } if (isset($relatedToParam['element']) || isset($relatedToParam['sourceElement']) || isset($relatedToParam['targetElement'])) { @@ -179,7 +178,10 @@ private function _subparse($relCriteria) foreach ($elementParams as $elementParam) { if (isset($relCriteria[$elementParam])) { - $elements = ArrayHelper::toArray($relCriteria[$elementParam], [], false); + $elements = $relCriteria[$elementParam]; + if (!is_array($elements)) { + $elements = is_string($elements) ? StringHelper::split($elements) : [$elements]; + } if (isset($elements[0]) && ($elements[0] === 'and' || $elements[0] === 'or')) { $glue = array_shift($elements); @@ -247,7 +249,10 @@ private function _subparse($relCriteria) if ($relCriteria['field']) { // Loop through all of the fields in this rel criteria, create the Matrix-specific conditions right away // and save the normal field IDs for later - $fields = ArrayHelper::toArray($relCriteria['field']); + $fields = $relCriteria['field']; + if (!is_array($fields)) { + $fields = is_string($fields) ? StringHelper::split($fields) : [$fields]; + } foreach ($fields as $field) { if (($fieldModel = $this->_getField($field, $fieldHandleParts)) === null) { diff --git a/src/elements/db/EntryQuery.php b/src/elements/db/EntryQuery.php index 9190ed953a3..63acb0bedb7 100644 --- a/src/elements/db/EntryQuery.php +++ b/src/elements/db/EntryQuery.php @@ -13,6 +13,7 @@ use craft\elements\Entry; use craft\helpers\ArrayHelper; use craft\helpers\Db; +use craft\helpers\StringHelper; use craft\models\EntryType; use craft\models\Section; use craft\models\UserGroup; @@ -88,7 +89,7 @@ public function __construct($elementType, array $config = []) { // Default status if (!isset($config['status'])) { - $config['status'] = 'live'; + $config['status'] = ['live']; } parent::__construct($elementType, $config); @@ -302,8 +303,14 @@ public function before($value) $value = $value->format(DateTime::W3C); } - $this->postDate = ArrayHelper::toArray($this->postDate); - $this->postDate[] = '<'.$value; + if (!$this->postDate) { + $this->postDate = '<'.$value; + } else { + if (!is_array($this->postDate)) { + $this->postDate = [$this->postDate]; + } + $this->postDate[] = '<'.$value;; + } return $this; } @@ -321,8 +328,14 @@ public function after($value) $value = $value->format(DateTime::W3C); } - $this->postDate = ArrayHelper::toArray($this->postDate); - $this->postDate[] = '>='.$value; + if (!$this->postDate) { + $this->postDate = '>='.$value; + } else { + if (!is_array($this->postDate)) { + $this->postDate = [$this->postDate]; + } + $this->postDate[] = '>='.$value;; + } return $this; } @@ -514,7 +527,11 @@ private function _applyRefParam() return; } - $refs = ArrayHelper::toArray($this->ref); + $refs = $this->ref; + if (!is_array($refs)) { + $refs = is_string($refs) ? StringHelper::split($refs) : [$refs]; + } + $joinSections = false; $condition = ['or']; diff --git a/src/elements/db/UserQuery.php b/src/elements/db/UserQuery.php index e5d2f387334..14cf1c09f64 100644 --- a/src/elements/db/UserQuery.php +++ b/src/elements/db/UserQuery.php @@ -95,7 +95,7 @@ public function __construct($elementType, array $config = []) // Default status if (!isset($config['status'])) { - $config['status'] = User::STATUS_ACTIVE; + $config['status'] = [User::STATUS_ACTIVE]; } parent::__construct($elementType, $config); diff --git a/src/events/UserActivateEvent.php b/src/events/UserActivateEvent.php deleted file mode 100644 index 0cec4d2e8e0..00000000000 --- a/src/events/UserActivateEvent.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @since 3.0 - */ -class UserActivateEvent extends CancelableEvent -{ - // Properties - // ========================================================================= - - /** - * @var User|null The user model associated with the event. - */ - public $user; -} diff --git a/src/events/UserUnlockEvent.php b/src/events/UserEvent.php similarity index 86% rename from src/events/UserUnlockEvent.php rename to src/events/UserEvent.php index f0a5ce9e796..13bcdf99556 100644 --- a/src/events/UserUnlockEvent.php +++ b/src/events/UserEvent.php @@ -10,12 +10,12 @@ use craft\elements\User; /** - * User unlock event class. + * User event class. * * @author Pixel & Tonic, Inc. * @since 3.0 */ -class UserUnlockEvent extends CancelableEvent +class UserEvent extends CancelableEvent { // Properties // ========================================================================= diff --git a/src/events/UserSuspendEvent.php b/src/events/UserSuspendEvent.php deleted file mode 100644 index d3e0c5f0827..00000000000 --- a/src/events/UserSuspendEvent.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @since 3.0 - */ -class UserSuspendEvent extends CancelableEvent -{ - // Properties - // ========================================================================= - - /** - * @var User|null The user model associated with the event. - */ - public $user; -} diff --git a/src/events/UserTokenEvent.php b/src/events/UserTokenEvent.php deleted file mode 100644 index b7869ddc58f..00000000000 --- a/src/events/UserTokenEvent.php +++ /dev/null @@ -1,28 +0,0 @@ - - * @since 3.0 - */ -class UserTokenEvent extends Event -{ - // Properties - // ========================================================================= - - /** - * @var User|null The user model associated with the event. - */ - public $user; -} diff --git a/src/events/UserUnsuspendEvent.php b/src/events/UserUnsuspendEvent.php deleted file mode 100644 index 49c2a23c144..00000000000 --- a/src/events/UserUnsuspendEvent.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @since 3.0 - */ -class UserUnsuspendEvent extends CancelableEvent -{ - // Properties - // ========================================================================= - - /** - * @var User|null The user model associated with the event. - */ - public $user; -} diff --git a/src/fields/BaseRelationField.php b/src/fields/BaseRelationField.php index 17df7c00078..9d99c8ce335 100644 --- a/src/fields/BaseRelationField.php +++ b/src/fields/BaseRelationField.php @@ -334,7 +334,7 @@ public function getIsTranslatable(ElementInterface $element = null): bool */ public function getInputHtml($value, ElementInterface $element = null): string { - /** @var Element $element */ + /** @var Element|null $element */ if ($element !== null && $element->hasEagerLoadedElements($this->handle)) { $value = $element->getEagerLoadedElements($this->handle); } diff --git a/src/fields/Tags.php b/src/fields/Tags.php index 5302b910fc3..1500cd76c8b 100644 --- a/src/fields/Tags.php +++ b/src/fields/Tags.php @@ -10,6 +10,7 @@ use Craft; use craft\base\Element; use craft\base\ElementInterface; +use craft\elements\db\ElementQuery; use craft\elements\db\ElementQueryInterface; use craft\elements\Tag; use craft\models\TagGroup; @@ -76,11 +77,17 @@ public function init() public function getInputHtml($value, ElementInterface $element = null): string { /** @var Element|null $element */ - if (!($value instanceof ElementQueryInterface)) { - /** @var Element $class */ - $class = static::elementType(); - $value = $class::find() - ->id(false); + if ($element !== null && $element->hasEagerLoadedElements($this->handle)) { + $value = $element->getEagerLoadedElements($this->handle); + } + + if ($value instanceof ElementQueryInterface) { + $value = $value + ->status(null) + ->enabledForSite(false) + ->all(); + } else if (!is_array($value)) { + $value = []; } $tagGroup = $this->_getTagGroup(); diff --git a/src/helpers/ArrayHelper.php b/src/helpers/ArrayHelper.php index f6d19959d62..351bfdd8ac4 100644 --- a/src/helpers/ArrayHelper.php +++ b/src/helpers/ArrayHelper.php @@ -7,6 +7,8 @@ namespace craft\helpers; +use Craft; + /** * Class ArrayHelper * @@ -28,6 +30,8 @@ public static function toArray($object, $properties = [], $recursive = true): ar } if (is_string($object)) { + Craft::$app->getDeprecator()->log('ArrayHelper::toArray(string)', 'Passing a string to ArrayHelper::toArray() has been deprectaed. Use StringHelper::split() instead.'); + // Split it on the non-escaped commas $object = preg_split('/(?= :startDate", "{$dateColumnSql} < :endDate"]; + $params = [ + ':startDate' => Db::prepareDateForDb($startDate), + ':endDate' => Db::prepareDateForDb($endDate), + ]; + $orderBy = ['date' => SORT_ASC]; + + // If this is an element query, modify the prepared query directly + if ($query instanceof ElementQueryInterface) { + $query = $query->prepare(Craft::$app->getDb()->getQueryBuilder()); + /** @var Query $subQuery */ + $subQuery = $query->from['subquery']; + $subQuery + ->addSelect($query->select) + ->addSelect([$select]) + ->andWhere($condition, $params) + ->groupBy($sqlGroup) + ->orderBy($orderBy); + $query + ->select(['subquery.value', 'subquery.date']) + ->orderBy($orderBy); + } else { + $query + ->addSelect([$select]) + ->andWhere($condition, $params) + ->groupBy($sqlGroup) + ->orderBy($orderBy); + } + // Execute the query - $results = $query - ->addSelect([$select]) - ->andWhere( - ['and', "{$dateColumnSql} >= :startDate", "{$dateColumnSql} < :endDate"], - [':startDate' => Db::prepareDateForDb($startDate), ':endDate' => Db::prepareDateForDb($endDate)]) - ->groupBy($sqlGroup) - ->orderBy(['date' => SORT_ASC]) - ->all(); + $results = $query->all(); // Assemble the data $rows = []; @@ -287,4 +311,4 @@ public static function dateRanges(): array return $dateRanges; } -} +} \ No newline at end of file diff --git a/src/helpers/Component.php b/src/helpers/Component.php index 16f4434e3cd..f18458493d7 100644 --- a/src/helpers/Component.php +++ b/src/helpers/Component.php @@ -39,8 +39,6 @@ public static function createComponent($config, string $instanceOf = null): Comp $class = $config; $config = []; } else { - $config = ArrayHelper::toArray($config); - if (empty($config['type'])) { throw new InvalidConfigException('The config passed into Component::createComponent() did not specify a class: '.Json::encode($config)); } diff --git a/src/helpers/Db.php b/src/helpers/Db.php index faed4799ff1..84d8c6b47b9 100644 --- a/src/helpers/Db.php +++ b/src/helpers/Db.php @@ -458,7 +458,7 @@ public static function parseParam(string $column, $value) return ''; } - $value = ArrayHelper::toArray($value); + $value = self::_toArray($value); if (!count($value)) { return ''; @@ -506,7 +506,7 @@ public static function parseParam(string $column, $value) $condition[] = [ 'not', [ - [$column => null], + $column => null, ] ]; } @@ -554,7 +554,7 @@ public static function parseDateParam(string $column, $value) { $normalizedValues = []; - $value = ArrayHelper::toArray($value); + $value = self::_toArray($value); if (!count($value)) { return ''; @@ -614,6 +614,43 @@ public static function isTypeSupported(string $type, Connection $db = null): boo // Private Methods // ========================================================================= + /** + * Converts a given param value to an array. + * + * @param mixed $value + * + * @return array + */ + private static function _toArray($value): array + { + if ($value === null) { + return []; + } + + if (is_string($value)) { + // Split it on the non-escaped commas + $value = preg_split('/(? $val) { + // Remove leading/trailing whitespace + $val = trim($val); + + // Remove any backslashes used to escape commas + $val = str_replace('\,', ',', $val); + + $value[$key] = $val; + } + + // Remove any empty elements and reset the keys + $value = array_merge(array_filter($value)); + + return $value; + } + + return ArrayHelper::toArray($value); + } + /** * Normalizes “empty” values. * diff --git a/src/helpers/FileHelper.php b/src/helpers/FileHelper.php index c2786ef7bf1..b1d954b941f 100644 --- a/src/helpers/FileHelper.php +++ b/src/helpers/FileHelper.php @@ -23,6 +23,14 @@ */ class FileHelper extends \yii\helpers\FileHelper { + // Static + // ========================================================================= + + /** + * @inheritdoc + */ + public static $mimeMagicFile = '@app/config/mimeTypes.php'; + // Properties // ========================================================================= diff --git a/src/helpers/Image.php b/src/helpers/Image.php index ffa92085738..02f63dbd05c 100644 --- a/src/helpers/Image.php +++ b/src/helpers/Image.php @@ -61,7 +61,10 @@ public static function calculateMissingDimension($targetWidth, $targetHeight, $s */ public static function canManipulateAsImage(string $extension): bool { - return in_array(StringHelper::toLowerCase($extension), Craft::$app->getImages()->getSupportedImageFormats(), true); + $formats = Craft::$app->getImages()->getSupportedImageFormats(); + $formats[] = 'svg'; + + return in_array(strtolower($extension), $formats); } /** diff --git a/src/helpers/MigrationHelper.php b/src/helpers/MigrationHelper.php index b253a2143f4..e749c8f71e5 100644 --- a/src/helpers/MigrationHelper.php +++ b/src/helpers/MigrationHelper.php @@ -35,7 +35,9 @@ public static function doesForeignKeyExist(string $tableName, $columns): bool $schema = $db->getSchema(); $tableName = $schema->getRawTableName($tableName); $schema->refreshTableSchema($tableName); - $columns = ArrayHelper::toArray($columns); + if (is_string($columns)) { + $columns = StringHelper::split($columns); + } $table = $db->getTableSchema($tableName); foreach ($table->foreignKeys as $num => $fk) { @@ -87,7 +89,9 @@ public static function doesIndexExist(string $tableName, $columns, bool $unique { $db = Craft::$app->getDb(); $schema = $db->getSchema(); - $columns = ArrayHelper::toArray($columns); + if (is_string($columns)) { + $columns = StringHelper::split($columns); + } $allIndexes = $schema->findIndexes($tableName); $needleIndex = $db->getIndexName($tableName, $columns, $unique, $foreignKey); diff --git a/src/helpers/StringHelper.php b/src/helpers/StringHelper.php index 8f281011447..71e7d0f73ee 100644 --- a/src/helpers/StringHelper.php +++ b/src/helpers/StringHelper.php @@ -511,6 +511,19 @@ public static function toSnakeCase(string $string): string return implode('_', $words); } + /** + * Splits a string into chunks on a given delimiter. + * + * @param string $string The string + * @param string $delimiter The delimiter to split the string on (defaults to a comma) + * + * @return string[] The segments of the string + */ + public static function split(string $string, string $delimiter = ','): array + { + return preg_split('/\s*'.preg_quote($delimiter, '/').'\s*/', $string, -1, PREG_SPLIT_NO_EMPTY); + } + /** * Splits a string into an array of the words in the string. * diff --git a/src/migrations/Install.php b/src/migrations/Install.php index 7c857c6d994..db60e944ea2 100644 --- a/src/migrations/Install.php +++ b/src/migrations/Install.php @@ -220,7 +220,7 @@ protected function createTables() 'fingerprint' => $this->string()->notNull(), 'lastOccurrence' => $this->dateTime()->notNull(), 'file' => $this->string()->notNull(), - 'line' => $this->smallInteger()->notNull()->unsigned(), + 'line' => $this->smallInteger()->unsigned(), 'message' => $this->string(), 'traces' => $this->text(), 'dateCreated' => $this->dateTime()->notNull(), diff --git a/src/migrations/m170731_185314_delete_compiled_behaviors.php b/src/migrations/m170816_133741_delete_compiled_behaviors.php similarity index 80% rename from src/migrations/m170731_185314_delete_compiled_behaviors.php rename to src/migrations/m170816_133741_delete_compiled_behaviors.php index 97f159ee4e4..7bc26a74976 100644 --- a/src/migrations/m170731_185314_delete_compiled_behaviors.php +++ b/src/migrations/m170816_133741_delete_compiled_behaviors.php @@ -7,9 +7,9 @@ use craft\helpers\FileHelper; /** - * m170731_185314_delete_compiled_behaviors migration. + * m170816_133741_delete_compiled_behaviors migration. */ -class m170731_185314_delete_compiled_behaviors extends Migration +class m170816_133741_delete_compiled_behaviors extends Migration { /** * @inheritdoc @@ -34,7 +34,7 @@ public function safeUp() */ public function safeDown() { - echo "m170731_185314_delete_compiled_behaviors cannot be reverted.\n"; + echo "m170816_133741_delete_compiled_behaviors cannot be reverted.\n"; return false; } } diff --git a/src/migrations/m170821_180624_deprecation_line_nullable.php b/src/migrations/m170821_180624_deprecation_line_nullable.php new file mode 100644 index 00000000000..a0281040aeb --- /dev/null +++ b/src/migrations/m170821_180624_deprecation_line_nullable.php @@ -0,0 +1,29 @@ +alterColumn('{{%deprecationerrors}}', 'line', $this->smallInteger()->unsigned()); + } + + /** + * @inheritdoc + */ + public function safeDown() + { + echo "m170821_180624_deprecation_line_nullable cannot be reverted.\n"; + return false; + } +} diff --git a/src/migrations/m170903_192801_longblob_for_queue_jobs.php b/src/migrations/m170903_192801_longblob_for_queue_jobs.php new file mode 100644 index 00000000000..c5f4712ac0f --- /dev/null +++ b/src/migrations/m170903_192801_longblob_for_queue_jobs.php @@ -0,0 +1,32 @@ +db->getIsMysql()) { + // "binary" resolves to LONGBLOB now rather than BLOB + $this->alterColumn('{{%queue}}', 'job', $this->binary()->notNull()); + } + } + + /** + * @inheritdoc + */ + public function safeDown() + { + echo "m170903_192801_longblob_for_queue_jobs cannot be reverted.\n"; + return false; + } +} diff --git a/src/queue/Queue.php b/src/queue/Queue.php index 7680e4bdf88..ccea430d650 100644 --- a/src/queue/Queue.php +++ b/src/queue/Queue.php @@ -418,8 +418,7 @@ protected function pushMessage($message, $ttr, $delay, $priority) false) ->execute(); - $tableSchema = $db->getTableSchema('{{%queue}}'); - return $db->getLastInsertID($tableSchema->sequenceName); + return $db->getLastInsertID('{{%queue}}'); } /** diff --git a/src/queue/jobs/ResaveElements.php b/src/queue/jobs/ResaveElements.php index d53476f83d5..aeffdb68ed8 100644 --- a/src/queue/jobs/ResaveElements.php +++ b/src/queue/jobs/ResaveElements.php @@ -44,7 +44,7 @@ class ResaveElements extends BaseJob */ public function execute($queue) { - $class = $this->elementTydddhpe; + $class = $this->elementType; // Let's save ourselves some trouble and just clear all the caches for this element class Craft::$app->getTemplateCaches()->deleteCachesByElementType($class); diff --git a/src/services/Categories.php b/src/services/Categories.php index 725b4aca0a4..0fc3c55e677 100644 --- a/src/services/Categories.php +++ b/src/services/Categories.php @@ -314,7 +314,7 @@ public function saveGroup(CategoryGroup $group, bool $runValidation = true): boo } // If they've set maxLevels to 0 (don't ask why), then pretend like there are none. - if ($group->maxLevels === 0) { + if ((int)$group->maxLevels === 0) { $group->maxLevels = null; } diff --git a/src/services/Elements.php b/src/services/Elements.php index b34fea1bd13..d29cbb52bfb 100644 --- a/src/services/Elements.php +++ b/src/services/Elements.php @@ -1190,7 +1190,10 @@ public function eagerLoadElements(string $elementType, array $elements, $with) } // Normalize the paths and find any custom path criterias - $with = ArrayHelper::toArray($with); + if (is_string($with)) { + $with = StringHelper::split($with); + } + $paths = []; $pathCriterias = []; diff --git a/src/services/EntryRevisions.php b/src/services/EntryRevisions.php index 9e94a3b9d0f..f4c44385e94 100644 --- a/src/services/EntryRevisions.php +++ b/src/services/EntryRevisions.php @@ -334,18 +334,37 @@ public function getVersionById(int $versionId) return $version; } + /** + * Returns whether an entry has any versions stored. + * + * @param int $entryId The entry ID to search for + * @param int|null $siteId The site ID to search for + * + * @return bool + */ + public function doesEntryHaveVersions(int $entryId, int $siteId = null): bool + { + if (!$siteId) { + $siteId = Craft::$app->getSites()->getPrimarySite()->id; + } + + return $this->_getRevisionsQuery() + ->where(['entryId' => $entryId, 'siteId' => $siteId]) + ->exists(); + } + /** * Returns versions by an entry ID. * * @param int $entryId The entry ID to search for. - * @param int $siteId The site ID to search for. + * @param int|null $siteId The site ID to search for. * @param int|null $limit The limit on the number of versions to retrieve. * @param bool $includeCurrent Whether to include the current "top" version of the entry. * @param bool $withContent Whether the field content should be included on the versions * * @return EntryVersion[] */ - public function getVersionsByEntryId(int $entryId, int $siteId, int $limit = null, bool $includeCurrent = false, bool $withContent = false): array + public function getVersionsByEntryId(int $entryId, int $siteId = null, int $limit = null, bool $includeCurrent = false, bool $withContent = false): array { if (!$siteId) { $siteId = Craft::$app->getSites()->getPrimarySite()->id; @@ -397,7 +416,7 @@ public function saveVersion(Entry $entry): bool $versionRecord = new EntryVersionRecord(); $versionRecord->entryId = $entry->id; $versionRecord->sectionId = $entry->sectionId; - $versionRecord->creatorId = Craft::$app->getUser()->getIdentity()->id ?? $entry->authorId; + $versionRecord->creatorId = $entry->revisionCreatorId ?? Craft::$app->getUser()->getIdentity()->id ?? $entry->authorId; $versionRecord->siteId = $entry->siteId; $versionRecord->num = $totalVersions + 1; $versionRecord->data = $this->_getRevisionData($entry); diff --git a/src/services/Images.php b/src/services/Images.php index 702017abdcc..11d5b089e1e 100644 --- a/src/services/Images.php +++ b/src/services/Images.php @@ -107,7 +107,7 @@ public function getIsImagick(): bool public function getSupportedImageFormats(): array { if ($this->getIsImagick()) { - return array_map([StringHelper::class, 'toLowerCase'], Imagick::queryFormats()); + return array_map('strtolower', Imagick::queryFormats()); } $output = []; @@ -260,7 +260,7 @@ public function checkMemoryForImage(string $filePath, bool $toTheMax = false): b } /** - * Cleans an image by it's path, clearing embedded potentially malicious embedded code. + * Cleans an image by its path, clearing embedded potentially malicious embedded code. * * @param string $filePath * diff --git a/src/services/Matrix.php b/src/services/Matrix.php index 4f8b2caab7b..7063eae9092 100644 --- a/src/services/Matrix.php +++ b/src/services/Matrix.php @@ -730,7 +730,7 @@ public function saveField(MatrixField $field, ElementInterface $owner): bool Craft::$app->getSession()->addAssetBundleFlash(MatrixAsset::class); foreach ($collapsedBlockIds as $blockId) { - Craft::$app->getSession()->addJsFlash('debugger;'."\n".'Craft.MatrixInput.rememberCollapsedBlockId('.$blockId.');'); + Craft::$app->getSession()->addJsFlash('Craft.MatrixInput.rememberCollapsedBlockId('.$blockId.');'); } } diff --git a/src/services/Sections.php b/src/services/Sections.php index 3da2cee86d0..01553e555c5 100644 --- a/src/services/Sections.php +++ b/src/services/Sections.php @@ -410,7 +410,7 @@ public function saveSection(Section $section, bool $runValidation = true): bool } // If they've set maxLevels to 0 (don't ask why), then pretend like there are none. - if ($section->maxLevels === 0 || $section->maxLevels === '0') { + if ((int)$section->maxLevels === 0) { $section->maxLevels = null; } diff --git a/src/services/TemplateCaches.php b/src/services/TemplateCaches.php index fe5126d432a..362ce76b77b 100644 --- a/src/services/TemplateCaches.php +++ b/src/services/TemplateCaches.php @@ -518,10 +518,14 @@ public function deleteCachesByElementId($elementId, bool $deleteQueryCaches = tr */ public function handleResponse() { - Craft::$app->getQueue()->push(new DeleteStaleTemplateCaches([ - 'elementId' => array_keys($this->_deleteCachesIndex), - ])); - $this->_deleteCachesIndex = null; + // It's possible this is already null + if ($this->_deleteCachesIndex !== null) { + Craft::$app->getQueue()->push(new DeleteStaleTemplateCaches([ + 'elementId' => array_keys($this->_deleteCachesIndex), + ])); + + $this->_deleteCachesIndex = null; + } } /** diff --git a/src/services/Users.php b/src/services/Users.php index 3dd5c0da235..7ac5ecaaf00 100644 --- a/src/services/Users.php +++ b/src/services/Users.php @@ -15,12 +15,9 @@ use craft\errors\ImageException; use craft\errors\UserNotFoundException; use craft\errors\VolumeException; -use craft\events\UserActivateEvent; +use craft\events\UserEvent; use craft\events\UserAssignGroupEvent; use craft\events\UserGroupsAssignEvent; -use craft\events\UserSuspendEvent; -use craft\events\UserUnlockEvent; -use craft\events\UserUnsuspendEvent; use craft\helpers\Assets as AssetsHelper; use craft\helpers\DateTimeHelper; use craft\helpers\Db; @@ -48,19 +45,19 @@ class Users extends Component // ========================================================================= /** - * @event UserTokenEvent The event that is triggered before a user's email is verified. + * @event UserEvent The event that is triggered before a user's email is verified. */ const EVENT_BEFORE_VERIFY_EMAIL = 'beforeVerifyEmail'; /** - * @event UserTokenEvent The event that is triggered after a user's email is verified. + * @event UserEvent The event that is triggered after a user's email is verified. */ const EVENT_AFTER_VERIFY_EMAIL = 'afterVerifyEmail'; /** - * @event UserActivateEvent The event that is triggered before a user is activated. + * @event UserEvent The event that is triggered before a user is activated. * - * You may set [[UserActivateEvent::isValid]] to `false` to prevent the user from getting activated. + * You may set [[UserEvent::isValid]] to `false` to prevent the user from getting activated. */ const EVENT_BEFORE_ACTIVATE_USER = 'beforeActivateUser'; @@ -70,38 +67,43 @@ class Users extends Component const EVENT_AFTER_ACTIVATE_USER = 'afterActivateUser'; /** - * @event UserUnlockEvent The event that is triggered before a user is unlocked. + * @event UserEvent The event that is triggered after a user is locked. + */ + const EVENT_AFTER_LOCK_USER = 'afterLockUser'; + + /** + * @event UserEvent The event that is triggered before a user is unlocked. * - * You may set [[UserUnlockEvent::isValid]] to `false` to prevent the user from getting unlocked. + * You may set [[UserEvent::isValid]] to `false` to prevent the user from getting unlocked. */ const EVENT_BEFORE_UNLOCK_USER = 'beforeUnlockUser'; /** - * @event UserUnlockEvent The event that is triggered after a user is unlocked. + * @event UserEvent The event that is triggered after a user is unlocked. */ const EVENT_AFTER_UNLOCK_USER = 'afterUnlockUser'; /** - * @event UserSuspendEvent The event that is triggered before a user is suspended. + * @event UserEvent The event that is triggered before a user is suspended. * - * You may set [[UserSuspendEvent::isValid]] to `false` to prevent the user from getting suspended. + * You may set [[UserEvent::isValid]] to `false` to prevent the user from getting suspended. */ const EVENT_BEFORE_SUSPEND_USER = 'beforeSuspendUser'; /** - * @event UserSuspendEvent The event that is triggered after a user is suspended. + * @event UserEvent The event that is triggered after a user is suspended. */ const EVENT_AFTER_SUSPEND_USER = 'afterSuspendUser'; /** - * @event UserUnsuspendEvent The event that is triggered before a user is unsuspended. + * @event UserEvent The event that is triggered before a user is unsuspended. * - * You may set [[UserUnsuspendEvent::isValid]] to `false` to prevent the user from getting unsuspended. + * You may set [[UserEvent::isValid]] to `false` to prevent the user from getting unsuspended. */ const EVENT_BEFORE_UNSUSPEND_USER = 'beforeUnsuspendUser'; /** - * @event UserUnsuspendEvent The event that is triggered after a user is unsuspended. + * @event UserEvent The event that is triggered after a user is unsuspended. */ const EVENT_AFTER_UNSUSPEND_USER = 'afterUnsuspendUser'; @@ -481,13 +483,14 @@ public function handleInvalidLogin(User $user) // Was that one too many? $maxInvalidLogins = Craft::$app->getConfig()->getGeneral()->maxInvalidLogins; + $alreadyLocked = $user->locked; if ($maxInvalidLogins) { if ($this->_isUserInsideInvalidLoginWindow($userRecord)) { $userRecord->invalidLoginCount++; // Was that one bad password too many? - if ($userRecord->invalidLoginCount > $maxInvalidLogins) { + if ($userRecord->invalidLoginCount >= $maxInvalidLogins) { $userRecord->locked = true; $userRecord->invalidLoginCount = null; $userRecord->invalidLoginWindowStart = null; @@ -510,6 +513,13 @@ public function handleInvalidLogin(User $user) // Update the User model too $user->lastInvalidLoginDate = $now; + + if (!$alreadyLocked && $user->locked && $this->hasEventHandlers(self::EVENT_AFTER_LOCK_USER)) { + // Fire an 'afterLockUser' event + $this->trigger(self::EVENT_AFTER_LOCK_USER, new UserEvent([ + 'user' => $user, + ])); + } } /** @@ -523,7 +533,7 @@ public function handleInvalidLogin(User $user) public function activateUser(User $user): bool { // Fire a 'beforeActivateUser' event - $event = new UserActivateEvent([ + $event = new UserEvent([ 'user' => $user, ]); $this->trigger(self::EVENT_BEFORE_ACTIVATE_USER, $event); @@ -553,7 +563,7 @@ public function activateUser(User $user): bool // Fire an 'afterActivateUser' event if ($this->hasEventHandlers(self::EVENT_AFTER_ACTIVATE_USER)) { - $this->trigger(self::EVENT_AFTER_ACTIVATE_USER, new UserActivateEvent([ + $this->trigger(self::EVENT_AFTER_ACTIVATE_USER, new UserEvent([ 'user' => $user ])); } @@ -609,7 +619,7 @@ public function verifyEmailForUser(User $user): bool public function unlockUser(User $user): bool { // Fire a 'beforeUnlockUser' event - $event = new UserUnlockEvent([ + $event = new UserEvent([ 'user' => $user, ]); $this->trigger(self::EVENT_BEFORE_UNLOCK_USER, $event); @@ -640,7 +650,7 @@ public function unlockUser(User $user): bool // Fire an 'afterUnlockUser' event if ($this->hasEventHandlers(self::EVENT_AFTER_UNLOCK_USER)) { - $this->trigger(self::EVENT_AFTER_UNLOCK_USER, new UserUnlockEvent([ + $this->trigger(self::EVENT_AFTER_UNLOCK_USER, new UserEvent([ 'user' => $user ])); } @@ -659,7 +669,7 @@ public function unlockUser(User $user): bool public function suspendUser(User $user): bool { // Fire a 'beforeSuspendUser' event - $event = new UserSuspendEvent([ + $event = new UserEvent([ 'user' => $user, ]); $this->trigger(self::EVENT_BEFORE_SUSPEND_USER, $event); @@ -683,7 +693,7 @@ public function suspendUser(User $user): bool // Fire an 'afterSuspendUser' event if ($this->hasEventHandlers(self::EVENT_AFTER_SUSPEND_USER)) { - $this->trigger(self::EVENT_AFTER_SUSPEND_USER, new UserSuspendEvent([ + $this->trigger(self::EVENT_AFTER_SUSPEND_USER, new UserEvent([ 'user' => $user ])); } @@ -702,7 +712,7 @@ public function suspendUser(User $user): bool public function unsuspendUser(User $user): bool { // Fire a 'beforeUnsuspendUser' event - $event = new UserUnsuspendEvent([ + $event = new UserEvent([ 'user' => $user, ]); $this->trigger(self::EVENT_BEFORE_UNSUSPEND_USER, $event); @@ -730,7 +740,7 @@ public function unsuspendUser(User $user): bool // Fire an 'afterUnsuspendUser' event if ($this->hasEventHandlers(self::EVENT_AFTER_UNSUSPEND_USER)) { - $this->trigger(self::EVENT_AFTER_UNSUSPEND_USER, new UserUnsuspendEvent([ + $this->trigger(self::EVENT_AFTER_UNSUSPEND_USER, new UserEvent([ 'user' => $user ])); } diff --git a/src/templates/_components/fieldtypes/Matrix/input.html b/src/templates/_components/fieldtypes/Matrix/input.html index 42af8b060f5..bf8bbfa41cb 100644 --- a/src/templates/_components/fieldtypes/Matrix/input.html +++ b/src/templates/_components/fieldtypes/Matrix/input.html @@ -10,7 +10,7 @@ {% set blockId = 'new'~totalNewBlocks %} {% endif %} -
+
{% if not static %} diff --git a/src/templates/_components/fieldtypes/Matrix/settings.html b/src/templates/_components/fieldtypes/Matrix/settings.html index b3054195757..1bcd7c074a5 100644 --- a/src/templates/_components/fieldtypes/Matrix/settings.html +++ b/src/templates/_components/fieldtypes/Matrix/settings.html @@ -111,7 +111,7 @@
{{ "Field Settings"|t('app') }}
{% if craft.app.getIsMultiSite() %} {% set translationMethods = field.supportedTranslationMethods %} - {% if translationMethods|length > 0 %} + {% if translationMethods|length > 1 %}
{{ forms.selectField({ label: "Translation Method"|t('app'), diff --git a/src/templates/_includes/field.html b/src/templates/_includes/field.html index c37244252cb..103ae459422 100644 --- a/src/templates/_includes/field.html +++ b/src/templates/_includes/field.html @@ -16,7 +16,7 @@ {% if instructions or input %} {% include "_includes/forms/field" with { label: field.name|t('site')|e, - transslatable: translatable, + translatable: translatable, siteId: siteId, required: (not static ? required : false), instructions: instructions|e, diff --git a/src/templates/_includes/forms/checkboxGroup.html b/src/templates/_includes/forms/checkboxGroup.html index 1736919e906..5c463b14fea 100644 --- a/src/templates/_includes/forms/checkboxGroup.html +++ b/src/templates/_includes/forms/checkboxGroup.html @@ -6,17 +6,19 @@ {%- set values = (values is defined ? values : []) %} {%- set name = (name is defined and name ? name~'[]' : null) %} -{%- for key, option in options %} - {%- set optionLabel = (option.label is defined ? option.label : option) %} - {%- set optionValue = (option.value is defined ? option.value : key) -%} -
- {% include "_includes/forms/checkbox" with { - label: optionLabel, - id: (loop.first and id is defined ? id : null), - name: name, - value: optionValue, - checked: (optionValue in values), - autofocus: (autofocus is defined and autofocus and loop.first and not craft.app.request.isMobileBrowser(true)) - } only %} -
-{%- endfor %} +
+ {%- for key, option in options %} + {%- set optionLabel = (option.label is defined ? option.label : option) %} + {%- set optionValue = (option.value is defined ? option.value : key) -%} +
+ {% include "_includes/forms/checkbox" with { + label: optionLabel, + id: (loop.first and id is defined ? id : null), + name: name, + value: optionValue, + checked: (optionValue in values), + autofocus: (autofocus is defined and autofocus and loop.first and not craft.app.request.isMobileBrowser(true)) + } only %} +
+ {%- endfor %} +
diff --git a/src/templates/_includes/forms/radioGroup.html b/src/templates/_includes/forms/radioGroup.html index 8886d94697a..3f4c8937ae9 100644 --- a/src/templates/_includes/forms/radioGroup.html +++ b/src/templates/_includes/forms/radioGroup.html @@ -1,17 +1,19 @@ {%- set options = (options is defined ? options : []) %} {%- set value = (value is defined ? value : null) %} -{%- for key, option in options %} - {%- set optionLabel = (option.label is defined ? option.label : option) %} - {%- set optionValue = (option.value is defined ? option.value : key) -%} -
- {% include "_includes/forms/radio" with { - label: optionLabel, - id: (id is defined and loop.first ? id : null), - name: (name is defined ? name : null), - value: optionValue, - checked: (optionValue == value), - autofocus: (autofocus is defined and autofocus and loop.first and not craft.app.request.isMobileBrowser(true)) - } only %} -
-{% endfor %} +
+ {%- for key, option in options %} + {%- set optionLabel = (option.label is defined ? option.label : option) %} + {%- set optionValue = (option.value is defined ? option.value : key) -%} +
+ {% include "_includes/forms/radio" with { + label: optionLabel, + id: (id is defined and loop.first ? id : null), + name: (name is defined ? name : null), + value: optionValue, + checked: (optionValue == value), + autofocus: (autofocus is defined and autofocus and loop.first and not craft.app.request.isMobileBrowser(true)) + } only %} +
+ {% endfor %} +
diff --git a/src/validators/UniqueValidator.php b/src/validators/UniqueValidator.php index 643ae02dad3..69e5f76f5fb 100644 --- a/src/validators/UniqueValidator.php +++ b/src/validators/UniqueValidator.php @@ -7,7 +7,7 @@ namespace craft\validators; -use craft\helpers\ArrayHelper; +use craft\helpers\StringHelper; use yii\base\Model; use yii\db\ActiveRecord; use yii\validators\UniqueValidator as YiiUniqueValidator; @@ -51,7 +51,11 @@ public function validateAttribute($model, $attribute) // Set the primary key values on the record, if they're set $pks = $record::primaryKey(); - $pkMap = $this->pk !== null ? ArrayHelper::toArray($this->pk) : $pks; + if ($this->pk !== null) { + $pkMap = is_string($this->pk) ? StringHelper::split($this->pk) : $this->pk; + } else { + $pkMap = $pks; + } $isNewRecord = true; foreach ($pkMap as $k => $v) { diff --git a/src/volumes/MissingVolume.php b/src/volumes/MissingVolume.php index dcec9217a5a..968bfe12007 100644 --- a/src/volumes/MissingVolume.php +++ b/src/volumes/MissingVolume.php @@ -10,6 +10,7 @@ use craft\base\MissingComponentInterface; use craft\base\MissingComponentTrait; use craft\base\Volume; +use yii\base\NotSupportedException; /** * MissingVolume represents a volume with an invalid class. @@ -30,16 +31,121 @@ class MissingVolume extends Volume implements MissingComponentInterface /** * @inheritdoc */ - protected function createAdapter() + public function getRootUrl() { - return null; + return false; } /** * @inheritdoc */ - public function getRootUrl() + public function getFileList(string $directory, bool $recursive): array + { + throw new NotSupportedException('getFileList() is not implemented.'); + } + + /** + * @inheritdoc + */ + public function getFileMetadata(string $uri): array + { + throw new NotSupportedException('getFileMetadata() is not implemented.'); + } + + /** + * @inheritdoc + */ + public function createFileByStream(string $path, $stream, array $config) + { + throw new NotSupportedException('createFileByStream() is not implemented.'); + } + + /** + * @inheritdoc + */ + public function updateFileByStream(string $path, $stream, array $config) + { + throw new NotSupportedException('updateFileByStream() is not implemented.'); + } + + /** + * @inheritdoc + */ + public function fileExists(string $path): bool + { + throw new NotSupportedException('fileExists() is not implemented.'); + } + + /** + * @inheritdoc + */ + public function deleteFile(string $path) + { + throw new NotSupportedException('deleteFile() is not implemented.'); + } + + /** + * @inheritdoc + */ + public function renameFile(string $path, string $newPath) + { + throw new NotSupportedException('renameFile() is not implemented.'); + } + + /** + * @inheritdoc + */ + public function copyFile(string $path, string $newPath) + { + throw new NotSupportedException('copyFile() is not implemented.'); + } + + /** + * @inheritdoc + */ + public function saveFileLocally(string $uriPath, string $targetPath): int + { + throw new NotSupportedException('saveFileLocally() is not implemented.'); + } + + /** + * @inheritdoc + */ + public function getFileStream(string $uriPath) + { + throw new NotSupportedException('getFileStream() is not implemented.'); + } + + /** + * @inheritdoc + */ + public function folderExists(string $path): bool + { + throw new NotSupportedException('folderExists() is not implemented.'); + } + + /** + * + * @inheritdoc + */ + public function createDir(string $path) + { + throw new NotSupportedException('createDir() is not implemented.'); + } + + /** + * @inheritdoc + */ + public function deleteDir(string $path) + { + throw new NotSupportedException('deleteDir() is not implemented.'); + } + + /** + * @inheritdoc + */ + public function renameDir(string $path, string $newName) { - return null; + throw new NotSupportedException('renameDir() is not implemented.'); } } diff --git a/src/web/User.php b/src/web/User.php index 7018da0c190..1b86d9f3175 100644 --- a/src/web/User.php +++ b/src/web/User.php @@ -49,15 +49,6 @@ class User extends \yii\web\User // Public Methods // ========================================================================= - /** - * Initializes the application component. - */ - public function init() - { - parent::init(); - $this->_setStaticIdentity(); - } - // Authentication // ------------------------------------------------------------------------- @@ -370,7 +361,20 @@ protected function renewAuthStatus() { // Only renew if the request meets our user agent and IP requirements if (Craft::$app->getIsInstalled() && $this->_validateUserAgentAndIp()) { - parent::renewAuthStatus(); + // Prevent the user session from getting extended? + $request = Craft::$app->getRequest(); + if ($this->authTimeout !== null && $request->getIsCpRequest() && $request->getParam('dontExtendSession')) { + $this->absoluteAuthTimeout = $this->authTimeout; + $this->authTimeout = null; + $absoluteAuthTimeoutParam = $this->absoluteAuthTimeoutParam; + $this->absoluteAuthTimeoutParam = $this->authTimeoutParam; + parent::renewAuthStatus(); + $this->authTimeout = $this->absoluteAuthTimeout; + $this->absoluteAuthTimeout = null; + $this->absoluteAuthTimeoutParam = $absoluteAuthTimeoutParam; + } else { + parent::renewAuthStatus(); + } } } @@ -431,40 +435,6 @@ protected function afterLogout($identity) // Private Methods // ========================================================================= - /** - * Statically sets the identity in the event that this request should not be extending it. - * - * @return void - */ - private function _setStaticIdentity() - { - // Is the request specifying that the session should not be extended? - $request = Craft::$app->getRequest(); - - if ( - $request->getIsGet() && - $request->getIsCpRequest() && - $request->getParam('dontExtendSession') - ) { - // Prevent getIdentity() from automatically fetching the identity from session - $this->enableSession = false; - - // Code adapted from \yii\web\User::renewAuthStatus() - $session = Craft::$app->getSession(); - $id = $session->getHasSessionId() || $session->getIsActive() ? $session->get($this->idParam) : null; - - if ($id === null) { - $identity = null; - } else { - /** @var $class IdentityInterface */ - $class = $this->identityClass; - $identity = $class::findIdentity($id); - } - - $this->setIdentity($identity); - } - } - /** * Validates that the request has a user agent and IP associated with it, * if the 'requireUserAgentAndIpForSession' config setting is enabled. diff --git a/src/web/assets/cp/dist/js/Craft.js b/src/web/assets/cp/dist/js/Craft.js index e9eb3c1323a..2cbc100bbec 100644 --- a/src/web/assets/cp/dist/js/Craft.js +++ b/src/web/assets/cp/dist/js/Craft.js @@ -1,4 +1,4 @@ -/*! - 2017-08-14 */ +/*! - 2017-09-06 */ (function($){ /** global: Craft */ @@ -2516,7 +2516,7 @@ Craft.BaseElementIndex = Garnish.Base.extend( afterAction: function(action, params) { // There may be a new background task that needs to be run - Craft.cp.runPendingTasks(); + Craft.cp.runQueue(); this.onAfterAction(action, params); }, @@ -6147,7 +6147,7 @@ Craft.AssetIndex = Craft.BaseElementIndex.extend( responseArray.push(data); // If assets were just merged we should get the reference tags updated right away - Craft.cp.runPendingTasks(); + Craft.cp.runQueue(); } if (responseArray.length >= parameterArray.length) { @@ -6644,13 +6644,12 @@ Craft.AuthManager = Garnish.Base.extend( dataType: 'json', complete: $.proxy(function(jqXHR, textStatus) { if (textStatus === 'success') { - this.updateRemainingSessionTime(jqXHR.responseJSON.timeout); - - this.submitLoginIfLoggedOut = false; - if (typeof jqXHR.responseJSON.csrfTokenValue !== 'undefined' && typeof Craft.csrfTokenValue !== 'undefined') { Craft.csrfTokenValue = jqXHR.responseJSON.csrfTokenValue; } + + this.updateRemainingSessionTime(jqXHR.responseJSON.timeout); + this.submitLoginIfLoggedOut = false; } else { this.updateRemainingSessionTime(-1); @@ -8826,7 +8825,7 @@ Craft.CP = Garnish.Base.extend( }, _trackJobProgressInternal: function() { - Craft.queueActionRequest('queue/get-job-info', $.proxy(function(response, textStatus) { + Craft.queueActionRequest('queue/get-job-info?dontExtendSession=1', $.proxy(function(response, textStatus) { if (textStatus === 'success') { this.trackJobProgressTimeout = null; this.setJobInfo(response, true); @@ -8895,7 +8894,7 @@ Craft.CP = Garnish.Base.extend( }, updateJobIcon: function(animate) { - if (!this.enableQueue) { + if (!this.enableQueue || !this.$nav.length) { return; } @@ -14718,7 +14717,7 @@ Craft.StructureTableSorter = Garnish.DragSort.extend({ } // See if we should run any pending tasks - Craft.cp.runPendingTasks(); + Craft.cp.runQueue(); } }, this)); } diff --git a/src/web/assets/cp/dist/js/Craft.min.js b/src/web/assets/cp/dist/js/Craft.min.js index 678c4237172..87fa7658a1a 100644 --- a/src/web/assets/cp/dist/js/Craft.min.js +++ b/src/web/assets/cp/dist/js/Craft.min.js @@ -1,2 +1,2 @@ -!function(t){t.extend(Craft,{navHeight:48,asciiCharMap:{a:["à","á","ả","ã","ạ","ă","ắ","ằ","ẳ","ẵ","ặ","â","ấ","ầ","ẩ","ẫ","ậ","ä","ā","ą","å","α","ά","ἀ","ἁ","ἂ","ἃ","ἄ","ἅ","ἆ","ἇ","ᾀ","ᾁ","ᾂ","ᾃ","ᾄ","ᾅ","ᾆ","ᾇ","ὰ","ά","ᾰ","ᾱ","ᾲ","ᾳ","ᾴ","ᾶ","ᾷ","а","أ"],b:["б","β","Ъ","Ь","ب"],c:["ç","ć","č","ĉ","ċ"],d:["ď","ð","đ","ƌ","ȡ","ɖ","ɗ","ᵭ","ᶁ","ᶑ","д","δ","د","ض"],e:["é","è","ẻ","ẽ","ẹ","ê","ế","ề","ể","ễ","ệ","ë","ē","ę","ě","ĕ","ė","ε","έ","ἐ","ἑ","ἒ","ἓ","ἔ","ἕ","ὲ","έ","е","ё","э","є","ə"],f:["ф","φ","ف"],g:["ĝ","ğ","ġ","ģ","г","ґ","γ","ج"],h:["ĥ","ħ","η","ή","ح","ه"],i:["í","ì","ỉ","ĩ","ị","î","ï","ī","ĭ","į","ı","ι","ί","ϊ","ΐ","ἰ","ἱ","ἲ","ἳ","ἴ","ἵ","ἶ","ἷ","ὶ","ί","ῐ","ῑ","ῒ","ΐ","ῖ","ῗ","і","ї","и"],j:["ĵ","ј","Ј"],k:["ķ","ĸ","к","κ","Ķ","ق","ك"],l:["ł","ľ","ĺ","ļ","ŀ","л","λ","ل"],m:["м","μ","م"],n:["ñ","ń","ň","ņ","ʼn","ŋ","ν","н","ن"],o:["ó","ò","ỏ","õ","ọ","ô","ố","ồ","ổ","ỗ","ộ","ơ","ớ","ờ","ở","ỡ","ợ","ø","ō","ő","ŏ","ο","ὀ","ὁ","ὂ","ὃ","ὄ","ὅ","ὸ","ό","ö","о","و","θ"],p:["п","π"],r:["ŕ","ř","ŗ","р","ρ","ر"],s:["ś","š","ş","с","σ","ș","ς","س","ص"],t:["ť","ţ","т","τ","ț","ت","ط"],u:["ú","ù","ủ","ũ","ụ","ư","ứ","ừ","ử","ữ","ự","ü","û","ū","ů","ű","ŭ","ų","µ","у"],v:["в"],w:["ŵ","ω","ώ"],x:["χ"],y:["ý","ỳ","ỷ","ỹ","ỵ","ÿ","ŷ","й","ы","υ","ϋ","ύ","ΰ","ي"],z:["ź","ž","ż","з","ζ","ز"],aa:["ع"],ae:["æ"],ch:["ч"],dj:["ђ","đ"],dz:["џ"],gh:["غ"],kh:["х","خ"],lj:["љ"],nj:["њ"],oe:["œ"],ps:["ψ"],sh:["ш"],shch:["щ"],ss:["ß"],th:["þ","ث","ذ","ظ"],ts:["ц"],ya:["я"],yu:["ю"],zh:["ж"],"(c)":["©"],A:["Á","À","Ả","Ã","Ạ","Ă","Ắ","Ằ","Ẳ","Ẵ","Ặ","Â","Ấ","Ầ","Ẩ","Ẫ","Ậ","Ä","Å","Ā","Ą","Α","Ά","Ἀ","Ἁ","Ἂ","Ἃ","Ἄ","Ἅ","Ἆ","Ἇ","ᾈ","ᾉ","ᾊ","ᾋ","ᾌ","ᾍ","ᾎ","ᾏ","Ᾰ","Ᾱ","Ὰ","Ά","ᾼ","А"],B:["Б","Β"],C:["Ć","Č","Ĉ","Ċ"],D:["Ď","Ð","Đ","Ɖ","Ɗ","Ƌ","ᴅ","ᴆ","Д","Δ"],E:["É","È","Ẻ","Ẽ","Ẹ","Ê","Ế","Ề","Ể","Ễ","Ệ","Ë","Ē","Ę","Ě","Ĕ","Ė","Ε","Έ","Ἐ","Ἑ","Ἒ","Ἓ","Ἔ","Ἕ","Έ","Ὲ","Е","Ё","Э","Є","Ə"],F:["Ф","Φ"],G:["Ğ","Ġ","Ģ","Г","Ґ","Γ"],H:["Η","Ή"],I:["Í","Ì","Ỉ","Ĩ","Ị","Î","Ï","Ī","Ĭ","Į","İ","Ι","Ί","Ϊ","Ἰ","Ἱ","Ἳ","Ἴ","Ἵ","Ἶ","Ἷ","Ῐ","Ῑ","Ὶ","Ί","И","І","Ї"],K:["К","Κ"],L:["Ĺ","Ł","Л","Λ","Ļ"],M:["М","Μ"],N:["Ń","Ñ","Ň","Ņ","Ŋ","Н","Ν"],O:["Ó","Ò","Ỏ","Õ","Ọ","Ô","Ố","Ồ","Ổ","Ỗ","Ộ","Ơ","Ớ","Ờ","Ở","Ỡ","Ợ","Ö","Ø","Ō","Ő","Ŏ","Ο","Ό","Ὀ","Ὁ","Ὂ","Ὃ","Ὄ","Ὅ","Ὸ","Ό","О","Θ","Ө"],P:["П","Π"],R:["Ř","Ŕ","Р","Ρ"],S:["Ş","Ŝ","Ș","Š","Ś","С","Σ"],T:["Ť","Ţ","Ŧ","Ț","Т","Τ"],U:["Ú","Ù","Ủ","Ũ","Ụ","Ư","Ứ","Ừ","Ử","Ữ","Ự","Û","Ü","Ū","Ů","Ű","Ŭ","Ų","У"],V:["В"],W:["Ω","Ώ"],X:["Χ"],Y:["Ý","Ỳ","Ỷ","Ỹ","Ỵ","Ÿ","Ῠ","Ῡ","Ὺ","Ύ","Ы","Й","Υ","Ϋ"],Z:["Ź","Ž","Ż","З","Ζ"],AE:["Æ"],CH:["Ч"],DJ:["Ђ"],DZ:["Џ"],KH:["Х"],LJ:["Љ"],NJ:["Њ"],PS:["Ψ"],SH:["Ш"],SHCH:["Щ"],SS:["ẞ"],TH:["Þ"],TS:["Ц"],YA:["Я"],YU:["Ю"],ZH:["Ж"]," ":[" "," "," "," "," "," "," "," "," "," "," "," "," "," "," "]},t:function(t,e,s){if(void 0!==Craft.translations[t]&&void 0!==Craft.translations[t][e]&&(e=Craft.translations[t][e]),s)for(var i in s)s.hasOwnProperty(i)&&(e=e.replace("{"+i+"}",s[i]));return e},formatDate:function(e){return"object"!=typeof e&&(e=new Date(e)),t.datepicker.formatDate(Craft.datepickerOptions.dateFormat,e)},escapeHtml:function(e){return t("
").text(e).html()},getText:function(e){return t("
").html(e).text()},encodeUriComponent:function(t){t=encodeURIComponent(t);var e={"!":"%21","*":"%2A","'":"%27","(":"%28",")":"%29"};for(var s in e){var i=new RegExp("\\"+s,"g");t=t.replace(i,e[s])}return t},selectFullValue:function(e){var s=t(e),i=s.val();if(void 0!==s[0].setSelectionRange){var n=2*i.length;s[0].setSelectionRange(0,n)}else s.val(i)},formatInputId:function(t){return this.rtrim(t.replace(/[\[\]\\]+/g,"-"),"-")},getUrl:function(e,s,i){if("string"!=typeof e&&(e=""),-1!==e.search("://")||"//"===e.substr(0,2))return e;e=Craft.trim(e,"/");var n="";if(t.isPlainObject(s)){var a=[];for(var r in s)if(s.hasOwnProperty(r)){var o=s[r];"#"===r?n=o:null!==o&&""!==o&&a.push(r+"="+o)}s=a}s=Garnish.isArray(s)?s.join("&"):Craft.trim(s,"&?");var l=e.indexOf("?");-1!==l&&(s=e.substr(l+1)+(s?"&"+s:""),e=e.substr(0,l));var h;if(i){if(h=i,e){var d=h.match(/[&\?]p=[^&]+/);d&&(h=h.replace(d[0],d[0]+"/"+e),e="")}}else h=Craft.baseUrl;if(-1!==(l=h.indexOf("?"))&&(s=h.substr(l+1)+(s?"&"+s:""),h=h.substr(0,l)),!Craft.omitScriptNameInUrls&&e)if(Craft.usePathInfo)-1===h.search(Craft.scriptName)&&(h=Craft.rtrim(h,"/")+"/"+Craft.scriptName);else{if(s&&"p="===s.substr(0,2)){var c,u=s.indexOf("&");-1!==u?(c=s.substring(2,u),s=s.substr(u+1)):(c=s.substr(2),s=null),e=(c=Craft.rtrim(c))+(e?"/"+e:"")}s="p="+e+(s?"&"+s:""),e=null}return e&&(h=Craft.rtrim(h,"/")+"/"+e),s&&(h+="?"+s),n&&(h+="#"+n),h},getCpUrl:function(t,e){return this.getUrl(t,e,Craft.baseCpUrl)},getSiteUrl:function(t,e){return this.getUrl(t,e,Craft.baseSiteUrl)},getResourceUrl:function(t,e){return Craft.getUrl(t,e,Craft.resourceUrl)},getActionUrl:function(t,e){return Craft.getUrl(t,e,Craft.actionUrl)},redirectTo:function(t){document.location.href=this.getUrl(t)},getCsrfInput:function(){return Craft.csrfTokenName?'':""},postActionRequest:function(e,s,i,n){"function"==typeof s&&(n=i,i=s,s={});var a={};Craft.csrfTokenValue&&Craft.csrfTokenName&&(a["X-CSRF-Token"]=Craft.csrfTokenValue);var r=t.ajax(t.extend({url:Craft.getActionUrl(e),type:"POST",dataType:"json",headers:a,data:s,success:i,error:function(t,e){i&&i(null,e,t)},complete:function(t,e){"success"!==e&&(void 0!==Craft.cp?Craft.cp.displayError():alert(Craft.t("app","An unknown error occurred.")))}},n));return n&&"function"==typeof n.send&&n.send(r),r},_waitingOnAjax:!1,_ajaxQueue:[],queueActionRequest:function(t,e,s,i){"function"==typeof e&&(i=s,s=e,e=void 0),Craft._ajaxQueue.push([t,e,s,i]),Craft._waitingOnAjax||Craft._postNextActionRequestInQueue()},_postNextActionRequestInQueue:function(){Craft._waitingOnAjax=!0;var t=Craft._ajaxQueue.shift();Craft.postActionRequest(t[0],t[1],function(e,s,i){t[2]&&"function"==typeof t[2]&&t[2](e,s,i),Craft._ajaxQueue.length?Craft._postNextActionRequestInQueue():Craft._waitingOnAjax=!1},t[3])},stringToArray:function(e){if("string"!=typeof e)return e;for(var s=e.split(","),i=0;i=32&&i<128)e+=n;else for(var a in Craft.asciiCharMap)if(Craft.asciiCharMap.hasOwnProperty(a))for(var r=0;r]*href="(?:'+i.join("|")+')".*?><\/script>',"g");e=e.replace(r,"")}t("head").append(e)}},appendFootHtml:function(e){if(e){var s=t("script[src]");if(s.length){for(var i=[],n=0;n]*src="(?:'+i.join("|")+')".*?><\/script>',"g");e=e.replace(r,"")}Garnish.$bod.append(e)}},initUiElements:function(e){t(".grid",e).grid(),t(".pane",e).pane(),t(".info",e).infoicon(),t(".checkbox-select",e).checkboxselect(),t(".fieldtoggle",e).fieldtoggle(),t(".lightswitch",e).lightswitch(),t(".nicetext",e).nicetext(),t(".pill",e).pill(),t(".formsubmit",e).formsubmit(),t(".menubtn",e).menubtn()},_elementIndexClasses:{},_elementSelectorModalClasses:{},_elementEditorClasses:{},registerElementIndexClass:function(t,e){if(void 0!==this._elementIndexClasses[t])throw"An element index class has already been registered for the element type “"+t+"”.";this._elementIndexClasses[t]=e},registerElementSelectorModalClass:function(t,e){if(void 0!==this._elementSelectorModalClasses[t])throw"An element selector modal class has already been registered for the element type “"+t+"”.";this._elementSelectorModalClasses[t]=e},registerElementEditorClass:function(t,e){if(void 0!==this._elementEditorClasses[t])throw"An element editor class has already been registered for the element type “"+t+"”.";this._elementEditorClasses[t]=e},createElementIndex:function(t,e,s){return new(void 0!==this._elementIndexClasses[t]?this._elementIndexClasses[t]:Craft.BaseElementIndex)(t,e,s)},createElementSelectorModal:function(t,e){return new(void 0!==this._elementSelectorModalClasses[t]?this._elementSelectorModalClasses[t]:Craft.BaseElementSelectorModal)(t,e)},createElementEditor:function(t,e,s){return new(void 0!==this._elementEditorClasses[t]?this._elementEditorClasses[t]:Craft.BaseElementEditor)(e,s)},getLocalStorage:function(t,e){return t="Craft-"+Craft.systemUid+"."+t,"undefined"!=typeof localStorage&&void 0!==localStorage[t]?JSON.parse(localStorage[t]):e},setLocalStorage:function(t,e){if("undefined"!=typeof localStorage){t="Craft-"+Craft.systemUid+"."+t;try{localStorage[t]=JSON.stringify(e)}catch(t){}}},getElementInfo:function(e){var s=t(e);return s.hasClass("element")||(s=s.find(".element:first")),{id:s.data("id"),siteId:s.data("site-id"),label:s.data("label"),status:s.data("status"),url:s.data("url"),hasThumb:s.hasClass("hasthumb"),$element:s}},setElementSize:function(e,s){var i=t(e);if("small"!==s&&"large"!==s&&(s="small"),!i.hasClass(s)){var n="small"===s?"large":"small";if(i.addClass(s).removeClass(n),i.hasClass("hasthumb")){var a=i.find("> .elementthumb > img"),r=t("",{sizes:("small"===s?"30":"100")+"px",srcset:a.attr("srcset")||a.attr("data-pfsrcset")});a.replaceWith(r),picturefill({elements:[r[0]]})}}}}),t.extend(t.fn,{animateLeft:function(t,e,s,i){return"ltr"===Craft.orientation?this.velocity({left:t},e,s,i):this.velocity({right:t},e,s,i)},animateRight:function(t,e,s,i){return"ltr"===Craft.orientation?this.velocity({right:t},e,s,i):this.velocity({left:t},e,s,i)},disable:function(){return this.each(function(){var e=t(this);e.addClass("disabled"),e.data("activatable")&&e.removeAttr("tabindex")})},enable:function(){return this.each(function(){var e=t(this);e.removeClass("disabled"),e.data("activatable")&&e.attr("tabindex","0")})},grid:function(){return this.each(function(){var e=t(this),s={};e.data("item-selector")&&(s.itemSelector=e.data("item-selector")),e.data("cols")&&(s.cols=parseInt(e.data("cols"))),e.data("max-cols")&&(s.maxCols=parseInt(e.data("max-cols"))),e.data("min-col-width")&&(s.minColWidth=parseInt(e.data("min-col-width"))),e.data("mode")&&(s.mode=e.data("mode")),e.data("fill-mode")&&(s.fillMode=e.data("fill-mode")),e.data("col-class")&&(s.colClass=e.data("col-class")),e.data("snap-to-grid")&&(s.snapToGrid=!!e.data("snap-to-grid")),new Craft.Grid(this,s)})},infoicon:function(){return this.each(function(){new Craft.InfoIcon(this)})},pane:function(){return this.each(function(){t.data(this,"pane")||new Craft.Pane(this)})},checkboxselect:function(){return this.each(function(){t.data(this,"checkboxselect")||new Garnish.CheckboxSelect(this)})},fieldtoggle:function(){return this.each(function(){t.data(this,"fieldtoggle")||new Craft.FieldToggle(this)})},lightswitch:function(e,s,i){return"settings"===e?("string"==typeof s?(e={})[s]=i:e=s,this.each(function(){var s=t.data(this,"lightswitch");s&&s.setSettings(e)})):(t.isPlainObject(e)||(e={}),this.each(function(){var s=t.extend({},e);Garnish.hasAttr(this,"data-value")&&(s.value=t(this).attr("data-value")),t.data(this,"lightswitch")||new Craft.LightSwitch(this,s)}))},nicetext:function(){return this.each(function(){t.data(this,"nicetext")||new Garnish.NiceText(this)})},pill:function(){return this.each(function(){t.data(this,"pill")||new Garnish.Pill(this)})},formsubmit:function(){this.on("click",function(e){var s=t(e.currentTarget);if(!s.attr("data-confirm")||confirm(s.attr("data-confirm"))){var i;i=s.data("menu")?s.data("menu").$anchor.closest("form"):s.closest("form"),s.attr("data-action")&&t('').val(s.attr("data-action")).appendTo(i),s.attr("data-redirect")&&t('').val(s.attr("data-redirect")).appendTo(i),s.attr("data-param")&&t('').attr({name:s.attr("data-param"),value:s.attr("data-value")}).appendTo(i),i.submit()}})},menubtn:function(){return this.each(function(){var e=t(this);if(!e.data("menubtn")&&e.next().hasClass("menu")){var s={};e.data("menu-anchor")&&(s.menuAnchor=e.data("menu-anchor")),new Garnish.MenuBtn(e,s)}})}}),Garnish.$doc.ready(function(){Craft.initUiElements()}),Craft.BaseElementEditor=Garnish.Base.extend({$element:null,elementId:null,siteId:null,$form:null,$fieldsContainer:null,$cancelBtn:null,$saveBtn:null,$spinner:null,$languageSelect:null,$siteSpinner:null,hud:null,init:function(e,s){void 0===s&&t.isPlainObject(e)&&(s=e,e=null),this.$element=t(e),this.setSettings(s,Craft.BaseElementEditor.defaults),this.loadHud()},setElementAttribute:function(t,e){this.settings.attributes||(this.settings.attributes={}),null===e?delete this.settings.attributes[t]:this.settings.attributes[t]=e},getBaseData:function(){var e=t.extend({},this.settings.params);return this.settings.siteId?e.siteId=this.settings.siteId:this.$element&&this.$element.data("site-id")&&(e.siteId=this.$element.data("site-id")),this.settings.elementId?e.elementId=this.settings.elementId:this.$element&&this.$element.data("id")&&(e.elementId=this.$element.data("id")),this.settings.elementType&&(e.elementType=this.settings.elementType),this.settings.attributes&&(e.attributes=this.settings.attributes),e},loadHud:function(){this.onBeginLoading();var e=this.getBaseData();e.includeSites=this.settings.showSiteSwitcher,Craft.postActionRequest("elements/get-editor-html",e,t.proxy(this,"showHud"))},showHud:function(e,s){if(this.onEndLoading(),"success"===s){var i=t();if(e.sites){var n=t('
'),a=t('
').appendTo(n);this.$siteSelect=t("').appendTo(h),this.$spinner=t('