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