diff --git a/CHANGELOG.md b/CHANGELOG.md
index 46d7e9fd37..4d8c92c895 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
# Release Notes
+## 5.0.0-alpha.6 (2024-04-26)
+
+### What's changed
+- Add tokens to eloquent cli install. [#9962](https://github.com/statamic/cms/issues/9962) by @ryanmitchell
+- Token class changes [#9964](https://github.com/statamic/cms/issues/9964) by @jasonvarga
+- Revert caching entry to property on Page instances [#9958](https://github.com/statamic/cms/issues/9958) by @jasonvarga
+- Changes to `User` role methods [#9921](https://github.com/statamic/cms/issues/9921) by @duncanmcclean
+
+
+
## 5.0.0-alpha.5 (2024-04-22)
### What's changed
diff --git a/resources/lang/de.json b/resources/lang/de.json
index bd04c0a309..2ceccb03ce 100644
--- a/resources/lang/de.json
+++ b/resources/lang/de.json
@@ -948,6 +948,7 @@
"Unable to change password": "Passwort kann nicht geändert werden.",
"Unable to delete filter preset": "Filtervoreinstellung kann nicht gelöscht werden.",
"Unable to delete view": "Ansicht kann nicht gelöscht werden.",
+ "Unable to rename view": "Ansicht kann nicht umbenannt werden.",
"Unable to save changes": "Änderungen können nicht gespeichert werden.",
"Unable to save column preferences.": "Spaltenvoreinstellungen können nicht gespeichert werden.",
"Unable to save favorite": "Favorit kann nicht gespeichert werden.",
@@ -1000,6 +1001,7 @@
"View deleted": "Ansicht gelöscht",
"View History": "Verlauf anzeigen",
"View on Marketplace": "Auf Marktplatz anzeigen",
+ "View renamed": "Ansicht umbenannt",
"View saved": "Ansicht gespeichert",
"View Site": "Website anzeigen",
"View Useful Links": "Nützliche Links anzeigen",
@@ -1032,6 +1034,7 @@
"You are not authorized to view this collection.": "Du bist nicht berechtigt, diese Sammlung anzuzeigen.",
"You are now impersonating": "Du gibst dich jetzt aus als",
"You can't do this while logged in": "Dies ist nicht möglich, wenn du eingeloggt bist.",
+ "You're already editing this item.": "Du bearbeitest diesen Eintrag bereits.",
"Your Favorites": "Meine Favoriten",
"Your working copy will be replaced by the contents of this revision.": "Deine Arbeitskopie wird durch den Inhalt dieser Überarbeitung ersetzt."
}
diff --git a/resources/lang/de/messages.php b/resources/lang/de/messages.php
index bc6a86f3df..d988d6051f 100644
--- a/resources/lang/de/messages.php
+++ b/resources/lang/de/messages.php
@@ -100,6 +100,7 @@
'fieldset_link_fields_prefix_instructions' => 'Jedem Feld im verknüpften Fieldset wird dieses Präfix vorangestellt. Nützlich, wenn du die gleichen Felder mehrfach importieren möchtest.',
'fieldsets_handle_instructions' => 'Verweist an anderer Stelle auf dieses Fieldset. Kann später nicht ohne Weiteres geändert werden.',
'fieldsets_title_instructions' => 'Beschreibt in der Regel, welche Felder darin enthalten sein werden, wie z.B. Bilderblock oder Metadaten.',
+ 'filters_view_already_exists' => 'Es existiert bereits eine Ansicht mit diesem Namen. Durch das Erstellen dieser Ansicht wird die vorhandene Ansicht mit diesem Namen überschrieben.',
'focal_point_instructions' => 'Das Setzen des Fokuspunkts ermöglicht das dynamische Zuschneiden des Fotos auf den Motivschwerpunkt.',
'focal_point_previews_are_examples' => 'Vorschau Bildausschnitte sind nur Beispiele',
'forgot_password_enter_email' => 'Bitte gib deine E-Mail-Adresse ein, damit wir dir einen Link zum Zurücksetzen des Passworts zusenden können.',
@@ -228,7 +229,7 @@
'user_groups_title_instructions' => 'Gewöhnlich ein Substantiv im Plural, wie z.B. Autor:innen oder Fotograf:innen.',
'user_wizard_account_created' => 'Das Benutzerkonto wurde erstellt.',
'user_wizard_intro' => 'Benutzer:innen können Rollen zugewiesen werden, die ihre Berechtigungen, ihren Zugriff und ihre Möglichkeiten im gesamten Control Panel anpassen.',
- 'user_wizard_invitation_body' => 'Aktiviere dein neues Statamic-Konto auf :site, um mit der Verwaltung dieser Website zu beginnen. Zu deiner Sicherheit läuft der untenstehende Link nach :expiry Stunde ab. Wende dich danach bitte an die Administrator:in der Website, um ein neues Passwort zu erhalten.',
+ 'user_wizard_invitation_body' => 'Aktiviere dein neues Statamic-Konto auf :site, um mit der Verwaltung dieser Website zu beginnen. Zu deiner Sicherheit läuft der untenstehende Link nach :expiry Stunden ab. Wende dich danach bitte an die Administrator:in der Website, um ein neues Passwort zu erhalten.',
'user_wizard_invitation_intro' => 'Sende der neuen Benutzer:in ein Begrüßungsmail mit den Angaben zur Kontoaktivierung.',
'user_wizard_invitation_share' => 'Kopiere diese Zugangsdaten und gib sie über deinen bevorzugten Weg an :email
weiter.',
'user_wizard_invitation_share_before' => 'Nach dem Erstellen einer Benutzer:in bekommst du alle Angaben, die du :email
über deine bevorzugte Weise weitergeben kannst.',
diff --git a/resources/lang/de/validation.php b/resources/lang/de/validation.php
index f93008830c..c1cbf6c011 100644
--- a/resources/lang/de/validation.php
+++ b/resources/lang/de/validation.php
@@ -44,10 +44,10 @@
'gt.file' => 'Muss größer als :value Kilobyte sein.',
'gt.numeric' => 'Muss größer als :value sein.',
'gt.string' => 'Muss größer als :value Zeichen sein.',
- 'gte.numeric' => 'Muss größer oder gleich :value sein.',
+ 'gte.array' => 'Muss :value oder mehr Elemente haben.',
'gte.file' => 'Muss größer oder gleich :value Kilobyte sein.',
+ 'gte.numeric' => 'Muss größer oder gleich :value sein.',
'gte.string' => 'Muss größer oder gleich :value Zeichen sein.',
- 'gte.array' => 'Muss :value oder mehr Elemente haben.',
'image' => 'Muss ein Bild sein.',
'in' => 'Dies ist ungültig.',
'in_array' => 'Dieses Feld existiert nicht in :other .',
@@ -57,26 +57,26 @@
'ipv6' => 'Muss eine gültige IPv6-Adresse sein.',
'json' => 'Muss eine gültige JSON-Zeichenfolge sein.',
'lowercase' => 'Muss klein geschrieben sein.',
- 'lt.numeric' => 'Muss kleiner als :value sein.',
+ 'lt.array' => 'Muss weniger als :value Elemente enthalten.',
'lt.file' => 'Muss kleiner als :value Kilobyte sein.',
+ 'lt.numeric' => 'Muss kleiner als :value sein.',
'lt.string' => 'Muss aus weniger als :value Zeichen bestehen.',
- 'lt.array' => 'Muss weniger als :value Elemente enthalten.',
- 'lte.numeric' => 'Muss kleiner oder gleich :value sein.',
+ 'lte.array' => 'Darf nicht mehr als :value Elemente enthalten.',
'lte.file' => 'Muss kleiner oder gleich :value Kilobyte sein.',
+ 'lte.numeric' => 'Muss kleiner oder gleich :value sein.',
'lte.string' => 'Muss kleiner oder gleich :value Zeichen sein.',
- 'lte.array' => 'Darf nicht mehr als :value Elemente enthalten.',
'mac_address' => 'Muss eine gültige MAC-Adresse sein.',
- 'max.numeric' => 'Darf nicht größer als :max sein.',
+ 'max.array' => 'Darf nicht mehr als :max Elemente haben.',
'max.file' => 'Darf nicht größer als :max kilobytes sein.',
+ 'max.numeric' => 'Darf nicht größer als :max sein.',
'max.string' => 'Darf nicht mehr als :max Zeichen haben.',
- 'max.array' => 'Darf nicht mehr als :max Elemente haben.',
'max_digits' => 'Darf nicht mehr als :max Ziffern enthalten.',
'mimes' => 'Muss eine Datei vom Typ :values sein.',
'mimetypes' => 'Muss eine Datei vom Typ :values sein.',
- 'min.numeric' => 'Muss mindestens :min sein.',
+ 'min.array' => 'Muss mindestens :min Elemente enthalten.',
'min.file' => 'Muss mindestens :min Kilobyte sein.',
+ 'min.numeric' => 'Muss mindestens :min sein.',
'min.string' => 'Muss mindestens :min Zeichen haben.',
- 'min.array' => 'Muss mindestens :min Elemente enthalten.',
'min_digits' => 'Darf nicht weniger als :max Ziffern enthalten.',
'missing' => 'Muss fehlen.',
'missing_if' => 'Muss fehlen, wenn :other gleich :value ist.',
@@ -110,35 +110,37 @@
'starts_with' => 'Muss mit :values beginnen',
'string' => 'Muss eine Zeichenfolge sein.',
'timezone' => 'Muss eine gültige Zeitzone sein.',
+ 'ulid' => 'Muss eine gültige ULID sein.',
'unique' => 'Dieser Wert wurde bereits vergeben.',
'uploaded' => 'Der Upload ist fehlgeschlagen.',
'uppercase' => 'Muss in Großbuchstaben geschrieben sein.',
'url' => 'Das Format ist ungültig.',
- 'ulid' => 'Muss eine gültige ULID sein.',
'uuid' => 'Muss eine gültige UUID sein.',
- 'unique_entry_value' => 'Dieser Wert wurde bereits vergeben.',
- 'unique_term_value' => 'Dieser Wert wurde bereits vergeben.',
- 'unique_user_value' => 'Dieser Wert wurde bereits vergeben.',
- 'unique_form_handle' => 'Dieser Wert wurde bereits vergeben.',
+ 'arr_fieldtype' => 'Dies ist ungültig.',
+ 'code_fieldtype_rulers' => 'Dies ist ungültig.',
+ 'date_fieldtype_date_required' => 'Eine Datumsangabe ist erforderlich.',
+ 'date_fieldtype_end_date_invalid' => 'Kein gültiges Enddatum.',
+ 'date_fieldtype_end_date_required' => 'Ein Enddatum ist erforderlich.',
+ 'date_fieldtype_only_single_mode_allowed' => 'Wenn der Handle für dieses Feld «date» heisst, kann nur der Modus «Einzeln» gewählt werden.',
+ 'date_fieldtype_start_date_invalid' => 'Kein gültiges Startdatum.',
+ 'date_fieldtype_start_date_required' => 'Ein Anfangsdatum ist erforderlich.',
+ 'date_fieldtype_time_required' => 'Eine Zeitangabe ist erforderlich.',
'duplicate_field_handle' => 'Ein Feld mit einem Handle :handle gibt es bereits.',
+ 'duplicate_uri' => 'Doppelte URI :value',
'one_site_without_origin' => 'Mindestens eine Website darf keine Quelle enthalten.',
+ 'options_require_keys' => 'Alle Optionen müssen Schlüssel haben.',
'origin_cannot_be_disabled' => 'Kann keine deaktivierte Quelle auswählen.',
- 'unique_uri' => 'Diese URI ist bereits vergeben.',
- 'duplicate_uri' => 'Doppelte URI :value',
+ 'parent_cannot_be_itself' => 'Kann nicht selbst übergeordnet sein.',
+ 'parent_causes_root_children' => 'Dadurch würde die Startseite Unterseiten bekommen.',
+ 'parent_exceeds_max_depth' => 'Damit würde die maximale Tiefe überschritten.',
'reserved' => 'Dies ist ein reserviertes Wort.',
'reserved_field_handle' => 'Feld mit dem Handle :handle ist ein reserviertes Wort.',
- 'parent_causes_root_children' => 'Dadurch würde die Startseite Unterseiten bekommen.',
- 'parent_cannot_be_itself' => 'Kann nicht selbst übergeordnet sein.',
+ 'unique_entry_value' => 'Dieser Wert wurde bereits vergeben.',
+ 'unique_form_handle' => 'Dieser Wert wurde bereits vergeben.',
+ 'unique_term_value' => 'Dieser Wert wurde bereits vergeben.',
+ 'unique_user_value' => 'Dieser Wert wurde bereits vergeben.',
+ 'unique_uri' => 'Diese URI wurde bereits vergeben.',
'time' => 'Keine gültige Uhrzeit.',
- 'date_fieldtype_date_required' => 'Eine Datumsangabe ist erforderlich.',
- 'date_fieldtype_time_required' => 'Eine Zeitangabe ist erforderlich.',
- 'date_fieldtype_start_date_required' => 'Ein Anfangsdatum ist erforderlich.',
- 'date_fieldtype_start_date_invalid' => 'Kein gültiges Startdatum.',
- 'date_fieldtype_end_date_required' => 'Ein Enddatum ist erforderlich.',
- 'date_fieldtype_end_date_invalid' => 'Kein gültiges Enddatum.',
- 'date_fieldtype_only_single_mode_allowed' => 'Wenn der Handle für dieses Feld «date» heisst, kann nur der Modus «Einzeln» gewählt werden.',
- 'code_fieldtype_rulers' => 'Dies ist ungültig.',
- 'options_require_keys' => 'Alle Optionen müssen Schlüssel haben.',
'custom.attribute-name.rule-name' => 'benutzerdefinierte Nachricht',
'attributes' => [],
];
diff --git a/resources/lang/de_CH.json b/resources/lang/de_CH.json
index e310cc6407..b99b9304cc 100644
--- a/resources/lang/de_CH.json
+++ b/resources/lang/de_CH.json
@@ -948,6 +948,7 @@
"Unable to change password": "Passwort kann nicht geändert werden.",
"Unable to delete filter preset": "Filtervoreinstellung kann nicht gelöscht werden.",
"Unable to delete view": "Ansicht kann nicht gelöscht werden.",
+ "Unable to rename view": "Ansicht kann nicht umbenannt werden.",
"Unable to save changes": "Änderungen können nicht gespeichert werden.",
"Unable to save column preferences.": "Spaltenvoreinstellungen können nicht gespeichert werden.",
"Unable to save favorite": "Favorit kann nicht gespeichert werden.",
@@ -1000,6 +1001,7 @@
"View deleted": "Ansicht gelöscht",
"View History": "Verlauf anzeigen",
"View on Marketplace": "Auf Marktplatz anzeigen",
+ "View renamed": "Ansicht umbenannt",
"View saved": "Ansicht gespeichert",
"View Site": "Website anzeigen",
"View Useful Links": "Nützliche Links anzeigen",
@@ -1032,6 +1034,7 @@
"You are not authorized to view this collection.": "Du bist nicht berechtigt, diese Sammlung anzuzeigen.",
"You are now impersonating": "Du gibst dich jetzt aus als",
"You can't do this while logged in": "Dies ist nicht möglich, wenn du eingeloggt bist.",
+ "You're already editing this item.": "Du bearbeitest diesen Eintrag bereits.",
"Your Favorites": "Meine Favoriten",
"Your working copy will be replaced by the contents of this revision.": "Deine Arbeitskopie wird durch den Inhalt dieser Überarbeitung ersetzt."
}
diff --git a/resources/lang/de_CH/messages.php b/resources/lang/de_CH/messages.php
index e0e2671dd8..ec5b692603 100644
--- a/resources/lang/de_CH/messages.php
+++ b/resources/lang/de_CH/messages.php
@@ -100,6 +100,7 @@
'fieldset_link_fields_prefix_instructions' => 'Jedem Feld im verknüpften Fieldset wird dieses Präfix vorangestellt. Nützlich, wenn du die gleichen Felder mehrfach importieren möchtest.',
'fieldsets_handle_instructions' => 'Damit wird an anderer Stelle auf dieses Fieldset verwiesen. Kann später nicht ohne Weiteres geändert werden.',
'fieldsets_title_instructions' => 'Beschreibt in der Regel, welche Felder darin enthalten sein werden, wie z.B. Bilderblock oder Metadaten.',
+ 'filters_view_already_exists' => 'Es existiert bereits eine Ansicht mit diesem Namen. Durch das Erstellen dieser Ansicht wird die vorhandene Ansicht mit diesem Namen überschrieben.',
'focal_point_instructions' => 'Das Setzen des Fokuspunkts ermöglicht das dynamische Zuschneiden des Fotos auf den Motivschwerpunkt.',
'focal_point_previews_are_examples' => 'Vorschau Bildausschnitte sind nur Beispiele',
'forgot_password_enter_email' => 'Bitte gib deine E-Mail-Adresse ein, damit wir dir einen Link zum Zurücksetzen des Passworts zusenden können.',
@@ -228,7 +229,7 @@
'user_groups_title_instructions' => 'Gewöhnlich ein Substantiv im Plural, wie z.B. Autor:innen oder Fotograf:innen.',
'user_wizard_account_created' => 'Das Benutzerkonto wurde erstellt.',
'user_wizard_intro' => 'Benutzer:innen können Rollen zugewiesen werden, die ihre Berechtigungen, ihren Zugriff und ihre Möglichkeiten im gesamten Control Panel anpassen.',
- 'user_wizard_invitation_body' => 'Aktiviere dein neues Statamic-Konto auf :site, um mit der Verwaltung dieser Website zu beginnen. Zu deiner Sicherheit läuft der untenstehende Link nach :expiry Stunde ab. Wende dich danach bitte an die Administrator:in der Website, um ein neues Passwort zu erhalten.',
+ 'user_wizard_invitation_body' => 'Aktiviere dein neues Statamic-Konto auf :site, um mit der Verwaltung dieser Website zu beginnen. Zu deiner Sicherheit läuft der untenstehende Link nach :expiry Stunden ab. Wende dich danach bitte an die Administrator:in der Website, um ein neues Passwort zu erhalten.',
'user_wizard_invitation_intro' => 'Sende der neuen Benutzer:in ein Begrüssungsmail mit den Angaben zur Kontoaktivierung.',
'user_wizard_invitation_share' => 'Kopiere diese Zugangsdaten und gib sie über deinen bevorzugten Weg an :email
weiter.',
'user_wizard_invitation_share_before' => 'Nach dem Erstellen einer Benutzer:in bekommst du alle Angaben, die du :email
über deine bevorzugte Weise weitergeben kannst.',
diff --git a/resources/lang/de_CH/validation.php b/resources/lang/de_CH/validation.php
index bc2f30bb1c..014b70fc94 100644
--- a/resources/lang/de_CH/validation.php
+++ b/resources/lang/de_CH/validation.php
@@ -44,10 +44,10 @@
'gt.file' => 'Muss grösser als :value Kilobyte sein.',
'gt.numeric' => 'Muss grösser als :value sein.',
'gt.string' => 'Muss grösser als :value Zeichen sein.',
- 'gte.numeric' => 'Muss grösser oder gleich :value sein.',
+ 'gte.array' => 'Muss :value oder mehr Elemente haben.',
'gte.file' => 'Muss grösser oder gleich :value Kilobyte sein.',
+ 'gte.numeric' => 'Muss grösser oder gleich :value sein.',
'gte.string' => 'Muss grösser oder gleich :value Zeichen sein.',
- 'gte.array' => 'Muss :value oder mehr Elemente haben.',
'image' => 'Muss ein Bild sein.',
'in' => 'Dies ist ungültig.',
'in_array' => 'Dieses Feld existiert nicht in :other .',
@@ -57,26 +57,26 @@
'ipv6' => 'Muss eine gültige IPv6-Adresse sein.',
'json' => 'Muss eine gültige JSON-Zeichenfolge sein.',
'lowercase' => 'Muss klein geschrieben sein.',
- 'lt.numeric' => 'Muss kleiner als :value sein.',
+ 'lt.array' => 'Muss weniger als :value Elemente enthalten.',
'lt.file' => 'Muss kleiner als :value Kilobyte sein.',
+ 'lt.numeric' => 'Muss kleiner als :value sein.',
'lt.string' => 'Muss aus weniger als :value Zeichen bestehen.',
- 'lt.array' => 'Muss weniger als :value Elemente enthalten.',
- 'lte.numeric' => 'Muss kleiner oder gleich :value sein.',
+ 'lte.array' => 'Darf nicht mehr als :value Elemente enthalten.',
'lte.file' => 'Muss kleiner oder gleich :value Kilobyte sein.',
+ 'lte.numeric' => 'Muss kleiner oder gleich :value sein.',
'lte.string' => 'Muss kleiner oder gleich :value Zeichen sein.',
- 'lte.array' => 'Darf nicht mehr als :value Elemente enthalten.',
'mac_address' => 'Muss eine gültige MAC-Adresse sein.',
- 'max.numeric' => 'Darf nicht grösser als :max sein.',
+ 'max.array' => 'Darf nicht mehr als :max Elemente haben.',
'max.file' => 'Darf nicht grösser als :max kilobytes sein.',
+ 'max.numeric' => 'Darf nicht grösser als :max sein.',
'max.string' => 'Darf nicht mehr als :max Zeichen haben.',
- 'max.array' => 'Darf nicht mehr als :max Elemente haben.',
'max_digits' => 'Darf nicht mehr als :max Ziffern enthalten.',
'mimes' => 'Muss eine Datei vom Typ :values sein.',
'mimetypes' => 'Muss eine Datei vom Typ :values sein.',
- 'min.numeric' => 'Muss mindestens :min sein.',
+ 'min.array' => 'Muss mindestens :min Elemente enthalten.',
'min.file' => 'Muss mindestens :min Kilobyte sein.',
+ 'min.numeric' => 'Muss mindestens :min sein.',
'min.string' => 'Muss mindestens :min Zeichen haben.',
- 'min.array' => 'Muss mindestens :min Elemente enthalten.',
'min_digits' => 'Darf nicht weniger als :max Ziffern enthalten.',
'missing' => 'Muss fehlen.',
'missing_if' => 'Muss fehlen, wenn :other gleich :value ist.',
@@ -110,35 +110,37 @@
'starts_with' => 'Muss mit :values beginnen',
'string' => 'Muss eine Zeichenfolge sein.',
'timezone' => 'Muss eine gültige Zeitzone sein.',
+ 'ulid' => 'Muss eine gültige ULID sein.',
'unique' => 'Dieser Wert wurde bereits vergeben.',
'uploaded' => 'Der Upload ist fehlgeschlagen.',
'uppercase' => 'Muss in Grossbuchstaben geschrieben sein.',
'url' => 'Das Format ist ungültig.',
- 'ulid' => 'Muss eine gültige ULID sein.',
'uuid' => 'Muss eine gültige UUID sein.',
- 'unique_entry_value' => 'Dieser Wert wurde bereits vergeben.',
- 'unique_term_value' => 'Dieser Wert wurde bereits vergeben.',
- 'unique_user_value' => 'Dieser Wert wurde bereits vergeben.',
- 'unique_form_handle' => 'Dieser Wert wurde bereits vergeben.',
+ 'arr_fieldtype' => 'Dies ist ungültig.',
+ 'code_fieldtype_rulers' => 'Dies ist ungültig.',
+ 'date_fieldtype_date_required' => 'Eine Datumsangabe ist erforderlich.',
+ 'date_fieldtype_end_date_invalid' => 'Kein gültiges Enddatum.',
+ 'date_fieldtype_end_date_required' => 'Ein Enddatum ist erforderlich.',
+ 'date_fieldtype_only_single_mode_allowed' => 'Wenn der Handle für dieses Feld «date» heisst, kann nur der Modus «Einzeln» gewählt werden.',
+ 'date_fieldtype_start_date_invalid' => 'Kein gültiges Startdatum.',
+ 'date_fieldtype_start_date_required' => 'Ein Anfangsdatum ist erforderlich.',
+ 'date_fieldtype_time_required' => 'Eine Zeitangabe ist erforderlich.',
'duplicate_field_handle' => 'Ein Feld mit einem Handle :handle gibt es bereits.',
+ 'duplicate_uri' => 'Doppelte URI :value',
'one_site_without_origin' => 'Mindestens eine Website darf keine Quelle enthalten.',
+ 'options_require_keys' => 'Alle Optionen müssen Schlüssel haben.',
'origin_cannot_be_disabled' => 'Kann keine deaktivierte Quelle auswählen.',
- 'unique_uri' => 'Diese URI ist bereits vergeben.',
- 'duplicate_uri' => 'Doppelte URI :value',
+ 'parent_cannot_be_itself' => 'Kann nicht selbst übergeordnet sein.',
+ 'parent_causes_root_children' => 'Dadurch würde die Startseite Unterseiten bekommen.',
+ 'parent_exceeds_max_depth' => 'Damit würde die maximale Tiefe überschritten.',
'reserved' => 'Dies ist ein reserviertes Wort.',
'reserved_field_handle' => 'Feld mit dem Handle :handle ist ein reserviertes Wort.',
- 'parent_causes_root_children' => 'Dadurch würde die Startseite Unterseiten bekommen.',
- 'parent_cannot_be_itself' => 'Kann nicht selbst übergeordnet sein.',
+ 'unique_entry_value' => 'Dieser Wert wurde bereits vergeben.',
+ 'unique_form_handle' => 'Dieser Wert wurde bereits vergeben.',
+ 'unique_term_value' => 'Dieser Wert wurde bereits vergeben.',
+ 'unique_user_value' => 'Dieser Wert wurde bereits vergeben.',
+ 'unique_uri' => 'Diese URI wurde bereits vergeben.',
'time' => 'Keine gültige Uhrzeit.',
- 'date_fieldtype_date_required' => 'Eine Datumsangabe ist erforderlich.',
- 'date_fieldtype_time_required' => 'Eine Zeitangabe ist erforderlich.',
- 'date_fieldtype_start_date_required' => 'Ein Anfangsdatum ist erforderlich.',
- 'date_fieldtype_start_date_invalid' => 'Kein gültiges Startdatum.',
- 'date_fieldtype_end_date_required' => 'Ein Enddatum ist erforderlich.',
- 'date_fieldtype_end_date_invalid' => 'Kein gültiges Enddatum.',
- 'date_fieldtype_only_single_mode_allowed' => 'Wenn der Handle für dieses Feld «date» heisst, kann nur der Modus «Einzeln» gewählt werden.',
- 'code_fieldtype_rulers' => 'Dies ist ungültig.',
- 'options_require_keys' => 'Alle Optionen müssen Schlüssel haben.',
'custom.attribute-name.rule-name' => 'benutzerdefinierte Nachricht',
'attributes' => [],
];
diff --git a/resources/lang/en/messages.php b/resources/lang/en/messages.php
index 35e468a497..713a43bbd9 100644
--- a/resources/lang/en/messages.php
+++ b/resources/lang/en/messages.php
@@ -172,6 +172,7 @@
'outpost_error_422' => 'Error communicating with statamic.com.',
'outpost_error_429' => 'Too many requests to statamic.com.',
'outpost_issue_try_later' => 'There was an issue communicating with statamic.com. Please try again later.',
+ 'outpost_license_key_error' => 'Statamic was unable to decrypt the provided license key file. Please re-download from statamic.com.',
'password_passkeys_only' => 'Please login using your passkey',
'password_protect_enter_password' => 'Enter password to unlock',
'password_protect_incorrect_password' => 'Incorrect password.',
diff --git a/resources/views/licensing.blade.php b/resources/views/licensing.blade.php
index 70a2cfd65d..a4722670e0 100644
--- a/resources/views/licensing.blade.php
+++ b/resources/views/licensing.blade.php
@@ -17,7 +17,11 @@
{{ __('Licensing') }}
- {{ __('statamic::messages.outpost_issue_try_later') }}
+ @if ($usingLicenseKeyFile)
+ {{ __('statamic::messages.outpost_license_key_error') }}
+ @else
+ {{ __('statamic::messages.outpost_issue_try_later') }}
+ @endif
{{ __('Try again') }}
diff --git a/resources/views/partials/licensing-alerts.blade.php b/resources/views/partials/licensing-alerts.blade.php
index 62fb2a2f29..e2260f92d7 100644
--- a/resources/views/partials/licensing-alerts.blade.php
+++ b/resources/views/partials/licensing-alerts.blade.php
@@ -4,7 +4,9 @@
@if ($licenses->requestFailed())
- @if ($licenses->requestErrorCode() === 422)
+ @if ($licenses->usingLicenseKeyFile())
+ {{ __('statamic::messages.outpost_license_key_error') }}
+ @elseif ($licenses->requestErrorCode() === 422)
{{ __('statamic::messages.outpost_error_422') }}
{{ join(' ', $licenses->requestValidationErrors()->unique()) }}
@elseif ($licenses->requestErrorCode() === 429)
diff --git a/src/Console/Commands/InstallEloquentDriver.php b/src/Console/Commands/InstallEloquentDriver.php
index 05dd564135..73edbdc97d 100644
--- a/src/Console/Commands/InstallEloquentDriver.php
+++ b/src/Console/Commands/InstallEloquentDriver.php
@@ -102,6 +102,7 @@ protected function availableRepositories(): Collection
'revisions' => 'Revisions',
'taxonomies' => 'Taxonomies',
'terms' => 'Terms',
+ 'tokens' => 'Tokens',
])->reject(function ($value, $key) {
switch ($key) {
case 'asset_containers':
@@ -149,6 +150,9 @@ protected function availableRepositories(): Collection
case 'terms':
return config('statamic.eloquent-driver.terms.driver') === 'eloquent';
+
+ case 'tokens':
+ return config('statamic.eloquent-driver.tokens.driver') === 'eloquent';
}
});
}
@@ -535,6 +539,21 @@ protected function migrateTerms(): void
}
}
+ protected function migrateTokens(): void
+ {
+ spin(
+ callback: function () {
+ $this->runArtisanCommand('vendor:publish --tag=statamic-eloquent-token-migrations');
+ $this->runArtisanCommand('migrate');
+
+ $this->switchToEloquentDriver('tokens');
+ },
+ message: 'Migrating tokens...'
+ );
+
+ $this->components->info('Configured tokens');
+ }
+
private function switchToEloquentDriver(string $repository): void
{
File::put(
diff --git a/src/Console/Commands/StarterKitInstall.php b/src/Console/Commands/StarterKitInstall.php
index 75d492e6c0..6264ef1a55 100644
--- a/src/Console/Commands/StarterKitInstall.php
+++ b/src/Console/Commands/StarterKitInstall.php
@@ -44,7 +44,9 @@ class StarterKitInstall extends Command
*/
public function handle()
{
- if ($this->validationFails($package = $this->getPackage(), new ComposerPackage)) {
+ [$package, $branch] = $this->getPackageAndBranch();
+
+ if ($this->validationFails($package, new ComposerPackage)) {
return;
}
@@ -59,6 +61,7 @@ public function handle()
}
$installer = StarterKitInstaller::package($package, $this, $licenseManager)
+ ->branch($branch)
->fromLocalRepo($this->option('local'))
->withConfig($this->option('with-config'))
->withoutDependencies($this->option('without-dependencies'))
@@ -89,13 +92,21 @@ public function handle()
}
/**
- * Get composer package.
+ * Get composer package (and optional branch).
*
* @return string
*/
- protected function getPackage()
+ protected function getPackageAndBranch()
{
- return $this->argument('package') ?: text('Package');
+ $package = $this->argument('package') ?: text('Package');
+
+ $parts = explode(':', $package);
+
+ if (count($parts) === 1) {
+ $parts[] = null;
+ }
+
+ return $parts;
}
/**
diff --git a/src/Console/Processes/Composer.php b/src/Console/Processes/Composer.php
index cf5bf4c70c..5a275385e6 100644
--- a/src/Console/Processes/Composer.php
+++ b/src/Console/Processes/Composer.php
@@ -6,6 +6,7 @@
use Statamic\Console\Composer\Lock;
use Statamic\Jobs\RunComposer;
use Statamic\Support\Str;
+use Statamic\View\Antlers\Language\Utilities\StringUtilities;
class Composer extends Process
{
@@ -322,7 +323,43 @@ private function prepareProcessArguments($parts)
*/
private function composerBinary(): string
{
- return $this->run(DIRECTORY_SEPARATOR === '\\' ? 'where composer' : 'which composer');
+ $isWindows = DIRECTORY_SEPARATOR === '\\';
+
+ $output = $this->run($isWindows ? 'where composer' : 'which composer');
+
+ if ($isWindows) {
+ return $this->locateComposerPharOnWindows($output);
+ }
+
+ return $output;
+ }
+
+ private function locateComposerPharOnWindows($output): string
+ {
+ $output = StringUtilities::normalizeLineEndings($output);
+
+ if (! Str::contains($output, "\n")) {
+ $candidates = [trim($output)];
+ } else {
+ $candidates = explode("\n", $output);
+ }
+
+ foreach ($candidates as $candidate) {
+ // Do we have a bat file? The phar is likely beside it.
+ if (Str::endsWith($candidate, '.bat')) {
+ // Remove that 🦇 extension.
+ $candidate = mb_substr($candidate, 0, mb_strlen($candidate) - 4);
+ }
+
+ $pharPath = $candidate.'.phar';
+
+ if (file_exists($pharPath)) {
+ // Use "composer.phar" if we have it.
+ return $pharPath;
+ }
+ }
+
+ return $output;
}
/**
diff --git a/src/Contracts/Tokens/TokenRepository.php b/src/Contracts/Tokens/TokenRepository.php
new file mode 100644
index 0000000000..84b23977ff
--- /dev/null
+++ b/src/Contracts/Tokens/TokenRepository.php
@@ -0,0 +1,16 @@
+handle();
- $fieldData = collect(Arr::dot(Arr::get($data, $dottedKey, [])));
+ $fieldData = Arr::get($data, $dottedKey, []);
+
+ if (! $fieldData) {
+ return;
+ }
+
+ $fieldData = collect(Arr::dot($fieldData));
if (! $fieldData->contains($this->originalValue())) {
return;
diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php
index e3a46ddd18..7950f9ece9 100644
--- a/src/Entries/Entry.php
+++ b/src/Entries/Entry.php
@@ -381,6 +381,7 @@ public function save()
Facades\Entry::save($this);
if ($this->id()) {
+ Blink::store('entry-uris')->forget($this->id());
Blink::store('structure-uris')->forget($this->id());
Blink::store('structure-entries')->forget($this->id());
Blink::forget($this->getOriginBlinkKey());
@@ -876,15 +877,23 @@ public function routeData()
public function uri()
{
+ if ($this->id() && Blink::store('entry-uris')->has($this->id())) {
+ return Blink::store('entry-uris')->get($this->id());
+ }
+
if (! $this->route()) {
return null;
}
- if ($structure = $this->structure()) {
- return $structure->entryUri($this);
+ $uri = ($structure = $this->structure())
+ ? $structure->entryUri($this)
+ : $this->routableUri();
+
+ if ($uri && $this->id()) {
+ Blink::store('entry-uris')->put($this->id(), $uri);
}
- return $this->routableUri();
+ return $uri;
}
public function fileExtension()
@@ -1011,6 +1020,11 @@ public function getQueryableValue(string $field)
return $this->value('authors');
}
+ // Reset the cached uri so it gets recalculated.
+ if ($field === 'uri') {
+ Blink::store('entry-uris')->forget($this->id());
+ }
+
if (method_exists($this, $method = Str::camel($field))) {
return $this->{$method}();
}
diff --git a/src/Extend/Manifest.php b/src/Extend/Manifest.php
index b038b02637..9be9273034 100644
--- a/src/Extend/Manifest.php
+++ b/src/Extend/Manifest.php
@@ -48,7 +48,9 @@ protected function formatPackage($package)
$statamic = $json['extra']['statamic'] ?? [];
$author = $json['authors'][0] ?? null;
- $marketplaceData = Marketplace::package($package['name'], $package['version']);
+ $edition = config('statamic.editions.addons.'.$package['name']);
+
+ $marketplaceData = Marketplace::package($package['name'], $package['version'], $edition);
return [
'id' => $package['name'],
@@ -58,6 +60,7 @@ protected function formatPackage($package)
'marketplaceSlug' => data_get($marketplaceData, 'slug', null),
'marketplaceUrl' => data_get($marketplaceData, 'url', null),
'marketplaceSellerSlug' => data_get($marketplaceData, 'seller', null),
+ 'isCommercial' => data_get($marketplaceData, 'is_commercial', false),
'latestVersion' => data_get($marketplaceData, 'latest_version', null),
'version' => Str::removeLeft($package['version'], 'v'),
'namespace' => $namespace,
diff --git a/src/Facades/Token.php b/src/Facades/Token.php
index 270c81ffbc..d2a8b7bc18 100644
--- a/src/Facades/Token.php
+++ b/src/Facades/Token.php
@@ -3,8 +3,17 @@
namespace Statamic\Facades;
use Illuminate\Support\Facades\Facade;
-use Statamic\Tokens\TokenRepository;
+use Statamic\Contracts\Tokens\TokenRepository;
+/**
+ * @method static \Statamic\Contracts\Tokens\Token make(?string $token, string $handler, array $data = [])
+ * @method static \Statamic\Contracts\Tokens\Token find(string $token)
+ * @method static bool save(\Statamic\Contracts\Tokens\Token $token)
+ * @method static bool delete(\Statamic\Contracts\Tokens\Token $token)
+ * @method static void collectGarbage()
+ *
+ * @see \Statamic\Tokens\TokenRepository
+ */
class Token extends Facade
{
protected static function getFacadeAccessor()
diff --git a/src/Http/Controllers/CP/LicensingController.php b/src/Http/Controllers/CP/LicensingController.php
index 01eec8da01..14ebeac585 100644
--- a/src/Http/Controllers/CP/LicensingController.php
+++ b/src/Http/Controllers/CP/LicensingController.php
@@ -16,6 +16,7 @@ public function show(Licenses $licenses)
'unlistedAddons' => $licenses->addons()->reject->existsOnMarketplace(),
'configCached' => app()->configurationIsCached(),
'addToCartUrl' => $this->addToCartUrl($site, $statamic, $addons),
+ 'usingLicenseKeyFile' => $licenses->usingLicenseKeyFile(),
]);
}
diff --git a/src/Http/Controllers/CP/Navigation/NavigationController.php b/src/Http/Controllers/CP/Navigation/NavigationController.php
index 6b3cd46c49..1a7bb706c8 100644
--- a/src/Http/Controllers/CP/Navigation/NavigationController.php
+++ b/src/Http/Controllers/CP/Navigation/NavigationController.php
@@ -40,7 +40,7 @@ public function edit($nav)
{
$nav = Nav::find($nav);
- $this->authorize('edit', $nav, __('You are not authorized to configure navs.'));
+ $this->authorize('configure', $nav, __('You are not authorized to configure navs.'));
$values = [
'title' => $nav->title(),
diff --git a/src/Http/Controllers/FrontendController.php b/src/Http/Controllers/FrontendController.php
index 150b38bfff..e315b0ef58 100644
--- a/src/Http/Controllers/FrontendController.php
+++ b/src/Http/Controllers/FrontendController.php
@@ -41,7 +41,7 @@ public function route(Request $request, ...$args)
$params = $request->route()->parameters();
$view = Arr::pull($params, 'view');
$data = Arr::pull($params, 'data');
- $data = array_merge($params, is_callable($data) ? $data() : $data);
+ $data = array_merge($params, is_callable($data) ? $data(...$params) : $data);
$view = app(View::class)
->template($view)
diff --git a/src/Http/Resources/CP/Entries/ListedEntry.php b/src/Http/Resources/CP/Entries/ListedEntry.php
index 2d4037492b..c6ccb2135d 100644
--- a/src/Http/Resources/CP/Entries/ListedEntry.php
+++ b/src/Http/Resources/CP/Entries/ListedEntry.php
@@ -36,7 +36,7 @@ public function toArray($request)
'status' => $entry->status(),
'private' => $entry->private(),
'date' => $this->when($collection->dated(), function () {
- return $this->resource->date()->inPreferredFormat();
+ return $this->resource->blueprint()->field('date')->fieldtype()->preProcessIndex($this->resource->date());
}),
$this->merge($this->values(['slug' => $entry->slug()])),
diff --git a/src/Licensing/LicenseManager.php b/src/Licensing/LicenseManager.php
index 0aa995c0d1..2a2170236f 100644
--- a/src/Licensing/LicenseManager.php
+++ b/src/Licensing/LicenseManager.php
@@ -117,4 +117,9 @@ public function refresh()
{
$this->outpost->clearCachedResponse();
}
+
+ public function usingLicenseKeyFile()
+ {
+ return $this->outpost->usingLicenseKeyFile();
+ }
}
diff --git a/src/Licensing/Outpost.php b/src/Licensing/Outpost.php
index 908467dbe1..60a43c639c 100644
--- a/src/Licensing/Outpost.php
+++ b/src/Licensing/Outpost.php
@@ -9,10 +9,15 @@
use Illuminate\Cache\NoLock;
use Illuminate\Contracts\Cache\LockProvider;
use Illuminate\Contracts\Cache\LockTimeoutException;
+use Illuminate\Contracts\Encryption\DecryptException;
+use Illuminate\Encryption\Encrypter;
use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use InvalidArgumentException;
+use RuntimeException;
use Statamic\Facades;
+use Statamic\Facades\Addon;
use Statamic\Statamic;
use Statamic\Support\Arr;
@@ -72,6 +77,10 @@ private function performAndCacheRequest()
private function performRequest()
{
+ if ($this->usingLicenseKeyFile()) {
+ return $this->licenseKeyFileResponse();
+ }
+
$response = $this->client->request('POST', self::ENDPOINT, [
'headers' => ['accept' => 'application/json'],
'json' => $this->payload(),
@@ -81,6 +90,27 @@ private function performRequest()
return json_decode($response->getBody()->getContents(), true);
}
+ private function licenseKeyFileResponse()
+ {
+ try {
+ $encrypter = new Encrypter(config('statamic.system.license_key'));
+ $decrypted = $encrypter->decrypt(File::get($this->licenseKeyPath()));
+ $response = collect(json_decode($decrypted, true));
+ } catch (DecryptException|RuntimeException $e) {
+ return ['error' => 500];
+ }
+
+ return $response->put('packages', collect($response['packages'])->merge(
+ Addon::all()
+ ->reject(fn ($addon) => array_key_exists($addon->package(), $response['packages']))
+ ->mapWithKeys(fn ($addon) => [$addon->package() => [
+ 'valid' => ! $addon->isCommercial(),
+ 'exists' => $addon->existsOnMarketplace(),
+ 'version_limit' => null,
+ ]])
+ ))->toArray();
+ }
+
public function payload()
{
return [
@@ -202,4 +232,14 @@ private function lock(string $key, int $seconds)
? $this->cache()->lock($key, $seconds)
: new NoLock($key, $seconds);
}
+
+ public function usingLicenseKeyFile()
+ {
+ return File::exists($this->licenseKeyPath());
+ }
+
+ private function licenseKeyPath()
+ {
+ return storage_path('license.key');
+ }
}
diff --git a/src/Marketplace/Marketplace.php b/src/Marketplace/Marketplace.php
index a38754d3bd..4a3c2f2fdf 100644
--- a/src/Marketplace/Marketplace.php
+++ b/src/Marketplace/Marketplace.php
@@ -11,10 +11,14 @@
class Marketplace
{
- public function package($package, $version = null)
+ public function package($package, $version = null, $edition = null)
{
$uri = "packages/$package/$version";
+ if ($edition) {
+ $uri .= "?edition=$edition";
+ }
+
return Cache::rememberWithExpiration("marketplace-$uri", function () use ($uri) {
try {
return [60 => Client::get($uri)['data']];
diff --git a/src/Policies/NavPolicy.php b/src/Policies/NavPolicy.php
index 8614dc0db4..9b63a6ad26 100644
--- a/src/Policies/NavPolicy.php
+++ b/src/Policies/NavPolicy.php
@@ -41,6 +41,11 @@ public function store($user)
// handled by before()
}
+ public function configure($user)
+ {
+ // handled by before()
+ }
+
public function view($user, $nav)
{
$user = User::fromUser($user);
diff --git a/src/Providers/AppServiceProvider.php b/src/Providers/AppServiceProvider.php
index daf9a74d65..c3f2a713a2 100644
--- a/src/Providers/AppServiceProvider.php
+++ b/src/Providers/AppServiceProvider.php
@@ -121,6 +121,7 @@ public function register()
\Statamic\Contracts\Assets\AssetRepository::class => \Statamic\Assets\AssetRepository::class,
\Statamic\Contracts\Forms\FormRepository::class => \Statamic\Forms\FormRepository::class,
\Statamic\Contracts\Forms\SubmissionRepository::class => \Statamic\Stache\Repositories\SubmissionRepository::class,
+ \Statamic\Contracts\Tokens\TokenRepository::class => \Statamic\Tokens\FileTokenRepository::class,
])->each(function ($concrete, $abstract) {
if (! $this->app->bound($abstract)) {
Statamic::repository($abstract, $concrete);
diff --git a/src/Routing/ResolveRedirect.php b/src/Routing/ResolveRedirect.php
index c6a8d59ddc..e549051e95 100644
--- a/src/Routing/ResolveRedirect.php
+++ b/src/Routing/ResolveRedirect.php
@@ -36,14 +36,14 @@ public function item($redirect, $parent = null, $localize = false)
return null;
}
- if ($redirect === '@child') {
- return $this->firstChild($parent);
- }
-
if (is_array($redirect)) {
$redirect = $redirect['url'];
}
+ if ($redirect === '@child') {
+ return $this->firstChild($parent);
+ }
+
if ($redirect instanceof Values) {
// Assume it's a `group` fieldtype with a `url` subfield.
return $redirect->url->value();
diff --git a/src/Stache/Repositories/EntryRepository.php b/src/Stache/Repositories/EntryRepository.php
index be2d678544..511d8a7ee6 100644
--- a/src/Stache/Repositories/EntryRepository.php
+++ b/src/Stache/Repositories/EntryRepository.php
@@ -8,6 +8,7 @@
use Statamic\Entries\EntryCollection;
use Statamic\Exceptions\CollectionNotFoundException;
use Statamic\Exceptions\EntryNotFoundException;
+use Statamic\Facades\Blink;
use Statamic\Facades\Collection;
use Statamic\Rules\Slug;
use Statamic\Stache\Query\EntryQueryBuilder;
@@ -151,6 +152,7 @@ public static function bindings(): array
public function substitute($item)
{
+ Blink::store('entry-uris')->forget($item->id());
$this->substitutionsById[$item->id()] = $item;
$this->substitutionsByUri[$item->locale().'@'.$item->uri()] = $item;
}
diff --git a/src/Stache/Stores/CollectionEntriesStore.php b/src/Stache/Stores/CollectionEntriesStore.php
index 5b61f7fe24..ff6df06611 100644
--- a/src/Stache/Stores/CollectionEntriesStore.php
+++ b/src/Stache/Stores/CollectionEntriesStore.php
@@ -2,10 +2,12 @@
namespace Statamic\Stache\Stores;
+use Illuminate\Support\Facades\Cache;
use Statamic\Entries\GetDateFromPath;
use Statamic\Entries\GetSlugFromPath;
use Statamic\Entries\GetSuffixFromPath;
use Statamic\Entries\RemoveSuffixFromPath;
+use Statamic\Facades\Blink;
use Statamic\Facades\Collection;
use Statamic\Facades\Entry;
use Statamic\Facades\File;
@@ -20,6 +22,7 @@
class CollectionEntriesStore extends ChildStore
{
protected $collection;
+ private bool $shouldBlinkEntryUris = true;
protected function collection()
{
@@ -95,6 +98,10 @@ public function makeItemFromFile($path, $contents)
$entry->date((new GetDateFromPath)($path));
}
+ // Blink the entry so that it can be used when building the URI. If it's not
+ // in there, it would try to retrieve the entry, which doesn't exist yet.
+ Blink::store('structure-entries')->put($id, $entry);
+
if (isset($idGenerated) || isset($positionGenerated)) {
$this->writeItemToDiskWithoutIncrementing($entry);
}
@@ -217,4 +224,37 @@ protected function writeItemToDisk($item)
$item->writeFile($path);
}
+
+ protected function cacheItem($item)
+ {
+ $key = $this->getItemKey($item);
+
+ $cacheKey = $this->getItemCacheKey($key);
+
+ Cache::forever($cacheKey, ['entry' => $item, 'uri' => $item->uri()]);
+ }
+
+ protected function getCachedItem($key)
+ {
+ $cacheKey = $this->getItemCacheKey($key);
+
+ if (! $cache = Cache::get($cacheKey)) {
+ return null;
+ }
+
+ if ($this->shouldBlinkEntryUris && $cache['uri']) {
+ Blink::store('entry-uris')->put($cache['entry']->id(), $cache['uri']);
+ }
+
+ return $cache['entry'];
+ }
+
+ public function withoutBlinkingEntryUris($callback)
+ {
+ $this->shouldBlinkEntryUris = false;
+ $return = $callback();
+ $this->shouldBlinkEntryUris = true;
+
+ return $return;
+ }
}
diff --git a/src/Stache/Stores/CollectionsStore.php b/src/Stache/Stores/CollectionsStore.php
index 9947adfc3d..b4ab6f94a7 100644
--- a/src/Stache/Stores/CollectionsStore.php
+++ b/src/Stache/Stores/CollectionsStore.php
@@ -84,11 +84,20 @@ protected function getDefaultPublishState($data)
public function updateEntryUris($collection, $ids = null)
{
- $index = Stache::store('entries')
- ->store($collection->handle())
- ->index('uri');
+ $store = Stache::store('entries')->store($collection->handle());
+ $this->updateEntriesWithinIndex($store->index('uri'), $ids);
+ $this->updateEntriesWithinStore($store, $ids);
+ }
- $this->updateEntriesWithinIndex($index, $ids);
+ private function updateEntriesWithinStore($store, $ids)
+ {
+ if (empty($ids)) {
+ $ids = $store->paths()->keys();
+ }
+
+ $entries = $store->withoutBlinkingEntryUris(fn () => collect($ids)->map(fn ($id) => Entry::find($id))->filter());
+
+ $entries->each(fn ($entry) => $store->cacheItem($entry));
}
public function updateEntryOrder($collection, $ids = null)
diff --git a/src/StarterKits/Installer.php b/src/StarterKits/Installer.php
index 53eecd974b..fd0500e966 100644
--- a/src/StarterKits/Installer.php
+++ b/src/StarterKits/Installer.php
@@ -45,7 +45,7 @@ final class Installer
*/
public function __construct(string $package, $console = null, ?LicenseManager $licenseManager = null)
{
- [$this->package, $this->branch] = $this->parseRawPackageArg($package);
+ $this->package = $package;
$this->licenseManager = $licenseManager;
@@ -66,17 +66,16 @@ public static function package(string $package, ?Command $console = null, ?Licen
}
/**
- * Parse out package and branch from raw package arg.
+ * Install from specific branch.
+ *
+ * @param string|null $branch
+ * @return $this
*/
- protected function parseRawPackageArg(string $package): array
+ public function branch($branch = null)
{
- $parts = explode(':', $package);
+ $this->branch = $branch;
- if (count($parts) === 1) {
- $parts[] = null;
- }
-
- return $parts;
+ return $this;
}
/**
diff --git a/src/Tokens/AbstractToken.php b/src/Tokens/AbstractToken.php
deleted file mode 100644
index 7038cce0a9..0000000000
--- a/src/Tokens/AbstractToken.php
+++ /dev/null
@@ -1,78 +0,0 @@
-token = $token ?? Generator::generate();
- $this->handler = $handler;
- $this->data = collect($data);
- $this->expiry = Carbon::now()->addHour();
- }
-
- public function token(): string
- {
- return $this->token;
- }
-
- public function handler(): string
- {
- return $this->handler;
- }
-
- public function data(): Collection
- {
- return $this->data;
- }
-
- public function get(string $key)
- {
- return $this->data->get($key);
- }
-
- public function save()
- {
- return Token::save($this);
- }
-
- public function delete()
- {
- return Token::delete($this);
- }
-
- public function handle($request, Closure $next)
- {
- return app($this->handler)->handle($this, $request, $next);
- }
-
- public function expiry(): Carbon
- {
- return $this->expiry;
- }
-
- public function expireAt(Carbon $expiry): self
- {
- $this->expiry = $expiry;
-
- return $this;
- }
-
- public function hasExpired(): bool
- {
- return $this->expiry->isPast();
- }
-}
diff --git a/src/Tokens/FileToken.php b/src/Tokens/FileToken.php
new file mode 100644
index 0000000000..28cb75d05b
--- /dev/null
+++ b/src/Tokens/FileToken.php
@@ -0,0 +1,24 @@
+token().'.yaml');
+ }
+
+ public function fileData()
+ {
+ return [
+ 'handler' => $this->handler,
+ 'expires_at' => $this->expiry->timestamp,
+ 'data' => $this->data->all(),
+ ];
+ }
+}
diff --git a/src/Tokens/FileTokenRepository.php b/src/Tokens/FileTokenRepository.php
new file mode 100644
index 0000000000..95d0d36357
--- /dev/null
+++ b/src/Tokens/FileTokenRepository.php
@@ -0,0 +1,67 @@
+makeWith(TokenContract::class, compact('token', 'handler', 'data'));
+ }
+
+ public function find(string $token): ?TokenContract
+ {
+ $path = storage_path('statamic/tokens/'.$token.'.yaml');
+
+ if (! File::exists($path)) {
+ return null;
+ }
+
+ return $this->makeFromPath($path);
+ }
+
+ public function save(TokenContract $token): bool
+ {
+ File::put(storage_path('statamic/tokens/'.$token->token().'.yaml'), $token->fileContents());
+
+ return true;
+ }
+
+ public function delete(TokenContract $token): bool
+ {
+ File::delete(storage_path('statamic/tokens/'.$token->token().'.yaml'));
+
+ return true;
+ }
+
+ public function collectGarbage(): void
+ {
+ File::getFilesByType(storage_path('statamic/tokens'), 'yaml')
+ ->map(fn ($path) => $this->makeFromPath($path))
+ ->filter->hasExpired()
+ ->each->delete();
+ }
+
+ private function makeFromPath(string $path): FileToken
+ {
+ $yaml = YAML::file($path)->parse();
+
+ $token = basename($path, '.yaml');
+
+ return $this
+ ->make($token, $yaml['handler'], $yaml['data'] ?? [])
+ ->expireAt(Carbon::createFromTimestamp($yaml['expires_at']));
+ }
+
+ public static function bindings(): array
+ {
+ return [
+ TokenContract::class => FileToken::class,
+ ];
+ }
+}
diff --git a/src/Tokens/Token.php b/src/Tokens/Token.php
index 5a403307f5..803a6faf00 100644
--- a/src/Tokens/Token.php
+++ b/src/Tokens/Token.php
@@ -2,23 +2,77 @@
namespace Statamic\Tokens;
-use Statamic\Data\ExistsAsFile;
+use Closure;
+use Facades\Statamic\Tokens\Generator;
+use Illuminate\Support\Carbon;
+use Illuminate\Support\Collection;
+use Statamic\Contracts\Tokens\Token as Contract;
+use Statamic\Facades\Token as Tokens;
-class Token extends AbstractToken
+abstract class Token implements Contract
{
- use ExistsAsFile;
+ protected $token;
+ protected $handler;
+ protected $data;
+ protected $expiry;
- public function path()
+ public function __construct(?string $token, string $handler, array $data = [])
{
- return storage_path('statamic/tokens/'.$this->token().'.yaml');
+ $this->token = $token ?? Generator::generate();
+ $this->handler = $handler;
+ $this->data = collect($data);
+ $this->expiry = Carbon::now()->addHour();
}
- public function fileData()
+ public function token(): string
{
- return [
- 'handler' => $this->handler,
- 'expires_at' => $this->expiry->timestamp,
- 'data' => $this->data->all(),
- ];
+ return $this->token;
+ }
+
+ public function handler(): string
+ {
+ return $this->handler;
+ }
+
+ public function data(): Collection
+ {
+ return $this->data;
+ }
+
+ public function get(string $key)
+ {
+ return $this->data->get($key);
+ }
+
+ public function save()
+ {
+ return Tokens::save($this);
+ }
+
+ public function delete()
+ {
+ return Tokens::delete($this);
+ }
+
+ public function handle($request, Closure $next)
+ {
+ return app($this->handler)->handle($this, $request, $next);
+ }
+
+ public function expiry(): Carbon
+ {
+ return $this->expiry;
+ }
+
+ public function expireAt(Carbon $expiry): self
+ {
+ $this->expiry = $expiry;
+
+ return $this;
+ }
+
+ public function hasExpired(): bool
+ {
+ return $this->expiry->isPast();
}
}
diff --git a/src/Tokens/TokenRepository.php b/src/Tokens/TokenRepository.php
index 33fdadfb40..982174d02e 100644
--- a/src/Tokens/TokenRepository.php
+++ b/src/Tokens/TokenRepository.php
@@ -2,58 +2,15 @@
namespace Statamic\Tokens;
-use Illuminate\Support\Carbon;
-use Statamic\Facades\File;
-use Statamic\Facades\YAML;
+use Statamic\Contracts\Tokens\Token as TokenContract;
+use Statamic\Contracts\Tokens\TokenRepository as Contract;
-class TokenRepository
+abstract class TokenRepository implements Contract
{
- public function make(?string $token, string $handler, array $data = []): Token
+ public function make(?string $token, string $handler, array $data = []): TokenContract
{
- return new Token($token, $handler, $data);
+ return app()->makeWith(TokenContract::class, compact('token', 'handler', 'data'));
}
- public function find(string $token)
- {
- $path = storage_path('statamic/tokens/'.$token.'.yaml');
-
- if (! File::exists($path)) {
- return null;
- }
-
- return $this->makeFromPath($path);
- }
-
- public function save(Token $token)
- {
- File::put(storage_path('statamic/tokens/'.$token->token().'.yaml'), $token->fileContents());
-
- return true;
- }
-
- public function delete(Token $token)
- {
- File::delete(storage_path('statamic/tokens/'.$token->token().'.yaml'));
-
- return true;
- }
-
- public function collectGarbage()
- {
- File::getFilesByType(storage_path('statamic/tokens'), 'yaml')
- ->map(fn ($path) => $this->makeFromPath($path))
- ->filter->hasExpired()
- ->each->delete();
- }
-
- private function makeFromPath(string $path): Token
- {
- $yaml = YAML::file($path)->parse();
-
- $token = basename($path, '.yaml');
-
- return $this
- ->make($token, $yaml['handler'], $yaml['data'] ?? [])
- ->expireAt(Carbon::createFromTimestamp($yaml['expires_at']));
- }
+ abstract public static function bindings(): array;
}
diff --git a/tests/Antlers/Runtime/PartialsTest.php b/tests/Antlers/Runtime/PartialsTest.php
index 8c7fa7ab53..99b5de565a 100644
--- a/tests/Antlers/Runtime/PartialsTest.php
+++ b/tests/Antlers/Runtime/PartialsTest.php
@@ -37,7 +37,7 @@ public function test_sections_work_inside_the_main_slot_content()
{
Collection::make('pages')->routes('{slug}')->save();
- EntryFactory::collection('pages')->id('1')->data(['title' => 'The Title', 'content' => 'The content'])->slug('/')->create();
+ EntryFactory::collection('pages')->id('1')->data(['title' => 'The Title', 'content' => 'The content'])->slug('test')->create();
$layout = <<<'LAYOUT'
{{ yield:test }}
@@ -59,7 +59,7 @@ public function test_sections_work_inside_the_main_slot_content()
$this->viewShouldReturnRaw('default', $default);
$this->viewShouldReturnRaw('test', $partial);
- $response = $this->get('/')->assertOk();
+ $response = $this->get('test')->assertOk();
$content = trim(StringUtilities::normalizeLineEndings($response->content()));
$expected = <<<'EXPECTED'
diff --git a/tests/Antlers/Runtime/RuntimeValuesTest.php b/tests/Antlers/Runtime/RuntimeValuesTest.php
index 8ee8ebf886..d8c627393a 100644
--- a/tests/Antlers/Runtime/RuntimeValuesTest.php
+++ b/tests/Antlers/Runtime/RuntimeValuesTest.php
@@ -20,16 +20,6 @@ public function test_supplemented_values_are_not_cached()
{
$this->withFakeViews();
- Collection::make('pages')->routes(['en' => '{slug}'])->save();
- EntryFactory::collection('pages')->id('1')->slug('home')->data(['title' => 'Home'])->create();
- EntryFactory::collection('pages')->id('2')->slug('about')->data(['title' => 'About'])->create();
-
- $template = <<<'EOT'
-{{ title }}
-
-{{ dont_cache:me_please }}{{ foo }}{{ /dont_cache:me_please }}
-EOT;
-
$instance = (new class extends Tags
{
public static $handle = 'dont_cache';
@@ -49,6 +39,16 @@ public function mePlease()
$instance::register();
+ Collection::make('pages')->routes(['en' => '{slug}'])->save();
+ EntryFactory::collection('pages')->id('1')->slug('home')->data(['title' => 'Home'])->create();
+ EntryFactory::collection('pages')->id('2')->slug('about')->data(['title' => 'About'])->create();
+
+ $template = <<<'EOT'
+{{ title }}
+
+{{ dont_cache:me_please }}{{ foo }}{{ /dont_cache:me_please }}
+EOT;
+
$this->viewShouldReturnRaw('default', $template);
$this->viewShouldReturnRaw('layout', '{{ template_content }}');
diff --git a/tests/Antlers/Runtime/TagCheckScopeTest.php b/tests/Antlers/Runtime/TagCheckScopeTest.php
index f58c5c035c..90cb15dc1a 100644
--- a/tests/Antlers/Runtime/TagCheckScopeTest.php
+++ b/tests/Antlers/Runtime/TagCheckScopeTest.php
@@ -132,9 +132,6 @@ public function test_node_processor_does_not_trash_scope_when_checking_if_someth
public function test_condition_augmentation_doesnt_reset_up_the_scope()
{
- $this->createData();
- $this->withFakeViews();
-
(new class extends Tags
{
public static $handle = 'just_a_tag';
@@ -144,6 +141,10 @@ public function index()
return [];
}
})::register();
+
+ $this->createData();
+ $this->withFakeViews();
+
$template = <<<'EOT'
{{ just_a_tag }}
diff --git a/tests/Data/Entries/EntryTest.php b/tests/Data/Entries/EntryTest.php
index 833b0dc3fd..4b1bdc26f5 100644
--- a/tests/Data/Entries/EntryTest.php
+++ b/tests/Data/Entries/EntryTest.php
@@ -630,20 +630,22 @@ public function a_localized_entry_in_a_structured_collection_without_a_route_for
$this->assertNull($entry->url());
}
- /** @test */
- public function it_gets_urls_for_first_child_redirects()
+ /**
+ * @test
+ *
+ * @dataProvider firstChildRedirectProvider
+ */
+ public function it_gets_urls_for_first_child_redirects($value)
{
- \Event::fake(); // Don't invalidate static cache etc when saving entries.
-
$this->setSites([
'en' => ['url' => 'http://domain.com/', 'locale' => 'en_US'],
]);
$collection = tap((new Collection)->handle('pages')->routes('{parent_uri}/{slug}'))->save();
- $parent = tap((new Entry)->id('1')->locale('en')->collection($collection)->slug('parent')->set('redirect', '@child'))->save();
+ $parent = tap((new Entry)->id('1')->locale('en')->collection($collection)->slug('parent')->set('redirect', $value))->save();
$child = tap((new Entry)->id('2')->locale('en')->collection($collection)->slug('child'))->save();
- $noChildren = tap((new Entry)->id('3')->locale('en')->collection($collection)->slug('nochildren')->set('redirect', '@child'))->save();
+ $noChildren = tap((new Entry)->id('3')->locale('en')->collection($collection)->slug('nochildren')->set('redirect', $value))->save();
$collection->structureContents([
'expects_root' => false, // irrelevant. just can't pass an empty array at the moment.
@@ -684,6 +686,14 @@ public function it_gets_urls_for_first_child_redirects()
$this->assertEquals(404, $noChildren->redirectUrl());
}
+ public static function firstChildRedirectProvider()
+ {
+ return [
+ 'string' => ['@child'],
+ 'array' => [['url' => '@child']],
+ ];
+ }
+
/** @test */
public function it_gets_and_sets_supplemental_data()
{
@@ -1344,7 +1354,7 @@ public function when_saving_quietly_the_cached_entrys_withEvents_flag_will_be_se
$entry->saveQuietly();
- $cached = Cache::get('stache::items::entries::blog::1');
+ $cached = Cache::get('stache::items::entries::blog::1')['entry'];
$reflection = new ReflectionClass($cached);
$property = $reflection->getProperty('withEvents');
$property->setAccessible(true);
@@ -1363,8 +1373,11 @@ public function it_clears_blink_caches_when_saving()
$mock->shouldReceive('store')->with('structure-uris')->once()->andReturn(
$this->mock(\Spatie\Blink\Blink::class)->shouldReceive('forget')->with('a')->once()->getMock()
);
- $mock->shouldReceive('store')->with('structure-entries')->once()->andReturn(
- $this->mock(\Spatie\Blink\Blink::class)->shouldReceive('forget')->with('a')->once()->getMock()
+ $mock->shouldReceive('store')->with('structure-entries')->twice()->andReturn(
+ tap($this->mock(\Spatie\Blink\Blink::class), function ($m) {
+ $m->shouldReceive('forget')->with('a')->once();
+ $m->shouldReceive('put')->once();
+ })
);
$entry->save();
diff --git a/tests/Data/Structures/TreeTest.php b/tests/Data/Structures/TreeTest.php
index 101bc90f9f..34e9511936 100644
--- a/tests/Data/Structures/TreeTest.php
+++ b/tests/Data/Structures/TreeTest.php
@@ -99,7 +99,7 @@ public function it_gets_the_parent()
$parent = $tree->parent();
$this->assertInstanceOf(Page::class, $parent);
- $this->assertEquals(Entry::find('pages-home'), $parent->entry());
+ $this->assertEquals(Entry::find('pages-home')->id(), $parent->entry()->id());
}
/** @test */
diff --git a/tests/Feature/Entries/MountingTest.php b/tests/Feature/Entries/MountingTest.php
index 54800821e7..4915bb164a 100644
--- a/tests/Feature/Entries/MountingTest.php
+++ b/tests/Feature/Entries/MountingTest.php
@@ -28,15 +28,15 @@ public function updating_a_mounted_page_will_update_the_uris_for_each_entry_in_t
$one = EntryFactory::collection('blog')->slug('one')->create();
$two = EntryFactory::collection('blog')->slug('two')->create();
- $this->assertEquals($one, Entry::findByUri('/pages/blog/one'));
- $this->assertEquals($two, Entry::findByUri('/pages/blog/two'));
+ $this->assertEquals($one->id(), Entry::findByUri('/pages/blog/one')->id());
+ $this->assertEquals($two->id(), Entry::findByUri('/pages/blog/two')->id());
$mount->slug('diary')->save();
$this->assertNull(Entry::findByUri('/pages/blog/one'));
$this->assertNull(Entry::findByUri('/pages/blog/two'));
- $this->assertEquals($one, Entry::findByUri('/pages/diary/one'));
- $this->assertEquals($two, Entry::findByUri('/pages/diary/two'));
+ $this->assertEquals($one->id(), Entry::findByUri('/pages/diary/one')->id());
+ $this->assertEquals($two->id(), Entry::findByUri('/pages/diary/two')->id());
}
/** @test */
@@ -54,8 +54,8 @@ public function updating_a_mounted_page_will_not_update_the_uris_when_slug_is_cl
$one = EntryFactory::collection('blog')->slug('one')->create();
$two = EntryFactory::collection('blog')->slug('two')->create();
- $this->assertEquals($one, Entry::findByUri('/pages/blog/one'));
- $this->assertEquals($two, Entry::findByUri('/pages/blog/two'));
+ $this->assertEquals($one->id(), Entry::findByUri('/pages/blog/one')->id());
+ $this->assertEquals($two->id(), Entry::findByUri('/pages/blog/two')->id());
// Since we're just saving the mount without changing the slug, we don't want to update the URIs.
$mock = \Mockery::mock(Collection::getFacadeRoot())->makePartial();
diff --git a/tests/Feature/Navigation/EditNavigationTest.php b/tests/Feature/Navigation/EditNavigationTest.php
index bbd9db838c..f29e9e4bc3 100644
--- a/tests/Feature/Navigation/EditNavigationTest.php
+++ b/tests/Feature/Navigation/EditNavigationTest.php
@@ -15,13 +15,13 @@ class EditNavigationTest extends TestCase
use PreventSavingStacheItemsToDisk;
/** @test */
- public function it_shows_the_edit_form_if_user_has_edit_permission()
+ public function it_shows_the_edit_form_if_user_has_configure_permission()
{
$nav = $this->createNav('foo');
Nav::shouldReceive('all')->andReturn(collect([$nav]));
Nav::shouldReceive('find')->andReturn($nav);
- $this->setTestRoles(['test' => ['access cp', 'edit foo nav']]);
+ $this->setTestRoles(['test' => ['access cp', 'configure navs']]);
$user = Facades\User::make()->assignRole('test')->save();
$response = $this
@@ -32,7 +32,7 @@ public function it_shows_the_edit_form_if_user_has_edit_permission()
}
/** @test */
- public function it_denies_access_if_user_doesnt_have_edit_permission()
+ public function it_denies_access_if_user_doesnt_have_configure_permission()
{
$nav = $this->createNav('foo');
Nav::shouldReceive('all')->andReturn(collect([$nav]));
diff --git a/tests/Licensing/OutpostTest.php b/tests/Licensing/OutpostTest.php
index bc4a15fa0c..efe2a26243 100644
--- a/tests/Licensing/OutpostTest.php
+++ b/tests/Licensing/OutpostTest.php
@@ -8,8 +8,10 @@
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
+use Illuminate\Encryption\Encrypter;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\File;
use Statamic\Facades\Addon;
use Statamic\Licensing\Outpost;
use Tests\TestCase;
@@ -30,8 +32,8 @@ public function it_builds_the_request_payload()
config(['statamic.editions.pro' => true]);
Addon::shouldReceive('all')->once()->andReturn(collect([
- new FakeOutpostAddon('foo/bar', '1.2.3', null),
- new FakeOutpostAddon('baz/qux', '4.5.6', 'example'),
+ new FakeOutpostAddon('foo/bar', '1.2.3', null, true, true),
+ new FakeOutpostAddon('baz/qux', '4.5.6', 'example', true, true),
]));
request()->server->set('SERVER_ADDR', '123.123.123.123');
@@ -88,6 +90,74 @@ public function the_cached_response_is_used()
$this->assertSame($first, $second);
}
+ /** @test */
+ public function license_key_file_is_used_when_it_exists()
+ {
+ config(['statamic.system.license_key' => 'testsitekey12345']);
+
+ $encrypter = new Encrypter('testsitekey12345');
+ $encryptedKeyFile = $encrypter->encrypt(json_encode([
+ 'foo' => 'bar',
+ 'packages' => [],
+ ]));
+
+ File::shouldReceive('exists')
+ ->with(storage_path('license.key'))
+ ->once()
+ ->andReturnTrue();
+
+ File::shouldReceive('get')
+ ->with(storage_path('license.key'))
+ ->once()
+ ->andReturn($encryptedKeyFile);
+
+ $outpost = $this->outpostWithJsonResponse(['newer' => 'response']);
+ $response = $outpost->response();
+
+ $this->assertArraySubset([
+ 'foo' => 'bar',
+ 'packages' => [],
+ ], $response);
+ }
+
+ /** @test */
+ public function license_key_file_response_merges_installed_addons_into_response()
+ {
+ config(['statamic.system.license_key' => 'testsitekey12345']);
+
+ $encrypter = new Encrypter('testsitekey12345');
+ $encryptedKeyFile = $encrypter->encrypt(json_encode(['packages' => [
+ 'foo/bar' => ['valid' => true, 'exists' => true, 'version_limit' => null],
+ ]]));
+
+ File::shouldReceive('exists')
+ ->with(storage_path('license.key'))
+ ->once()
+ ->andReturnTrue();
+
+ File::shouldReceive('get')
+ ->with(storage_path('license.key'))
+ ->once()
+ ->andReturn($encryptedKeyFile);
+
+ Addon::shouldReceive('all')->andReturn(collect([
+ (new FakeOutpostAddon('foo/bar', '1.2.3', null, true, true)),
+ (new FakeOutpostAddon('bar/baz', '1.2.3', null, true, true)),
+ (new FakeOutpostAddon('private/addon', '1.2.3', null, false, false)),
+ ]));
+
+ $outpost = $this->outpostWithJsonResponse(['newer' => 'response']);
+ $response = $outpost->response();
+
+ $this->assertArraySubset([
+ 'packages' => [
+ 'foo/bar' => ['valid' => true, 'exists' => true, 'version_limit' => null],
+ 'bar/baz' => ['valid' => false, 'exists' => true, 'version_limit' => null],
+ 'private/addon' => ['valid' => true, 'exists' => false, 'version_limit' => null],
+ ],
+ ], $response);
+ }
+
/** @test */
public function the_cached_response_is_ignored_if_the_payload_is_different()
{
@@ -257,12 +327,16 @@ class FakeOutpostAddon
protected $package;
protected $version;
protected $edition;
+ protected $existsOnMarketplace;
+ protected $isCommercial;
- public function __construct($package, $version, $edition)
+ public function __construct($package, $version, $edition, $existsOnMarketplace, $isCommercial)
{
$this->package = $package;
$this->version = $version;
$this->edition = $edition;
+ $this->existsOnMarketplace = $existsOnMarketplace;
+ $this->isCommercial = $isCommercial;
}
public function package()
@@ -279,4 +353,14 @@ public function edition()
{
return $this->edition;
}
+
+ public function existsOnMarketplace()
+ {
+ return $this->existsOnMarketplace;
+ }
+
+ public function isCommercial()
+ {
+ return $this->isCommercial;
+ }
}
diff --git a/tests/Listeners/UpdateAssetReferencesTest.php b/tests/Listeners/UpdateAssetReferencesTest.php
index 5b02193bfc..acfa6858c3 100644
--- a/tests/Listeners/UpdateAssetReferencesTest.php
+++ b/tests/Listeners/UpdateAssetReferencesTest.php
@@ -295,6 +295,34 @@ public function it_updates_assets_fields_regardless_of_max_files_setting()
$this->assertEquals('surfboard.jpg', $entry->fresh()->get('products'));
}
+ /** @test */
+ public function it_updates_multi_assets_fields_even_when_existing_field_value_is_null()
+ {
+ $collection = tap(Facades\Collection::make('articles'))->save();
+
+ $this->setInBlueprints('collections/articles', [
+ 'fields' => [
+ [
+ 'handle' => 'pics',
+ 'field' => [
+ 'type' => 'assets',
+ 'container' => 'test_container',
+ ],
+ ],
+ ],
+ ]);
+
+ $entry = tap(Facades\Entry::make()->collection($collection)->data([
+ 'pics' => null,
+ ]))->save();
+
+ $this->assertNull($entry->get('pics'));
+
+ $this->assetNorris->path('content/norris.jpg')->save();
+
+ $this->assertNull($entry->fresh()->get('pics'));
+ }
+
/** @test */
public function it_nullifies_references_when_deleting_an_asset()
{
diff --git a/tests/Routing/RoutesTest.php b/tests/Routing/RoutesTest.php
index 010e30d710..e5b07ae568 100644
--- a/tests/Routing/RoutesTest.php
+++ b/tests/Routing/RoutesTest.php
@@ -39,6 +39,10 @@ protected function resolveApplicationConfiguration($app)
Route::statamic('/route/with/placeholders/{foo}/{bar}/{baz}', 'test');
+ Route::statamic('/route/with/placeholders/closure/{foo}/{bar}/{baz}', 'test', function ($foo, $bar, $baz) {
+ return ['hello' => "$foo $bar $baz"];
+ });
+
Route::statamic('/route-with-custom-layout', 'test', [
'layout' => 'custom-layout',
'hello' => 'world',
@@ -141,6 +145,17 @@ public function it_renders_a_view_with_placeholders()
->assertSee('Hello one two three');
}
+ /** @test */
+ public function it_renders_a_view_with_placeholders_and_data_from_a_closure()
+ {
+ $this->viewShouldReturnRaw('layout', '{{ template_content }}');
+ $this->viewShouldReturnRaw('test', 'Hello {{ hello }}');
+
+ $this->get('/route/with/placeholders/closure/one/two/three')
+ ->assertOk()
+ ->assertSee('Hello one two three');
+ }
+
/** @test */
public function it_renders_a_view_with_custom_layout()
{
diff --git a/tests/Stache/Stores/EntriesStoreTest.php b/tests/Stache/Stores/EntriesStoreTest.php
index 4191f810c4..c9f446eaf2 100644
--- a/tests/Stache/Stores/EntriesStoreTest.php
+++ b/tests/Stache/Stores/EntriesStoreTest.php
@@ -106,7 +106,7 @@ public function it_makes_entry_instances_from_files()
public function if_slugs_are_not_required_the_filename_still_becomes_the_slug()
{
Facades\Collection::shouldReceive('findByHandle')->with('blog')->andReturn(
- (new \Statamic\Entries\Collection)->requiresSlugs(false)
+ (new \Statamic\Entries\Collection)->handle('blog')->requiresSlugs(false)
);
$item = $this->parent->store('blog')->makeItemFromFile(
@@ -122,7 +122,7 @@ public function if_slugs_are_not_required_the_filename_still_becomes_the_slug()
public function if_slugs_are_not_required_and_the_filename_is_the_same_as_the_id_then_slug_is_null()
{
Facades\Collection::shouldReceive('findByHandle')->with('blog')->andReturn(
- (new \Statamic\Entries\Collection)->requiresSlugs(false)
+ (new \Statamic\Entries\Collection)->handle('blog')->requiresSlugs(false)
);
$item = $this->parent->store('blog')->makeItemFromFile(
@@ -138,7 +138,7 @@ public function if_slugs_are_not_required_and_the_filename_is_the_same_as_the_id
public function if_slugs_are_required_and_the_filename_is_the_same_as_the_id_then_slug_is_the_id()
{
Facades\Collection::shouldReceive('findByHandle')->with('blog')->andReturn(
- (new \Statamic\Entries\Collection)->requiresSlugs(true)
+ (new \Statamic\Entries\Collection)->handle('blog')->requiresSlugs(true)
);
$item = $this->parent->store('blog')->makeItemFromFile(
diff --git a/tests/StarterKits/InstallTest.php b/tests/StarterKits/InstallTest.php
index fdbc2873eb..bf611c5a12 100644
--- a/tests/StarterKits/InstallTest.php
+++ b/tests/StarterKits/InstallTest.php
@@ -682,6 +682,29 @@ public function it_parses_branch_from_package_param_when_installing()
$this->assertFileExists(base_path('copied.md'));
}
+ /** @test */
+ public function it_installs_branch_with_slash_without_failing_package_validation()
+ {
+ $this->assertFileDoesNotExist($this->kitVendorPath());
+ $this->assertComposerJsonDoesntHave('repositories');
+ $this->assertFileDoesNotExist(base_path('copied.md'));
+
+ $this->installCoolRunnings([
+ 'package' => 'statamic/cool-runnings:dev-feature/custom-branch',
+ ]);
+
+ // Ensure `Composer::requireDev()` gets called with `package:branch`
+ $this->assertEquals(Blink::get('composer-require-dev-package'), 'statamic/cool-runnings');
+ $this->assertEquals(Blink::get('composer-require-dev-branch'), 'dev-feature/custom-branch');
+
+ // But ensure the rest of the installer handles parsed `package` without branch messing things up
+ $this->assertFalse(Blink::has('starter-kit-repository-added'));
+ $this->assertFileDoesNotExist($this->kitVendorPath());
+ $this->assertFileDoesNotExist(base_path('composer.json.bak'));
+ $this->assertComposerJsonDoesntHave('repositories');
+ $this->assertFileExists(base_path('copied.md'));
+ }
+
private function kitRepoPath($path = null)
{
return collect([base_path('repo/cool-runnings'), $path])->filter()->implode('/');
diff --git a/tests/Tokens/TokenRepositoryTest.php b/tests/Tokens/TokenRepositoryTest.php
index 8d11a98158..35adf65fb5 100644
--- a/tests/Tokens/TokenRepositoryTest.php
+++ b/tests/Tokens/TokenRepositoryTest.php
@@ -7,7 +7,7 @@
use Illuminate\Support\Collection;
use Statamic\Contracts\Tokens\Token;
use Statamic\Facades\File;
-use Statamic\Tokens\TokenRepository;
+use Statamic\Tokens\FileTokenRepository;
use Tests\TestCase;
class TokenRepositoryTest extends TestCase
@@ -18,7 +18,7 @@ public function setUp(): void
{
parent::setUp();
- $this->tokens = new TokenRepository;
+ $this->tokens = new FileTokenRepository;
}
/** @test */
diff --git a/tests/Tokens/TokenTest.php b/tests/Tokens/TokenTest.php
index f56eac71c0..cecda739c8 100644
--- a/tests/Tokens/TokenTest.php
+++ b/tests/Tokens/TokenTest.php
@@ -5,7 +5,7 @@
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Statamic\Facades;
-use Statamic\Tokens\Token;
+use Statamic\Tokens\FileToken as Token;
use Tests\TestCase;
class TokenTest extends TestCase