diff --git a/cmd/extension/extension_zip.go b/cmd/extension/extension_zip.go
index 9b487d4..57b954c 100644
--- a/cmd/extension/extension_zip.go
+++ b/cmd/extension/extension_zip.go
@@ -162,6 +162,14 @@ var extensionZipCmd = &cobra.Command{
}
}
+ if err := extension.BuildModifier(ext, extDir, extension.BuildModifierConfig{
+ AppBackendUrl: getStringOnStringError(cmd.Flags().GetString("overwrite-app-backend-url")),
+ AppBackendSecret: getStringOnStringError(cmd.Flags().GetString("overwrite-app-backend-secret")),
+ Version: getStringOnStringError(cmd.Flags().GetString("overwrite-version")),
+ }); err != nil {
+ return fmt.Errorf("build modifier: %w", err)
+ }
+
fileName := fmt.Sprintf("%s-%s.zip", name, tag)
if len(tag) == 0 {
fileName = fmt.Sprintf("%s.zip", name)
@@ -197,10 +205,17 @@ func init() {
extensionRootCmd.AddCommand(extensionZipCmd)
extensionZipCmd.Flags().BoolVar(&disableGit, "disable-git", false, "Use the source folder as it is")
extensionZipCmd.Flags().BoolVar(&extensionReleaseMode, "release", false, "Release mode (remove app secrets)")
+ extensionZipCmd.Flags().String("overwrite-app-backend-url", "", "Change all URLs in manifest.xml to this URL")
+ extensionZipCmd.Flags().String("overwrite-app-backend-secret", "", "Change the secret to this value")
+ extensionZipCmd.Flags().String("overwrite-version", "", "Change the extension version to this value")
extensionZipCmd.Flags().String("output-directory", "", "Output directory for the zip file")
extensionZipCmd.Flags().String("git-commit", "", "Commit Hash / Tag to use")
}
+func getStringOnStringError(val string, _ error) string {
+ return val
+}
+
func executeHooks(ext extension.Extension, hooks []string, extDir string) error {
env := []string{
fmt.Sprintf("EXTENSION_DIR=%s", extDir),
diff --git a/extension/_fixtures/istorier.xml b/extension/_fixtures/istorier.xml
new file mode 100644
index 0000000..418cbef
--- /dev/null
+++ b/extension/_fixtures/istorier.xml
@@ -0,0 +1,91 @@
+
+
+
+ InstoImmersiveElements
+
+ Transform your online store into an unforgettable brand experience. As an incredibly cost-effective alternative to external resources, the app is engineered to boost conversions.
+ Instorier AS
+ (c) by Instorier AS
+ 1.1.0
+ Resources/config/plugin.png
+ Proprietary
+
+
+
+ https://instorier.apps.shopware.io/app/lifecycle/register
+
+
+
+ https://instorier.apps.shopware.io/iframe
+
+
+ cms_slot
+ cms_slot_translation
+ language
+ sales_channel
+ sales_channel_domain
+ customer
+ newsletter_recipient
+ order
+ category_translation
+ country_state_translation
+ country_translation
+ currency_translation
+ customer_group_translation
+ locale_translation
+ media
+ media_default_folder
+ media_translation
+ payment_method_translation
+ product_manufacturer_translation
+ product_translation
+ shipping_method_translation
+ unit_translation
+ property_group_translation
+ property_group_option_translation
+ sales_channel_translation
+ sales_channel_type_translation
+ salutation_translation
+ plugin_translation
+ product_stream_translation
+ state_machine_translation
+ state_machine_state_translation
+ cms_page_translation
+ mail_template_translation
+ mail_header_footer_translation
+ document_type_translation
+ number_range_type_translation
+ delivery_time_translation
+ product_search_keyword
+ product_keyword_dictionary
+ mail_template_type_translation
+ promotion_translation
+ number_range_translation
+ product_review
+ seo_url
+ tax_rule_type_translation
+ product_cross_selling_translation
+ import_export_profile_translation
+ product_sorting_translation
+ product_feature_set_translation
+ app_translation
+ app_action_button_translation
+ landing_page_translation
+ app_cms_block_translation
+ app_script_condition_translation
+ app_flow_action_translation
+ tax_provider_translation
+ theme_translation
+
+ cms_slot
+ cms_slot_translation
+ media
+ media_translation
+
+ cms_slot
+ cms_slot_translation
+
+ cms_slot
+ cms_slot_translation
+
+
\ No newline at end of file
diff --git a/extension/app.go b/extension/app.go
index 2df4576..9a4b7d5 100644
--- a/extension/app.go
+++ b/extension/app.go
@@ -11,184 +11,9 @@ import (
"github.com/FriendsOfShopware/shopware-cli/version"
)
-type translatedXmlNode []struct {
- Text string `xml:",chardata"`
- Lang string `xml:"lang,attr"`
-}
-
-type appManifest struct {
- XMLName xml.Name `xml:"manifest"`
- Text string `xml:",chardata"`
- Xsi string `xml:"xsi,attr"`
- NoNamespaceSchemaLocation string `xml:"noNamespaceSchemaLocation,attr"`
- Meta appManifestMeta `xml:"meta"`
- Setup struct {
- Text string `xml:",chardata"`
- RegistrationUrl string `xml:"registrationUrl"`
- Secret string `xml:"secret"`
- } `xml:"setup"`
- Permissions struct {
- Text string `xml:",chardata"`
- Read string `xml:"read"`
- Create string `xml:"create"`
- Update string `xml:"update"`
- Delete string `xml:"delete"`
- } `xml:"permissions"`
- Webhooks struct {
- Text string `xml:",chardata"`
- Webhook struct {
- Text string `xml:",chardata"`
- Name string `xml:"name,attr"`
- URL string `xml:"url,attr"`
- Event string `xml:"event,attr"`
- } `xml:"webhook"`
- } `xml:"webhooks"`
- Admin struct {
- Text string `xml:",chardata"`
- Module []struct {
- Text string `xml:",chardata"`
- Name string `xml:"name,attr"`
- Parent string `xml:"parent,attr"`
- Position string `xml:"position,attr"`
- Source string `xml:"source,attr"`
- Label []struct {
- Text string `xml:",chardata"`
- Lang string `xml:"lang,attr"`
- } `xml:"label"`
- } `xml:"module"`
- MainModule struct {
- Text string `xml:",chardata"`
- Source string `xml:"source,attr"`
- } `xml:"main-module"`
- ActionButton []struct {
- Text string `xml:",chardata"`
- Action string `xml:"action,attr"`
- Entity string `xml:"entity,attr"`
- View string `xml:"view,attr"`
- URL string `xml:"url,attr"`
- Label string `xml:"label"`
- } `xml:"action-button"`
- } `xml:"admin"`
- CustomFields struct {
- Text string `xml:",chardata"`
- CustomFieldSet struct {
- Text string `xml:",chardata"`
- Name string `xml:"name"`
- Label []struct {
- Text string `xml:",chardata"`
- Lang string `xml:"lang,attr"`
- } `xml:"label"`
- RelatedEntities struct {
- Text string `xml:",chardata"`
- Order string `xml:"order"`
- } `xml:"related-entities"`
- Fields struct {
- Chardata string `xml:",chardata"`
- Text struct {
- Text string `xml:",chardata"`
- Name string `xml:"name,attr"`
- Label string `xml:"label"`
- Position string `xml:"position"`
- Required string `xml:"required"`
- HelpText string `xml:"help-text"`
- } `xml:"text"`
- Float struct {
- Text string `xml:",chardata"`
- Name string `xml:"name,attr"`
- Label []struct {
- Text string `xml:",chardata"`
- Lang string `xml:"lang,attr"`
- } `xml:"label"`
- HelpText string `xml:"help-text"`
- Position string `xml:"position"`
- Placeholder string `xml:"placeholder"`
- Min string `xml:"min"`
- Max string `xml:"max"`
- Steps string `xml:"steps"`
- } `xml:"float"`
- } `xml:"fields"`
- } `xml:"custom-field-set"`
- } `xml:"custom-fields"`
- Cookies struct {
- Text string `xml:",chardata"`
- Cookie struct {
- Text string `xml:",chardata"`
- Cookie string `xml:"cookie"`
- SnippetName string `xml:"snippet-name"`
- SnippetDescription string `xml:"snippet-description"`
- Value string `xml:"value"`
- Expiration string `xml:"expiration"`
- } `xml:"cookie"`
- Group struct {
- Text string `xml:",chardata"`
- SnippetName string `xml:"snippet-name"`
- SnippetDescription string `xml:"snippet-description"`
- Entries struct {
- Text string `xml:",chardata"`
- Cookie struct {
- Text string `xml:",chardata"`
- Cookie string `xml:"cookie"`
- SnippetName string `xml:"snippet-name"`
- SnippetDescription string `xml:"snippet-description"`
- Value string `xml:"value"`
- Expiration string `xml:"expiration"`
- } `xml:"cookie"`
- } `xml:"entries"`
- } `xml:"group"`
- } `xml:"cookies"`
- Payments struct {
- Text string `xml:",chardata"`
- PaymentMethod struct {
- Text string `xml:",chardata"`
- Identifier string `xml:"identifier"`
- Name []struct {
- Text string `xml:",chardata"`
- Lang string `xml:"lang,attr"`
- } `xml:"name"`
- Description []struct {
- Text string `xml:",chardata"`
- Lang string `xml:"lang,attr"`
- } `xml:"description"`
- PayURL string `xml:"pay-url"`
- FinalizeURL string `xml:"finalize-url"`
- Icon string `xml:"icon"`
- } `xml:"payment-method"`
- } `xml:"payments"`
-}
-
-type appManifestMeta struct {
- Text string `xml:",chardata"`
- Name string `xml:"name"`
- Label translatedXmlNode `xml:"label"`
- Description translatedXmlNode `xml:"description"`
- Author string `xml:"author"`
- Copyright string `xml:"copyright"`
- Version string `xml:"version"`
- License string `xml:"license"`
- Icon string `xml:"icon"`
- Privacy string `xml:"privacy"`
- Compatibility string `xml:"compatibility"`
- PrivacyPolicyExtensions []struct {
- Text string `xml:",chardata"`
- Lang string `xml:"lang,attr"`
- } `xml:"privacyPolicyExtensions"`
-}
-
-func getTranslatedTextFromXmlNode(node translatedXmlNode, keys []string) string {
- for _, n := range node {
- for _, key := range keys {
- if n.Lang == key {
- return n.Text
- }
- }
- }
-
- return ""
-}
-
type App struct {
path string
- manifest appManifest
+ manifest Manifest
config *Config
}
@@ -212,7 +37,7 @@ func newApp(path string) (*App, error) {
return nil, fmt.Errorf("newApp: %v", err)
}
- var manifest appManifest
+ var manifest Manifest
err = xml.Unmarshal(appFile, &manifest)
if err != nil {
@@ -294,12 +119,12 @@ func (a App) GetMetaData() *extensionMetadata {
return &extensionMetadata{
Label: extensionTranslated{
- German: getTranslatedTextFromXmlNode(a.manifest.Meta.Label, german),
- English: getTranslatedTextFromXmlNode(a.manifest.Meta.Label, english),
+ German: a.manifest.Meta.Label.GetValueByLanguage(german),
+ English: a.manifest.Meta.Label.GetValueByLanguage(english),
},
Description: extensionTranslated{
- German: getTranslatedTextFromXmlNode(a.manifest.Meta.Description, german),
- English: getTranslatedTextFromXmlNode(a.manifest.Meta.Description, english),
+ German: a.manifest.Meta.Description.GetValueByLanguage(german),
+ English: a.manifest.Meta.Description.GetValueByLanguage(english),
},
}
}
diff --git a/extension/asset_test.go b/extension/asset_test.go
index 19ac53f..201d69c 100644
--- a/extension/asset_test.go
+++ b/extension/asset_test.go
@@ -31,8 +31,8 @@ func TestConvertApp(t *testing.T) {
app := App{
path: t.TempDir(),
config: &Config{},
- manifest: appManifest{
- Meta: appManifestMeta{
+ manifest: Manifest{
+ Meta: Meta{
Name: "TestApp",
},
},
@@ -50,8 +50,8 @@ func TestConvertApp(t *testing.T) {
func TestConvertExtraBundlesOfConfig(t *testing.T) {
app := App{
path: t.TempDir(),
- manifest: appManifest{
- Meta: appManifestMeta{
+ manifest: Manifest{
+ Meta: Meta{
Name: "TestApp",
},
},
@@ -83,8 +83,8 @@ func TestConvertExtraBundlesOfConfig(t *testing.T) {
func TestConvertExtraBundlesOfConfigWithOverride(t *testing.T) {
app := App{
path: t.TempDir(),
- manifest: appManifest{
- Meta: appManifestMeta{
+ manifest: Manifest{
+ Meta: Meta{
Name: "TestApp",
},
},
diff --git a/extension/build_modifier.go b/extension/build_modifier.go
new file mode 100644
index 0000000..bdefb99
--- /dev/null
+++ b/extension/build_modifier.go
@@ -0,0 +1,176 @@
+package extension
+
+import (
+ "encoding/json"
+ "encoding/xml"
+ "fmt"
+ "net/url"
+ "os"
+ "path"
+)
+
+type BuildModifierConfig struct {
+ AppBackendUrl string
+ AppBackendSecret string
+ Version string
+}
+
+func BuildModifier(ext Extension, extensionRoot string, config BuildModifierConfig) error {
+ if (config.AppBackendUrl != "" || config.AppBackendSecret != "" || config.Version != "") && ext.GetType() == TypePlatformApp {
+ manifestBytes, _ := os.ReadFile(path.Join(extensionRoot, "manifest.xml"))
+
+ var manifest Manifest
+
+ if err := xml.Unmarshal(manifestBytes, &manifest); err != nil {
+ return fmt.Errorf("could not parse manifest.xml: %w", err)
+ }
+
+ if config.Version != "" {
+ manifest.Meta.Version = config.Version
+ }
+
+ if config.AppBackendSecret != "" && manifest.Setup != nil {
+ manifest.Setup.Secret = config.AppBackendSecret
+ }
+
+ if config.AppBackendUrl != "" {
+ if err := replaceUrlsInManifest(config, manifest); err != nil {
+ return err
+ }
+ }
+
+ newXml, err := xml.MarshalIndent(manifest, "", " ")
+
+ if err != nil {
+ return fmt.Errorf("could not marshal manifest.xml: %w", err)
+ }
+
+ if err := os.WriteFile(path.Join(extensionRoot, "manifest.xml"), newXml, os.ModePerm); err != nil {
+ return fmt.Errorf("could not write manifest.xml: %w", err)
+ }
+ }
+
+ if config.Version != "" && ext.GetType() == TypePlatformPlugin {
+ composerJson, err := os.ReadFile(path.Join(extensionRoot, "composer.json"))
+
+ if err != nil {
+ return fmt.Errorf("could not read composer.json: %w", err)
+ }
+
+ var composerJsonStruct map[string]interface{}
+
+ if err := json.Unmarshal(composerJson, &composerJsonStruct); err != nil {
+ return fmt.Errorf("could not unmarshal composer.json: %w", err)
+ }
+
+ composerJsonStruct["version"] = config.Version
+
+ newComposerJson, err := json.MarshalIndent(composerJsonStruct, "", " ")
+
+ if err != nil {
+ return fmt.Errorf("could not marshal composer.json: %w", err)
+ }
+
+ if err := os.WriteFile(path.Join(extensionRoot, "composer.json"), newComposerJson, os.ModePerm); err != nil {
+ return fmt.Errorf("could not write manifest.xml: %w", err)
+ }
+ }
+
+ return nil
+}
+
+func replaceUrlsInManifest(config BuildModifierConfig, manifest Manifest) error {
+ newBackendUrl, err := url.Parse(config.AppBackendUrl)
+
+ if err != nil {
+ return fmt.Errorf("could not parse app backend url: %w", err)
+ }
+
+ if manifest.Setup != nil {
+ if err := replaceUrl(&manifest.Setup.RegistrationUrl, newBackendUrl); err != nil {
+ return fmt.Errorf("could not replace app backend url: %w", err)
+ }
+ }
+
+ if manifest.Admin != nil {
+ if err := replaceUrl(&manifest.Admin.BaseAppUrl, newBackendUrl); err != nil {
+ return fmt.Errorf("could not replace app backend url: %w", err)
+ }
+
+ for index, button := range manifest.Admin.ActionButton {
+ if err := replaceUrl(&button.URL, newBackendUrl); err != nil {
+ return fmt.Errorf("could not replace action button url on index %d: %w", index, err)
+ }
+ }
+ }
+
+ if manifest.Gateways != nil {
+ if err := replaceUrl(&manifest.Gateways.Checkout, newBackendUrl); err != nil {
+ return fmt.Errorf("could not replace checkout gateway url: %w", err)
+ }
+ }
+
+ if manifest.Payments != nil {
+ for _, payment := range manifest.Payments.PaymentMethod {
+ if err := replaceUrl(&payment.RefundURL, newBackendUrl); err != nil {
+ return fmt.Errorf("could not replace refund url: %w", err)
+ }
+
+ if err := replaceUrl(&payment.CaptureURL, newBackendUrl); err != nil {
+ return fmt.Errorf("could not replace capture url: %w", err)
+ }
+
+ if err := replaceUrl(&payment.FinalizeURL, newBackendUrl); err != nil {
+ return fmt.Errorf("could not replace finanlize url: %w", err)
+ }
+
+ if err := replaceUrl(&payment.PayURL, newBackendUrl); err != nil {
+ return fmt.Errorf("could not replace pay url: %w", err)
+ }
+
+ if err := replaceUrl(&payment.RecurringURL, newBackendUrl); err != nil {
+ return fmt.Errorf("could not replace recurring url: %w", err)
+ }
+
+ if err := replaceUrl(&payment.ValidateURL, newBackendUrl); err != nil {
+ return fmt.Errorf("could not replace validate url: %w", err)
+ }
+ }
+ }
+
+ if manifest.Tax != nil {
+ for _, tax := range manifest.Tax.TaxProvider {
+ if err := replaceUrl(&tax.ProcessURL, newBackendUrl); err != nil {
+ return fmt.Errorf("could not replace tax provider url: %w", err)
+ }
+ }
+ }
+
+ if manifest.Webhooks != nil {
+ for _, webhook := range manifest.Webhooks.Webhook {
+ if err := replaceUrl(&webhook.URL, newBackendUrl); err != nil {
+ return fmt.Errorf("could not replace webhook url: %w", err)
+ }
+ }
+ }
+ return nil
+}
+
+func replaceUrl(registrationUrl *string, backendUrl *url.URL) error {
+ if registrationUrl == nil || *registrationUrl == "" {
+ return nil
+ }
+
+ currentUrl, err := url.Parse(*registrationUrl)
+
+ if err != nil {
+ return fmt.Errorf("could not parse url: %w", err)
+ }
+
+ currentUrl.Scheme = backendUrl.Scheme
+ currentUrl.Host = backendUrl.Host
+
+ *registrationUrl = currentUrl.String()
+
+ return nil
+}
diff --git a/extension/build_modifier_test.go b/extension/build_modifier_test.go
new file mode 100644
index 0000000..aaab3ff
--- /dev/null
+++ b/extension/build_modifier_test.go
@@ -0,0 +1,80 @@
+package extension
+
+import (
+ "encoding/xml"
+ "github.com/stretchr/testify/assert"
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+const exampleManifest = `
+
+
+ 1.0.0
+
+
+ http://localhost/foo
+
+`
+
+func TestSetVersionApp(t *testing.T) {
+ app := &App{}
+
+ tmpDir := t.TempDir()
+
+ assert.NoError(t, os.WriteFile(filepath.Join(tmpDir, "manifest.xml"), []byte(exampleManifest), 0644))
+
+ assert.NoError(t, BuildModifier(app, tmpDir, BuildModifierConfig{Version: "5.0.0"}))
+
+ bytes, err := os.ReadFile(filepath.Join(tmpDir, "manifest.xml"))
+
+ assert.NoError(t, err)
+
+ var manifest Manifest
+
+ assert.NoError(t, xml.Unmarshal(bytes, &manifest))
+
+ assert.Equal(t, "5.0.0", manifest.Meta.Version)
+}
+
+func TestSetRegistration(t *testing.T) {
+ app := &App{}
+
+ tmpDir := t.TempDir()
+
+ assert.NoError(t, os.WriteFile(filepath.Join(tmpDir, "manifest.xml"), []byte(exampleManifest), 0644))
+
+ assert.NoError(t, BuildModifier(app, tmpDir, BuildModifierConfig{AppBackendUrl: "https://foo.com"}))
+
+ bytes, err := os.ReadFile(filepath.Join(tmpDir, "manifest.xml"))
+
+ assert.NoError(t, err)
+
+ var manifest Manifest
+
+ assert.NoError(t, xml.Unmarshal(bytes, &manifest))
+
+ assert.Equal(t, "https://foo.com/foo", manifest.Setup.RegistrationUrl)
+}
+
+func TestSetRegistrationSecret(t *testing.T) {
+ app := &App{}
+
+ tmpDir := t.TempDir()
+
+ assert.NoError(t, os.WriteFile(filepath.Join(tmpDir, "manifest.xml"), []byte(exampleManifest), 0644))
+
+ assert.NoError(t, BuildModifier(app, tmpDir, BuildModifierConfig{AppBackendSecret: "secret"}))
+
+ bytes, err := os.ReadFile(filepath.Join(tmpDir, "manifest.xml"))
+
+ assert.NoError(t, err)
+
+ var manifest Manifest
+
+ assert.NoError(t, xml.Unmarshal(bytes, &manifest))
+
+ assert.Equal(t, "http://localhost/foo", manifest.Setup.RegistrationUrl)
+ assert.Equal(t, "secret", manifest.Setup.Secret)
+}
diff --git a/extension/manifest.go b/extension/manifest.go
new file mode 100644
index 0000000..48da61e
--- /dev/null
+++ b/extension/manifest.go
@@ -0,0 +1,371 @@
+package extension
+
+import (
+ "encoding/xml"
+)
+
+type Manifest struct {
+ XMLName xml.Name `xml:"manifest"`
+ Meta Meta `xml:"meta"`
+ Setup *Setup `xml:"setup,omitempty"`
+ Admin *Admin `xml:"admin,omitempty"`
+ Storefront *Storefront `xml:"storefront,omitempty"`
+ Permissions *Permissions `xml:"permissions,omitempty"`
+ AllowedHosts *AllowedHosts `xml:"allowed-hosts,omitempty"`
+ CustomFields *CustomFields `xml:"custom-fields,omitempty"`
+ Webhooks *Webhooks `xml:"webhooks,omitempty"`
+ Cookies *Cookies `xml:"cookies,omitempty"`
+ Payments *Payments `xml:"payments,omitempty"`
+ ShippingMethods *ShippingMethods `xml:"shipping-methods,omitempty"`
+ RuleConditions *RuleConditions `xml:"rule-conditions,omitempty"`
+ Tax *Tax `xml:"tax,omitempty"`
+ Gateways *Gateways `xml:"gateways,omitempty"`
+}
+
+type Meta struct {
+ Name string `xml:"name"`
+ Label TranslatableString `xml:"label"`
+ Description TranslatableString `xml:"description,omitempty"`
+ Author string `xml:"author,omitempty"`
+ Copyright string `xml:"copyright,omitempty"`
+ Version string `xml:"version"`
+ Icon string `xml:"icon,omitempty"`
+ License string `xml:"license"`
+ Compatibility string `xml:"compatibility,omitempty"`
+ Privacy string `xml:"privacy,omitempty"`
+ PrivacyPolicyExtensions TranslatableString `xml:"privacyPolicyExtensions,omitempty"`
+}
+
+type Setup struct {
+ RegistrationUrl string `xml:"registrationUrl"`
+ Secret string `xml:"secret,omitempty"`
+}
+
+type Admin struct {
+ ActionButton []ActionButton `xml:"action-button,omitempty"`
+ Module []Module `xml:"module,omitempty"`
+ MainModule *MainModule `xml:"main-module,omitempty"`
+ BaseAppUrl string `xml:"base-app-url,omitempty"`
+}
+
+type Storefront struct {
+ TemplateLoadPriority int `xml:"template-load-priority,omitempty"`
+}
+
+type Permissions struct {
+ Read []string `xml:"read,omitempty"`
+ Create []string `xml:"create,omitempty"`
+ Update []string `xml:"update,omitempty"`
+ Delete []string `xml:"delete,omitempty"`
+ Permission []string `xml:"permission,omitempty"`
+}
+
+type AllowedHosts struct {
+ Host []string `xml:"host"`
+}
+
+type CustomFields struct {
+ CustomFieldSet []CustomFieldSet `xml:"custom-field-set,omitempty"`
+}
+
+type Webhooks struct {
+ Webhook []Webhook `xml:"webhook,omitempty"`
+}
+
+type Cookies struct {
+ Cookie []Cookie `xml:"cookie,omitempty"`
+ Group []CookieGroup `xml:"group,omitempty"`
+}
+
+type Payments struct {
+ PaymentMethod []PaymentMethod `xml:"payment-method,omitempty"`
+}
+
+type ShippingMethods struct {
+ ShippingMethod []ShippingMethod `xml:"shipping-method,omitempty"`
+}
+
+type RuleConditions struct {
+ RuleCondition []RuleCondition `xml:"rule-condition,omitempty"`
+}
+
+type Tax struct {
+ TaxProvider []TaxProvider `xml:"tax-provider,omitempty"`
+}
+
+type Gateways struct {
+ Checkout string `xml:"checkout,omitempty"`
+}
+
+type TranslatableString []struct {
+ Value string `xml:",chardata"`
+ Lang string `xml:"lang,attr,omitempty"`
+}
+
+func (t TranslatableString) GetValueByLanguage(lang []string) string {
+ for _, v := range t {
+ for _, l := range lang {
+ if v.Lang == l {
+ return v.Value
+ }
+ }
+ }
+
+ return ""
+}
+
+type ActionButton struct {
+ Label TranslatableString `xml:"label"`
+ Action string `xml:"action,attr"`
+ Entity string `xml:"entity,attr"`
+ View string `xml:"view,attr"`
+ URL string `xml:"url,attr"`
+}
+
+type Module struct {
+ Label TranslatableString `xml:"label"`
+ Source string `xml:"source,attr,omitempty"`
+ Name string `xml:"name,attr"`
+ Parent string `xml:"parent,attr"`
+ Position int `xml:"position,attr,omitempty"`
+}
+
+type MainModule struct {
+ Source string `xml:"source,attr"`
+}
+
+type CustomFieldSet struct {
+ Name string `xml:"name"`
+ Label TranslatableString `xml:"label"`
+ RelatedEntities EntityList `xml:"related-entities"`
+ Fields CustomFieldList `xml:"fields"`
+ Global bool `xml:"global,attr,omitempty"`
+}
+
+type EntityList struct {
+ Product *struct{} `xml:"product,omitempty"`
+ Order *struct{} `xml:"order,omitempty"`
+ Category *struct{} `xml:"category,omitempty"`
+ Customer *struct{} `xml:"customer,omitempty"`
+ CustomerAddress *struct{} `xml:"customer_address,omitempty"`
+ Media *struct{} `xml:"media,omitempty"`
+ ProductManufacturer *struct{} `xml:"product_manufacturer,omitempty"`
+ SalesChannel *struct{} `xml:"sales_channel,omitempty"`
+ LandingPage *struct{} `xml:"landing_page,omitempty"`
+ Promotion *struct{} `xml:"promotion,omitempty"`
+ ProductStream *struct{} `xml:"product_stream,omitempty"`
+ PropertyGroup *struct{} `xml:"property_group,omitempty"`
+ ProductReview *struct{} `xml:"product_review,omitempty"`
+ EventAction *struct{} `xml:"event_action,omitempty"`
+ Country *struct{} `xml:"country,omitempty"`
+ Currency *struct{} `xml:"currency,omitempty"`
+ CustomerGroup *struct{} `xml:"customer_group,omitempty"`
+ DeliveryTime *struct{} `xml:"delivery_time,omitempty"`
+ DocumentBaseConfig *struct{} `xml:"document_base_config,omitempty"`
+ Language *struct{} `xml:"language,omitempty"`
+ NumberRange *struct{} `xml:"number_range,omitempty"`
+ PaymentMethod *struct{} `xml:"payment_method,omitempty"`
+ Rule *struct{} `xml:"rule,omitempty"`
+ Salutation *struct{} `xml:"salutation,omitempty"`
+ ShippingMethod *struct{} `xml:"shipping_method,omitempty"`
+ Tax *struct{} `xml:"tax,omitempty"`
+}
+
+type CustomFieldList struct {
+ Int []CustomFieldInt `xml:"int,omitempty"`
+ Float []CustomFieldFloat `xml:"float,omitempty"`
+ Text []CustomFieldText `xml:"text,omitempty"`
+ TextArea []CustomFieldTextArea `xml:"text-area,omitempty"`
+ Bool []CustomFieldBool `xml:"bool,omitempty"`
+ Datetime []CustomFieldDatetime `xml:"datetime,omitempty"`
+ SingleSelect []CustomFieldSingleSelect `xml:"single-select,omitempty"`
+ MultiSelect []CustomFieldMultiSelect `xml:"multi-select,omitempty"`
+ SingleEntitySelect []CustomFieldSingleEntitySelect `xml:"single-entity-select,omitempty"`
+ MultiEntitySelect []CustomFieldMultiEntitySelect `xml:"multi-entity-select,omitempty"`
+ ColorPicker []CustomFieldColorPicker `xml:"color-picker,omitempty"`
+ MediaSelection []CustomFieldMedia `xml:"media-selection,omitempty"`
+ Price []CustomFieldPrice `xml:"price,omitempty"`
+}
+
+type CustomFieldBase struct {
+ Label TranslatableString `xml:"label"`
+ HelpText TranslatableString `xml:"help-text,omitempty"`
+ Required bool `xml:"required,omitempty"`
+ Position int `xml:"position,omitempty"`
+ AllowCustomerWrite bool `xml:"allow-customer-write,omitempty"`
+ AllowCartExpose bool `xml:"allow-cart-expose,omitempty"`
+}
+
+type CustomFieldInt struct {
+ CustomFieldBase
+ XMLName xml.Name `xml:"int"`
+ Name string `xml:"name,attr"`
+ Placeholder TranslatableString `xml:"placeholder,omitempty"`
+ Steps int `xml:"steps,omitempty"`
+ Min int `xml:"min,omitempty"`
+ Max int `xml:"max,omitempty"`
+}
+
+type CustomFieldFloat struct {
+ CustomFieldBase
+ XMLName xml.Name `xml:"float"`
+ Name string `xml:"name,attr"`
+ Placeholder TranslatableString `xml:"placeholder,omitempty"`
+ Steps float64 `xml:"steps,omitempty"`
+ Min float64 `xml:"min,omitempty"`
+ Max float64 `xml:"max,omitempty"`
+}
+
+type CustomFieldText struct {
+ CustomFieldBase
+ XMLName xml.Name `xml:"text"`
+ Name string `xml:"name,attr"`
+ Placeholder TranslatableString `xml:"placeholder,omitempty"`
+}
+
+type CustomFieldTextArea struct {
+ CustomFieldBase
+ XMLName xml.Name `xml:"text-area"`
+ Name string `xml:"name,attr"`
+ Placeholder TranslatableString `xml:"placeholder,omitempty"`
+}
+
+type CustomFieldBool struct {
+ CustomFieldBase
+ XMLName xml.Name `xml:"bool"`
+ Name string `xml:"name,attr"`
+}
+
+type CustomFieldDatetime struct {
+ CustomFieldBase
+ XMLName xml.Name `xml:"datetime"`
+ Name string `xml:"name,attr"`
+}
+
+type CustomFieldSingleSelect struct {
+ CustomFieldBase
+ XMLName xml.Name `xml:"single-select"`
+ Name string `xml:"name,attr"`
+ Placeholder TranslatableString `xml:"placeholder,omitempty"`
+ Options OptionCollection `xml:"options"`
+}
+
+type CustomFieldMultiSelect struct {
+ CustomFieldSingleSelect
+ XMLName xml.Name `xml:"multi-select"`
+}
+
+type CustomFieldSingleEntitySelect struct {
+ CustomFieldBase
+ XMLName xml.Name `xml:"single-entity-select"`
+ Name string `xml:"name,attr"`
+ Placeholder TranslatableString `xml:"placeholder,omitempty"`
+ Entity string `xml:"entity"`
+ LabelProperty string `xml:"label-property"`
+}
+
+type CustomFieldMultiEntitySelect struct {
+ CustomFieldSingleEntitySelect
+ XMLName xml.Name `xml:"multi-entity-select"`
+}
+
+type CustomFieldColorPicker struct {
+ CustomFieldBase
+ XMLName xml.Name `xml:"color-picker"`
+ Name string `xml:"name,attr"`
+}
+
+type CustomFieldMedia struct {
+ CustomFieldBase
+ XMLName xml.Name `xml:"media-selection"`
+ Name string `xml:"name,attr"`
+}
+
+type CustomFieldPrice struct {
+ CustomFieldBase
+ XMLName xml.Name `xml:"price"`
+ Name string `xml:"name,attr"`
+}
+
+type OptionCollection struct {
+ Option []Option `xml:"option"`
+}
+
+type Option struct {
+ Name TranslatableString `xml:"name"`
+ Value string `xml:"value,attr"`
+}
+
+// Add more specific custom field types here...
+
+type Webhook struct {
+ Name string `xml:"name,attr"`
+ URL string `xml:"url,attr"`
+ Event string `xml:"event,attr"`
+ OnlyLiveVersion bool `xml:"onlyLiveVersion,attr,omitempty"`
+}
+
+type Cookie struct {
+ SnippetName string `xml:"snippet-name"`
+ SnippetDescription string `xml:"snippet-description,omitempty"`
+ Cookie string `xml:"cookie"`
+ Value string `xml:"value,omitempty"`
+ Expiration int `xml:"expiration,omitempty"`
+}
+
+type CookieGroup struct {
+ SnippetName string `xml:"snippet-name"`
+ SnippetDescription string `xml:"snippet-description,omitempty"`
+ Entries []CookieEntry `xml:"entries>cookie,omitempty"`
+}
+
+type CookieEntry struct {
+ Cookie
+}
+
+type PaymentMethod struct {
+ Identifier string `xml:"identifier"`
+ Name TranslatableString `xml:"name"`
+ Description TranslatableString `xml:"description,omitempty"`
+ PayURL string `xml:"pay-url,omitempty"`
+ FinalizeURL string `xml:"finalize-url,omitempty"`
+ ValidateURL string `xml:"validate-url,omitempty"`
+ CaptureURL string `xml:"capture-url,omitempty"`
+ RefundURL string `xml:"refund-url,omitempty"`
+ RecurringURL string `xml:"recurring-url,omitempty"`
+ Icon string `xml:"icon,omitempty"`
+}
+
+type ShippingMethod struct {
+ Identifier string `xml:"identifier"`
+ Name TranslatableString `xml:"name"`
+ Description TranslatableString `xml:"description,omitempty"`
+ Active bool `xml:"active,omitempty"`
+ DeliveryTime DeliveryTime `xml:"delivery-time"`
+ Icon string `xml:"icon,omitempty"`
+ Position int `xml:"position,omitempty"`
+ TrackingURL TranslatableString `xml:"tracking-url,omitempty"`
+}
+
+type DeliveryTime struct {
+ ID string `xml:"id"`
+ Name TranslatableString `xml:"name"`
+ Min int `xml:"min"`
+ Max int `xml:"max"`
+ Unit string `xml:"unit"`
+}
+
+type RuleCondition struct {
+ Identifier string `xml:"identifier"`
+ Name TranslatableString `xml:"name"`
+ Group string `xml:"group"`
+ Script string `xml:"script"`
+ Constraints []CustomFieldList `xml:"constraints"`
+}
+
+type TaxProvider struct {
+ Identifier string `xml:"identifier"`
+ Name string `xml:"name"`
+ Priority int `xml:"priority"`
+ ProcessURL string `xml:"process-url"`
+}
diff --git a/extension/manifest_test.go b/extension/manifest_test.go
new file mode 100644
index 0000000..22de232
--- /dev/null
+++ b/extension/manifest_test.go
@@ -0,0 +1,37 @@
+package extension
+
+import (
+ "encoding/xml"
+ "github.com/stretchr/testify/assert"
+ "os"
+ "testing"
+)
+
+func TestManifestRead(t *testing.T) {
+ bytes, err := os.ReadFile("_fixtures/istorier.xml")
+
+ assert.NoError(t, err)
+
+ manifest := Manifest{}
+
+ assert.NoError(t, xml.Unmarshal(bytes, &manifest))
+
+ assert.Equal(t, "InstoImmersiveElements", manifest.Meta.Name)
+ assert.Equal(t, "Immersive Elements", manifest.Meta.Label[0].Value)
+ assert.Equal(t, "Transform your online store into an unforgettable brand experience. As an incredibly cost-effective alternative to external resources, the app is engineered to boost conversions.", manifest.Meta.Description[0].Value)
+ assert.Equal(t, "Instorier AS", manifest.Meta.Author)
+ assert.Equal(t, "(c) by Instorier AS", manifest.Meta.Copyright)
+ assert.Equal(t, "1.1.0", manifest.Meta.Version)
+ assert.Equal(t, "Resources/config/plugin.png", manifest.Meta.Icon)
+ assert.Equal(t, "Proprietary", manifest.Meta.License)
+
+ assert.Equal(t, "https://instorier.apps.shopware.io/app/lifecycle/register", manifest.Setup.RegistrationUrl)
+ assert.Equal(t, "", manifest.Setup.Secret)
+
+ assert.Equal(t, "https://instorier.apps.shopware.io/iframe", manifest.Admin.BaseAppUrl)
+
+ assert.Len(t, manifest.Permissions.Read, 57)
+ assert.Len(t, manifest.Permissions.Create, 4)
+ assert.Len(t, manifest.Permissions.Update, 2)
+ assert.Len(t, manifest.Permissions.Delete, 2)
+}
diff --git a/extension/zip.go b/extension/zip.go
index 94a00eb..6cfad57 100644
--- a/extension/zip.go
+++ b/extension/zip.go
@@ -2,7 +2,6 @@ package extension
import (
"archive/zip"
- "bytes"
"context"
"encoding/json"
"encoding/xml"
@@ -505,63 +504,29 @@ func PrepareExtensionForRelease(ctx context.Context, sourceRoot, extensionRoot s
manifestPath := filepath.Join(extensionRoot, "manifest.xml")
- file, err := os.Open(manifestPath)
+ bytes, err := os.ReadFile(manifestPath)
+
if err != nil {
- return fmt.Errorf("cannot read manifest file: %w", err)
+ return err
}
- defer func() {
- _ = file.Close()
- }()
-
- var buf bytes.Buffer
- decoder := xml.NewDecoder(file)
- encoder := xml.NewEncoder(&buf)
-
- skip := false
+ var manifest Manifest
- for {
- token, err := decoder.Token()
- if err == io.EOF {
- break
- }
- if err != nil {
- return err
- }
-
- if v, ok := token.(xml.StartElement); ok {
- if v.Name.Local == "secret" {
- skip = true
- continue
- }
- }
-
- if v, ok := token.(xml.EndElement); ok {
- if v.Name.Local == "secret" {
- skip = false
- continue
- }
- }
-
- if skip {
- continue
- }
-
- if err := encoder.EncodeToken(token); err != nil {
- return err
- }
+ if err := xml.Unmarshal(bytes, &manifest); err != nil {
+ return fmt.Errorf("unmarshal manifest failed: %w", err)
}
- // must call flush, otherwise some elements will be missing
- if err := encoder.Flush(); err != nil {
- return err
+ if manifest.Setup != nil {
+ manifest.Setup.Secret = ""
}
- newManifest := buf.String()
- newManifest = strings.ReplaceAll(newManifest, "xmlns:_xmlns=\"xmlns\" _xmlns:xsi=", "xmlns:xsi=")
- newManifest = strings.ReplaceAll(newManifest, "xmlns:_XMLSchema-instance=\"http://www.w3.org/2001/XMLSchema-instance\" _XMLSchema-instance:noNamespaceSchemaLocation=", "xsi:noNamespaceSchemaLocation=")
+ newManifest, err := xml.MarshalIndent(manifest, "", " ")
+
+ if err != nil {
+ return fmt.Errorf("cannot marshal manifest failed: %w", err)
+ }
- if err := os.WriteFile(manifestPath, []byte(newManifest), os.ModePerm); err != nil {
+ if err := os.WriteFile(manifestPath, newManifest, os.ModePerm); err != nil {
return err
}