Repository: owncloud/ocis
Pull request #6869 (2023-07-21 13:48:03)

use magicmime
## Description

Getting a list of file extensions based on a mimetype is already possible in our rego files, the main downside till today was that the known range of mappings are limited.

This pr introduces the ability to provide a custom mime.types file to ocis and extend that store.
The format is based on https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types.
RFC 4288 provides more information.

## Motivation and Context
The default mime.types file extension mapping is limited, we have to give the option to extend the list of known mappings.

## How Has This Been Tested?
- unit tests
- local installation and manual testing

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [X] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Technical debt
- [ ] Tests only (no source changes)

## Checklist:
<!-- Tick the checkboxes when done. -->
<!-- Raise documentation ticket in owncloud.github.io/ -->
- [X] Code changes
- [X] Unit tests added
- [ ] Acceptance tests added
- [ ] Documentation ticket raised: <link> 


update-docs[bot]: Thanks for opening this pull request! The maintainers of this repository would appreciate it if you would create a [changelog](https://github.com/owncloud/ocis/blob/master/changelog/README.md) item based on your changes.

micbar: @aduffeck you had the idea to use the `file` command directly via syscall. 

I found no way to use the `file` command with a blob. The postprocessing service has no access to the path of the file on disk. We can only download the blob.
fschade: @micbar i hijacked the pr 😗 
fschade: @mmattel FYI, i've already added some information into the readme, fine?
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=6869)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6869&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6869&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6869&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6869&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6869&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6869&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6869&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6869&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6869&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6869&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6869&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6869&resolved=false&types=CODE_SMELL)

[![40.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/40-16px.png '40.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6869&metric=new_coverage&view=list) [40.0% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6869&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6869&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6869&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=6869)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6869&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6869&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6869&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6869&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6869&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6869&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6869&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6869&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6869&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6869&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6869&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6869&resolved=false&types=CODE_SMELL)

[![40.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/40-16px.png '40.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6869&metric=new_coverage&view=list) [40.0% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6869&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6869&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6869&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/changelog/unreleased/enhancement-extendable-policy-mimetype-extension-mapping.md b/changelog/unreleased/enhancement-extendable-policy-mimetype-extension-mapping.md
new file mode 100644
index 00000000000..d33c4adeb5c
--- /dev/null
+++ b/changelog/unreleased/enhancement-extendable-policy-mimetype-extension-mapping.md
@@ -0,0 +1,8 @@
+Enhancement: Extendable policy mimetype extension mapping
+
+The extension mimetype mappings known from rego can now be extended.
+To do this, ocis must be informed where the mimetype file (apache mime.types file format) is located.
+
+`export OCIS_MACHINE_AUTH_API_KEY=$OCIS_HOME/mime.types`
+
+https://github.com/owncloud/ocis/pull/6869
diff --git a/services/policies/README.md b/services/policies/README.md
index 4dccbf52d85..067c3312a6b 100644
--- a/services/policies/README.md
+++ b/services/policies/README.md
@@ -20,7 +20,7 @@ To configure the policies service, three environment variables need to be define
 
 Note that each query setting defines the [Complete Rules](https://www.openpolicyagent.org/docs/latest/#complete-rules) variable defined in the rego rule set the corresponding step uses for the evaluation. If the variable is mistyped or not found, the evaluation defaults to deny. Individual query definitions can be defined for each module.
 
-To activate a the policies service for a module, it must be started with a yaml configuration that points to one or more rego files. Note that if the service is scaled horizontally, each instance should have access to the same rego files to avoid unpredictable results. If a file path has been configured but the file it is not present or accessible, the evaluation defaults to deny.
+To activate the policies service for a module, it must be started with a yaml configuration that points to one or more rego files. Note that if the service is scaled horizontally, each instance should have access to the same rego files to avoid unpredictable results. If a file path has been configured but the file is not present or accessible, the evaluation defaults to deny.
 
 When using async post-processing which is done via the postprocessing service, the value `policies` must be added to the `POSTPROCESSING_STEPS` configuration in postprocessing service in the order where the evaluation should take place.
 
@@ -88,8 +88,8 @@ proxy:
 
 The same can be achieved by setting the following environment variable:
 
-```yaml
-PROXY_POLICIES_QUERY=data.proxy.granted
+```shell
+export PROXY_POLICIES_QUERY=data.proxy.granted
 ```
 
 ### Postprocessing
@@ -102,14 +102,14 @@ policies:
 
 The same can be achieved by setting the following environment variable:
 
-```yaml
-POLICIES_POSTPROCESSING_QUERY=data.postprocessing.granted
+```shell
+export POLICIES_POSTPROCESSING_QUERY=data.postprocessing.granted
 ```
 
 As soon as that query is configured, the postprocessing service must be informed to use the policies step by setting the environment variable:
 
-```yaml
-POSTPROCESSING_STEPS=policies
+```shell
+export POSTPROCESSING_STEPS=policies
 ```
 
 Note that additional steps can be configured and their position in the list defines the order of processing. For details see the postprocessing service documentation.
@@ -118,6 +118,30 @@ Note that additional steps can be configured and their position in the list defi
 
 To identify available keys for OPA, you need to look at [engine.go](https://github.com/owncloud/ocis/blob/master/services/policies/pkg/engine/engine.go) and the [policies.swagger.json](https://github.com/owncloud/ocis/blob/master/protogen/gen/ocis/services/policies/v0/policies.swagger.json) file. Note that which keys are available depends on from which module it is used.
 
+## Extend Mimetype File Extension Mapping
+
+In the extended set of the rego query language, it is possible to get a list of associated file extensions based on a mimetype, for example `ocis.mimetype.extensions("application/pdf")`.
+
+The list of mappings is restricted by default and is provided by the host system ocis is installed on.
+
+In order to extend this list, ocis must be provided with the path to a custom `mime.types` file that maps mimetypes to extensions.
+The location for the file must be accessible by all instances of the policy service. As a rule of thumb, use the directory where the ocis configuration files are stored.
+Note that existing mappings from the host are extended by the definitions from the mime types file, but not replaced.
+
+The path to that file can be provided via a yaml configuration or an environment variable. Note to replace the `OCIS_CONFIG_DIR` string by an existing path.
+
+```shell
+export OCIS_MACHINE_AUTH_API_KEY=OCIS_CONFIG_DIR/mime.types
+```
+
+```yaml
+policies:
+  engine:
+    mimes: OCIS_CONFIG_DIR/mime.types
+```
+
+A good example of how such a file should be formatted can be found in the [Apache svn repository](https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types).
+
 ## Example Policies
 
 The policies service contains a set of preconfigured example policies. See the [deployment examples](https://github.com/owncloud/ocis/tree/master/deployments/examples) directory for details. The contained policies disallow Infinite Scale to create certain file types, both via the proxy middleware and the events service via postprocessing.
diff --git a/services/policies/pkg/config/config.go b/services/policies/pkg/config/config.go
index 3eb9b05f0d1..11011376c4a 100644
--- a/services/policies/pkg/config/config.go
+++ b/services/policies/pkg/config/config.go
@@ -45,6 +45,8 @@ type TokenManager struct {
 type Engine struct {
 	Timeout  time.Duration `yaml:"timeout" env:"POLICIES_ENGINE_TIMEOUT" desc:"Sets the timeout the rego expression evaluation can take. The timeout can be set as number followed by a unit identifier like ms, s, etc. Rules default to deny if the timeout was reached."`
 	Policies []string      `yaml:"policies"`
+	// Mimes file path, RFC 4288
+	Mimes string `yaml:"mimes" env:"POLICIES_ENGINE_MIMES" desc:"Sets the mimes file path which maps mimetypes to associated file extensions. See the text description for details."`
 }
 
 // Postprocessing defines the config options for the postprocessing policy handling.
diff --git a/services/policies/pkg/engine/opa/engine.go b/services/policies/pkg/engine/opa/engine.go
index 45bd211bf79..21c07cdb4aa 100644
--- a/services/policies/pkg/engine/opa/engine.go
+++ b/services/policies/pkg/engine/opa/engine.go
@@ -2,6 +2,8 @@ package opa
 
 import (
 	"context"
+	"io"
+	"os"
 	"time"
 
 	"github.com/open-policy-agent/opa/rego"
@@ -17,16 +19,38 @@ type OPA struct {
 	printHook print.Hook
 	policies  []string
 	timeout   time.Duration
+	options   []func(r *rego.Rego)
 }
 
 // NewOPA returns a ready to use opa engine.
 func NewOPA(timeout time.Duration, logger log.Logger, conf config.Engine) (OPA, error) {
+	var mtReader io.ReadCloser
+
+	if conf.Mimes != "" {
+		var err error
+		mtReader, err = os.Open(conf.Mimes)
+		if err != nil {
+			return OPA{}, err
+		}
+
+		defer mtReader.Close()
+	}
+
+	rfMimetypeExtensions, err := RFMimetypeExtensions(mtReader)
+	if err != nil {
+		return OPA{}, err
+	}
+
 	return OPA{
-			policies:  conf.Policies,
-			timeout:   timeout,
-			printHook: logPrinter{logger: logger},
+		policies:  conf.Policies,
+		timeout:   timeout,
+		printHook: logPrinter{logger: logger},
+		options: []func(r *rego.Rego){
+			RFMimetypeDetect,
+			RFResourceDownload,
+			rfMimetypeExtensions,
 		},
-		nil
+	}, nil
 }
 
 // Evaluate evaluates the opa policies and returns the result.
@@ -34,19 +58,13 @@ func (o OPA) Evaluate(ctx context.Context, qs string, env engine.Environment) (b
 	ctx, cancel := context.WithTimeout(ctx, o.timeout)
 	defer cancel()
 
-	customFns := []func(r *rego.Rego){
-		RFResourceDownload,
-		RFMimetypeDetect,
-		RFMimetypeExtensions,
-	}
-
 	q, err := rego.New(
 		append([]func(r *rego.Rego){
 			rego.Query(qs),
 			rego.Load(o.policies, nil),
 			rego.EnablePrintStatements(true),
 			rego.PrintHook(o.printHook),
-		}, customFns...)...,
+		}, o.options...)...,
 	).PrepareForEval(ctx)
 	if err != nil {
 		return false, err
diff --git a/services/policies/pkg/engine/opa/rf_mimetype.go b/services/policies/pkg/engine/opa/rf_mimetype.go
index 033d664b660..f3b7b300f9f 100644
--- a/services/policies/pkg/engine/opa/rf_mimetype.go
+++ b/services/policies/pkg/engine/opa/rf_mimetype.go
@@ -1,6 +1,8 @@
 package opa
 
 import (
+	"bufio"
+	"io"
 	"mime"
 	"strings"
 
@@ -10,34 +12,70 @@ import (
 	"github.com/open-policy-agent/opa/types"
 )
 
-var RFMimetypeExtensions = rego.Function1(
-	&rego.Function{
-		Name:             "ocis.mimetype.extensions",
-		Decl:             types.NewFunction(types.Args(types.S), types.A),
-		Memoize:          true,
-		Nondeterministic: true,
-	},
-	func(_ rego.BuiltinContext, a *ast.Term) (*ast.Term, error) {
-		var mt string
+// RFMimetypeExtensions extends the rego dictionary with the possibility of mapping mimetypes to file extensions.
+// Be careful calling this multiple times with individual readers, the mime store is global,
+// which results in one global store which holds all known mimetype mappings at once.
+//
+// Rego: `ocis.mimetype.extensions("application/pdf")`
+// Result `[.pdf]`
+func RFMimetypeExtensions(f io.Reader) (func(*rego.Rego), error) {
+	if f != nil {
+		scanner := bufio.NewScanner(f)
+		for scanner.Scan() {
+			fields := strings.Fields(scanner.Text())
+			if len(fields) <= 1 || fields[0][0] == '#' {
+				continue
+			}
+			mimeType := fields[0]
+			for _, ext := range fields[1:] {
+				if ext[0] == '#' {
+					break
+				}
+				if err := mime.AddExtensionType("."+ext, mimeType); err != nil {
+					return nil, err
+				}
 
-		if err := ast.As(a.Value, &mt); err != nil {
-			return nil, err
+			}
 		}
-
-		detectedExtensions, err := mime.ExtensionsByType(mt)
-		if err != nil {
+		if err := scanner.Err(); err != nil {
 			return nil, err
 		}
+	}
 
-		var mimeTerms []*ast.Term
-		for _, extension := range detectedExtensions {
-			mimeTerms = append(mimeTerms, ast.NewTerm(ast.String(extension)))
-		}
+	return rego.Function1(
+		&rego.Function{
+			Name:             "ocis.mimetype.extensions",
+			Decl:             types.NewFunction(types.Args(types.S), types.A),
+			Memoize:          true,
+			Nondeterministic: true,
+		},
+		func(_ rego.BuiltinContext, a *ast.Term) (*ast.Term, error) {
+			var mt string
 
-		return ast.ArrayTerm(mimeTerms...), nil
-	},
-)
+			if err := ast.As(a.Value, &mt); err != nil {
+				return nil, err
+			}
+
+			detectedExtensions, err := mime.ExtensionsByType(mt)
+			if err != nil {
+				return nil, err
+			}
+
+			var mimeTerms []*ast.Term
+			for _, extension := range detectedExtensions {
+				mimeTerms = append(mimeTerms, ast.NewTerm(ast.String(extension)))
+			}
+
+			return ast.ArrayTerm(mimeTerms...), nil
+		},
+	), nil
+}
 
+// RFMimetypeDetect extends the rego dictionary with the possibility to detect mimetypes.
+// Be careful, the list of known mimetypes is limited.
+//
+// Rego: `ocis.mimetype.extensions(".txt")`
+// Result `text/plain`
 var RFMimetypeDetect = rego.Function1(
 	&rego.Function{
 		Name:             "ocis.mimetype.detect",
diff --git a/services/policies/pkg/engine/opa/rf_resource.go b/services/policies/pkg/engine/opa/rf_resource.go
index 60543c0b83c..9ff1d0ba1ee 100644
--- a/services/policies/pkg/engine/opa/rf_resource.go
+++ b/services/policies/pkg/engine/opa/rf_resource.go
@@ -11,6 +11,10 @@ import (
 	"github.com/open-policy-agent/opa/types"
 )
 
+// RFResourceDownload extends the rego dictionary with the possibility to download oCis resources.
+//
+// Rego: `ocis.resource.download("ocis/path/0034892347349827")`
+// Result: bytes
 var RFResourceDownload = rego.Function1(
 	&rego.Function{
 		Name:             "ocis.resource.download",


Test patch:
diff --git a/services/policies/pkg/engine/opa/rf_mimetype_test.go b/services/policies/pkg/engine/opa/rf_mimetype_test.go
index 404dc2b456d..d2f692deba9 100644
--- a/services/policies/pkg/engine/opa/rf_mimetype_test.go
+++ b/services/policies/pkg/engine/opa/rf_mimetype_test.go
@@ -2,6 +2,8 @@ package opa_test
 
 import (
 	"context"
+	"io"
+	"strings"
 
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
@@ -19,13 +21,37 @@ var _ = Describe("opa ocis mimetype functions", func() {
 			Expect(rs[0].Expressions[0].String()).To(Equal("text/plain"))
 		})
 	})
+	Describe("ocis.mimetype.extensions", func() {
+		DescribeTable("resolves extensions by mimetype",
+			func(mimetype string, expectations []string, f io.Reader) {
+				rfMimetypeExtensions, err := opa.RFMimetypeExtensions(f)
+				Expect(err).ToNot(HaveOccurred())
 
-	Describe("ocis.mimetype.extension_for_mimetype", func() {
-		It("provides matching extensions", func() {
-			r := rego.New(rego.Query(`ocis.mimetype.extensions("application/pdf")`), opa.RFMimetypeExtensions)
-			rs, err := r.Eval(context.Background())
-			Expect(err).ToNot(HaveOccurred())
-			Expect(rs[0].Expressions[0].String()).To(Equal("[.pdf]"))
-		})
+				r := rego.New(rego.Query(`ocis.mimetype.extensions("`+mimetype+`")`), rfMimetypeExtensions)
+				rs, err := r.Eval(context.Background())
+				Expect(err).ToNot(HaveOccurred())
+
+				got := rs[0].Expressions[0].String()
+
+				if len(expectations) == 0 {
+					Expect(got).To(Equal("[]"))
+				}
+
+				for i, expectation := range expectations {
+					if i+1 != len(expectations) {
+						expectation += " "
+					}
+
+					Expect(string(got[0])).To(Equal("["))
+					Expect(strings.Contains(got, expectation)).To(BeTrue())
+					Expect(string(got[len(got)-1])).To(Equal("]"))
+				}
+			},
+			Entry("With default mimetype", "application/pdf", []string{".pdf"}, nil),
+			Entry("With unknown mimetype", "ocis/with.custom.mt", []string{}, nil),
+			Entry("With custom mimetype", "ocis/with.custom.mt", []string{".with.custom.mt"}, strings.NewReader("ocis/with.custom.mt    with.custom.mt")),
+			Entry("With multiple custom mimetypes", "ocis/with.multiple.custom.mt", []string{".with.multiple.custom.1.mt", ".with.multiple.custom.2.mt"}, strings.NewReader("ocis/with.multiple.custom.mt                                with.multiple.custom.1.mt with.multiple.custom.2.mt")),
+			Entry("With custom ignored mimetype", "ocis/with.multiple.custom.ignored.mt", []string{}, strings.NewReader("#ocis/with.multiple.custom.ignored.mt with.multiple.custom.ignored.mt")),
+		)
 	})
 })
diff --git a/tests/acceptance/features/bootstrap/FeatureContext.php b/tests/acceptance/features/bootstrap/FeatureContext.php
index 0f263e66b39..0724f9da765 100644
--- a/tests/acceptance/features/bootstrap/FeatureContext.php
+++ b/tests/acceptance/features/bootstrap/FeatureContext.php
@@ -2793,7 +2793,7 @@ class FeatureContext extends BehatVariablesContext {
 				"code" => "%last_public_share_token%",
 				"function" => [
 					$this,
-					"getLastCreatedPublicShareToken"
+					"getLastPublicShareToken"
 				],
 				"parameter" => []
 			],
diff --git a/tests/acceptance/features/bootstrap/PublicWebDavContext.php b/tests/acceptance/features/bootstrap/PublicWebDavContext.php
index 8bb24e42a7d..1d0e96270b6 100644
--- a/tests/acceptance/features/bootstrap/PublicWebDavContext.php
+++ b/tests/acceptance/features/bootstrap/PublicWebDavContext.php
@@ -51,7 +51,7 @@ class PublicWebDavContext implements Context {
 			// accessing it as a public link using the "new" public webDAV API
 			// the client needs to provide the public link share token followed
 			// by just the name of the file - not the full path.
-			$fullPath = (string) $this->featureContext->getLastCreatedPublicShare()->path;
+			$fullPath = $this->featureContext->getLastPublicSharePath();
 			$fullPathParts = \explode("/", $fullPath);
 			$path = \end($fullPathParts);
 		} else {
@@ -76,7 +76,7 @@ class PublicWebDavContext implements Context {
 	 */
 	public function downloadPublicFileWithRangeAndPassword(string $range, string $password, string $publicWebDAVAPIVersion):void {
 		if ($publicWebDAVAPIVersion === "new") {
-			$path = (string) $this->featureContext->getLastCreatedPublicShare()->file_target;
+			$path = $this->featureContext->getLastPublicShareData()->data->file_target;
 		} else {
 			$path = "";
 		}
@@ -124,7 +124,7 @@ class PublicWebDavContext implements Context {
 		if ($publicWebDAVAPIVersion === "old") {
 			return;
 		}
-		$token = $this->featureContext->getLastCreatedPublicShareToken();
+		$token = $this->featureContext->getLastPublicShareToken();
 		$davPath = WebDavHelper::getDavPath(
 			$token,
 			0,
@@ -178,7 +178,7 @@ class PublicWebDavContext implements Context {
 		if ($publicWebDAVAPIVersion === "old") {
 			return;
 		}
-		$token = $this->featureContext->getLastCreatedPublicShareToken();
+		$token = $this->featureContext->getLastPublicShareToken();
 		$davPath = WebDavHelper::getDavPath(
 			$token,
 			0,
@@ -296,7 +296,7 @@ class PublicWebDavContext implements Context {
 	):void {
 		$path = \ltrim($path, "/");
 		$password = $this->featureContext->getActualPassword($password);
-		$token = $this->featureContext->getLastCreatedPublicShareToken();
+		$token = $this->featureContext->getLastPublicShareToken();
 		$davPath = WebDavHelper::getDavPath(
 			$token,
 			0,
@@ -401,7 +401,7 @@ class PublicWebDavContext implements Context {
 	 * @return void
 	 */
 	public function thePublicCopiesFileUsingTheWebDAVApi(string $source, string $destination, string $publicWebDAVAPIVersion):void {
-		$token = $this->featureContext->getLastCreatedPublicShareToken();
+		$token = $this->featureContext->getLastPublicShareToken();
 		$davPath = WebDavHelper::getDavPath(
 			$token,
 			0,
@@ -1086,7 +1086,7 @@ class PublicWebDavContext implements Context {
 		if ($publicWebDAVAPIVersion === "old") {
 			return;
 		}
-		$filename = (string)$this->featureContext->getLastCreatedPublicShare()->file_target;
+		$filename = (string)$this->featureContext->getLastPublicShareData()->data[0]->file_target;
 
 		$this->publicUploadContent(
 			$filename,
@@ -1251,7 +1251,7 @@ class PublicWebDavContext implements Context {
 		if ($publicWebDAVAPIVersion === "old") {
 			return;
 		} elseif ($publicWebDAVAPIVersion === "new") {
-			$path = (string) $this->featureContext->getLastCreatedPublicShare()->path;
+			$path = $this->featureContext->getLastPublicSharePath();
 		} else {
 			$path = "";
 		}
@@ -1329,7 +1329,7 @@ class PublicWebDavContext implements Context {
 		string $destination,
 		string $password
 	):void {
-		$token = $this->featureContext->getLastCreatedPublicShareToken();
+		$token = $this->featureContext->getLastPublicShareToken();
 		$davPath = WebDavHelper::getDavPath(
 			$token,
 			0,
@@ -1421,7 +1421,7 @@ class PublicWebDavContext implements Context {
 		string $fileName,
 		string $mtime
 	):void {
-		$token = $this->featureContext->getLastCreatedPublicShareToken();
+		$token = $this->featureContext->getLastPublicShareToken();
 		$baseUrl = $this->featureContext->getBaseUrl();
 		$mtime = \explode(" ", $mtime);
 		\array_pop($mtime);
@@ -1450,7 +1450,7 @@ class PublicWebDavContext implements Context {
 		string $fileName,
 		string $mtime
 	):void {
-		$token = $this->featureContext->getLastCreatedPublicShareToken();
+		$token = $this->featureContext->getLastPublicShareToken();
 		$baseUrl = $this->featureContext->getBaseUrl();
 		Assert::assertNotEquals(
 			$mtime,
@@ -1487,7 +1487,7 @@ class PublicWebDavContext implements Context {
 			return;
 		}
 		$password = $this->featureContext->getActualPassword($password);
-		$token = $this->featureContext->getLastCreatedPublicShareToken();
+		$token = $this->featureContext->getLastPublicShareToken();
 		$davPath = WebDavHelper::getDavPath(
 			$token,
 			0,
@@ -1593,7 +1593,7 @@ class PublicWebDavContext implements Context {
 		} else {
 			$body = null;
 		}
-		$token = $this->featureContext->getLastCreatedPublicShareToken();
+		$token = $this->featureContext->getLastPublicShareToken();
 		$davPath = WebDavHelper::getDavPath(
 			null,
 			null,
diff --git a/tests/acceptance/features/bootstrap/Sharing.php b/tests/acceptance/features/bootstrap/Sharing.php
index 84cdc753936..392ddd1e23a 100644
--- a/tests/acceptance/features/bootstrap/Sharing.php
+++ b/tests/acceptance/features/bootstrap/Sharing.php
@@ -43,6 +43,21 @@ trait Sharing {
 	 */
 	private array $createdUserGroupShares = [];
 
+	private ?string $userWhoCreatedLastPublicShare = null;
+
+	/**
+	 * Contains the API response to the last public link share that was created
+	 * by the test-runner using the Sharing API.
+	 * Shares created on the webUI do not have an entry.
+	 */
+	private ?SimpleXMLElement $lastPublicShareData = null;
+
+	/**
+	 * Contains the share id of the last public link share that was created by
+	 * the test-runner, either using the Sharing API or on the web UI.
+	 */
+	private ?string $lastPublicShareId = null;
+
 	private ?float $localLastShareTime = null;
 
 	/**
@@ -70,32 +85,61 @@ trait Sharing {
 		'attributes', 'permissions', 'share_with', 'share_with_displayname', 'share_with_additional_info'
 	];
 
+	/*
+	 * Contains information about the public links that have been created with the webUI.
+	 * Each entry in the array has a "name", "url" and "path".
+	 */
+	private array $createdPublicLinks = [];
+
 	/**
-	 * @var array
+	 * The end (last) entry will itself be an array with keys "name", "url" and "path"
+	 *
+	 * @return array
 	 */
-	private array $createdPublicShares = [];
+	public function getLastCreatedPublicLink():array {
+		return \end($this->createdPublicLinks);
+	}
 
 	/**
 	 * @return string
 	 */
-	public function getLastCreatedPublicShareToken():string {
-		return (string) $this->getLastCreatedPublicShare()->token;
+	public function getLastCreatedPublicLinkUrl():string {
+		$lastCreatedLink = $this->getLastCreatedPublicLink();
+		return $lastCreatedLink["url"];
+	}
+
+	/**
+	 * @return string
+	 */
+	public function getLastCreatedPublicLinkPath():string {
+		$lastCreatedLink = $this->getLastCreatedPublicLink();
+		return $lastCreatedLink["path"];
+	}
+
+	/**
+	 * @return string
+	 */
+	public function getLastCreatedPublicLinkToken():string {
+		$lastCreatedLinkUrl = $this->getLastCreatedPublicLinkUrl();
+		// The token is the last part of the URL, delimited by "/"
+		$urlParts = \explode("/", $lastCreatedLinkUrl);
+		return \end($urlParts);
 	}
 
 	/**
 	 * @return SimpleXMLElement|null
 	 */
-	public function getLastCreatedPublicShare():?SimpleXMLElement {
-		return \end($this->createdPublicShares);
+	public function getLastPublicShareData():?SimpleXMLElement {
+		return $this->lastPublicShareData;
 	}
 
 	/**
-	 * @param SimpleXMLElement $shareData
+	 * @param SimpleXMLElement $responseXml
 	 *
 	 * @return void
 	 */
-	public function addToCreatedPublicShares(SimpleXMLElement $shareData): void {
-		$this->createdPublicShares[] = $shareData;
+	public function setLastPublicShareData(SimpleXMLElement $responseXml): void {
+		$this->lastPublicShareData = $responseXml;
 	}
 
 	/**
@@ -125,8 +169,10 @@ trait Sharing {
 	/**
 	 * @return void
 	 */
-	public function emptyCreatedPublicShares():void {
-		$this->createdPublicShares = [];
+	public function resetLastPublicShareData():void {
+		$this->lastPublicShareData = null;
+		$this->lastPublicShareId = null;
+		$this->userWhoCreatedLastPublicShare = null;
 	}
 
 	/**
@@ -840,7 +886,7 @@ trait Sharing {
 		$user = $this->getActualUsername($user);
 
 		if ($updateLastPublicLink) {
-			$share_id = (string) $this->getLastCreatedPublicShare()->id;
+			$share_id = $this->getLastPublicLinkShareId();
 		} else {
 			if ($shareOwner === null) {
 				$share_id = $this->getLastCreatedUserGroupShareId();
@@ -957,6 +1003,17 @@ trait Sharing {
 		}
 	}
 
+	/**
+	 * @param string $name
+	 * @param string $url
+	 * @param string $path
+	 *
+	 * @return void
+	 */
+	public function addToListOfCreatedPublicLinks(string $name, string $url, string $path = ""):void {
+		$this->createdPublicLinks[] = ["name" => $name, "url" => $url, "path" => $path];
+	}
+
 	/**
 	 * @param string $user
 	 * @param string|null $path
@@ -1017,19 +1074,26 @@ trait Sharing {
 			|| (($httpStatusCode === 200) && ($this->ocsContext->getOCSResponseStatusCode($this->response) > 299))
 		) {
 			if ($shareType === 'public_link') {
-				$this->emptyCreatedPublicShares();
+				$this->resetLastPublicShareData();
 			} else {
 				$this->emptyCreatedUserGroupShares();
 			}
 		} else {
-			$response = $this->getResponseXml(null, __METHOD__);
-			if (isset($response->data)) {
-				$shareData = $response->data;
-				if ($shareType === 'public_link') {
-					$this->addToCreatedPublicShares($shareData);
-				} else {
+			if ($shareType === 'public_link') {
+				$this->setLastPublicShareData($this->getResponseXml(null, __METHOD__));
+				$this->setLastPublicLinkShareId((string) $this->lastPublicShareData->data[0]->id);
+				$this->setUserWhoCreatedLastPublicShare($user);
+				if (isset($this->lastPublicShareData->data)) {
+					$linkName = (string) $this->lastPublicShareData->data[0]->name;
+					$linkUrl = (string) $this->lastPublicShareData->data[0]->url;
+					$this->addToListOfCreatedPublicLinks($linkName, $linkUrl, $path);
+				}
+			} else {
+				$response = $this->getResponseXml(null, __METHOD__);
+				if (isset($response->data)) {
+					$shareData = $response->data;
 					$sharer = (string) $response->data->uid_owner;
-					$this->addToCreatedUserGroupshares($sharer, $shareData);
+					$this->addToCreatedUserGroupShares($sharer, $shareData);
 				}
 			}
 		}
@@ -1869,7 +1933,7 @@ trait Sharing {
 	public function deleteLastShareUsingSharingApi(string $user, string $sharer = null, bool $deleteLastPublicLink = false):void {
 		$user = $this->getActualUsername($user);
 		if ($deleteLastPublicLink) {
-			$shareId = (string) $this->getLastCreatedPublicShare()->id;
+			$shareId = $this->getLastPublicLinkShareId();
 		} else {
 			if ($sharer === null) {
 				$shareId = $this->getLastCreatedUserGroupShareId();
@@ -1978,7 +2042,7 @@ trait Sharing {
 	 * @throws Exception
 	 */
 	public function theUserGetsInfoOfLastPublicLinkShareUsingTheSharingApi():void {
-		$this->userGetsInfoOfLastPublicLinkShareUsingTheSharingApi((string) $this->getLastCreatedPublicShare()->uid_owner);
+		$this->userGetsInfoOfLastPublicLinkShareUsingTheSharingApi($this->getUserWhoCreatedLastPublicShare());
 	}
 
 	/**
@@ -1992,8 +2056,8 @@ trait Sharing {
 	 * @throws Exception
 	 */
 	public function userGetsInfoOfLastPublicLinkShareUsingTheSharingApi(string $user, ?string $language = null):void {
-		if ($this->getLastCreatedPublicShare()->id !== null) {
-			$shareId = (string) $this->getLastCreatedPublicShare()->id;
+		if ($this->lastPublicShareId !== null) {
+			$shareId = $this->lastPublicShareId;
 		} else {
 			throw new Exception(
 				__METHOD__ . " last public link share data was not found"
@@ -2044,6 +2108,46 @@ trait Sharing {
 		$this->asLastShareInfoAboutUserSharingWithUserShouldInclude($sharer, $sharer, $sharee, $table);
 	}
 
+	/**
+	 * Sets the id of the last public link shared file
+	 *
+	 * @param string $shareId
+	 *
+	 * @return void
+	 */
+	public function setLastPublicLinkShareId(string $shareId):void {
+		$this->lastPublicShareId = $shareId;
+	}
+
+	/**
+	 * Retrieves the id of the last public link shared file
+	 *
+	 * @return string|null
+	 */
+	public function getLastPublicLinkShareId():?string {
+		return $this->lastPublicShareId;
+	}
+
+	/**
+	 * Sets the user who created the last public link share
+	 *
+	 * @param string $user
+	 *
+	 * @return void
+	 */
+	public function setUserWhoCreatedLastPublicShare(string $user):void {
+		$this->userWhoCreatedLastPublicShare = $user;
+	}
+
+	/**
+	 * Gets the user who created the last public link share
+	 *
+	 * @return string|null
+	 */
+	public function getUserWhoCreatedLastPublicShare():?string {
+		return $this->userWhoCreatedLastPublicShare;
+	}
+
 	/**
 	 * Retrieves all the shares of the respective user
 	 *
@@ -2119,7 +2223,7 @@ trait Sharing {
 	 */
 	public function userGetsTheLastShareSharedWithHimUsingTheSharingApi(string $user, TableNode $table):void {
 		$user = $this->getActualUsername($user);
-		$shareId = (string) $this->getLastCreatedPublicShare()->id;
+		$shareId = $this->getLastPublicLinkShareId();
 		$this->getShareData($user, $shareId);
 		$this->checkFields($user, $table);
 	}
@@ -2325,7 +2429,7 @@ trait Sharing {
 	):void {
 		$user = $this->getActualUsername($user);
 		$this->verifyTableNodeRows($body, [], $this->shareResponseFields);
-		$this->getShareData($user, (string) $this->getLastCreatedPublicShare()->id);
+		$this->getShareData($user, (string)$this->getLastPublicLinkShareId());
 		$this->theHTTPStatusCodeShouldBe(
 			200,
 			"Error getting info of last public link share for user $user"
@@ -2460,7 +2564,7 @@ trait Sharing {
 	 * @throws Exception
 	 */
 	public function checkLastPublicLinkShareIDIsNotIncluded():void {
-		$shareId = (string) $this->getLastCreatedPublicShare()->id;
+		$shareId = $this->getLastPublicLinkShareId();
 		if ($this->isFieldInResponse('id', $shareId, false)) {
 			Assert::fail(
 				"Public link share id $shareId has been found in response"
@@ -2601,8 +2705,7 @@ trait Sharing {
 			"space_id"
 		];
 
-		$response = $this->getResponseXml(null, __METHOD__);
-		$this->addToCreatedPublicShares($response->data);
+		$this->setLastPublicShareData($this->getResponseXml(null, __METHOD__));
 		foreach ($bodyRows as $field => $value) {
 			if (\in_array($field, $userRelatedFieldNames)) {
 				$value = $this->substituteInLineCodes(
@@ -2632,7 +2735,7 @@ trait Sharing {
 			$value = $this->replaceValuesFromTable($field, $value);
 
 			Assert::assertTrue(
-				$this->isFieldInResponse($field, $value, true, $this->getLastCreatedPublicShare()),
+				$this->isFieldInResponse($field, $value, true, $this->getLastPublicShareData()->data[0]),
 				"$field doesn't have value '$value'"
 			);
 		}
@@ -3482,6 +3585,73 @@ trait Sharing {
 		return $usersShares['ocs']['data'];
 	}
 
+	/**
+	 * The tests can create public link shares with the API or with the webUI.
+	 * If lastPublicShareData is null, then there have not been any created with the API,
+	 * so look for details of a public link share created with the webUI.
+	 *
+	 * @return string authorization token
+	 */
+	public function getLastPublicShareToken():string {
+		if ($this->lastPublicShareData === null) {
+			return $this->getLastCreatedPublicLinkToken();
+		} else {
+			if (\count($this->lastPublicShareData->data->element) > 0) {
+				return (string)$this->lastPublicShareData->data[0]->token;
+			}
+
+			return (string)$this->lastPublicShareData->data->token;
+		}
+	}
+
+	/**
+	 * Returns the attribute values from the last public link share data
+	 *
+	 * @param $attr - attribute name to get
+	 *
+	 * @return string
+	 * @throws Exception
+	 */
+	public function getLastPublicShareAttribute(string $attr): string {
+		if ($this->lastPublicShareData === null) {
+			throw new Exception(__METHOD__ . "No public share data available.");
+		}
+		if (!\in_array($attr, $this->shareResponseFields)) {
+			throw new Exception(
+				__METHOD__ . " attribute $attr is not in the list of allowed attributes"
+			);
+		}
+		if (\count($this->lastPublicShareData->data->element) > 0) {
+			if (!isset($this->lastPublicShareData->data[0]->$attr)) {
+				throw new Exception(__METHOD__ . " No attribute $attr available in the last share data.");
+			}
+			return (string)$this->lastPublicShareData->data[0]->{$attr};
+		}
+
+		if (!isset($this->lastPublicShareData->data->$attr)) {
+			throw new Exception(__METHOD__ . " No attribute $attr available in the last share data.");
+		}
+
+		return (string)$this->lastPublicShareData->data->{$attr};
+	}
+
+	/**
+	 * @return string path of file that was shared (relevant when a single file has been shared)
+	 */
+	public function getLastPublicSharePath():string {
+		if ($this->lastPublicShareData === null) {
+			// There have not been any public links created with the API
+			// so get the path of the last public link created with the webUI
+			return $this->getLastCreatedPublicLinkPath();
+		} else {
+			if (\count($this->lastPublicShareData->data->element) > 0) {
+				return (string)$this->lastPublicShareData->data[0]->path;
+			}
+
+			return (string)$this->lastPublicShareData->data->path;
+		}
+	}
+
 	/**
 	 * Send request for preview of a file in a public link
 	 *
@@ -3508,7 +3678,8 @@ trait Sharing {
 	 * @return void
 	 */
 	public function thePublicAccessesThePreviewOfTheSharedFileUsingTheSharingApi(string $path):void {
-		$token = $this->getLastCreatedPublicShareToken();
+		$shareData = $this->getLastPublicShareData();
+		$token = (string) $shareData->data->token;
 		$this->getPublicPreviewOfFile($path, $token);
 		$this->pushToLastStatusCodesArrays();
 	}
@@ -3529,7 +3700,8 @@ trait Sharing {
 		$this->emptyLastHTTPStatusCodesArray();
 		$this->emptyLastOCSStatusCodesArray();
 		foreach ($paths as $path) {
-			$token = $this->getLastCreatedPublicShareToken();
+			$shareData = $this->getLastPublicShareData();
+			$token = (string) $shareData->data->token;
 			$this->getPublicPreviewOfFile($path["path"], $token);
 			$this->pushToLastStatusCodesArrays();
 		}
@@ -3550,12 +3722,12 @@ trait Sharing {
 		$user = $this->getActualUsername($user);
 		$userPassword = $this->getPasswordForUser($user);
 
-		$shareData = $this->getLastCreatedPublicShare();
-		$owner = (string) $shareData->uid_owner;
-		$name = $this->encodePath((string) $shareData->file_target);
+		$shareData = $this->getLastPublicShareData();
+		$owner = (string) $shareData->data->uid_owner;
+		$name = $this->encodePath((string) $shareData->data->file_target);
 		$name = \trim($name, "/");
-		$ownerDisplayName = (string) $shareData->displayname_owner;
-		$token = (string) $shareData->token;
+		$ownerDisplayName = (string) $shareData->data->displayname_owner;
+		$token = (string) $shareData->data->token;
 
 		if (\strtolower($shareServer) == "remote") {
 			$remote = $this->getRemoteBaseUrl();
@@ -3698,7 +3870,7 @@ trait Sharing {
 	 * @throws GuzzleException
 	 */
 	public function expireLastCreatedPublicLinkShare():void {
-		$shareId = (string) $this->getLastCreatedPublicShare()->id;
+		$shareId = $this->getLastPublicLinkShareId();
 		$this->expireShare($shareId);
 	}
 
diff --git a/tests/acceptance/features/bootstrap/SpacesContext.php b/tests/acceptance/features/bootstrap/SpacesContext.php
index ba827dbb114..f460b5e4485 100644
--- a/tests/acceptance/features/bootstrap/SpacesContext.php
+++ b/tests/acceptance/features/bootstrap/SpacesContext.php
@@ -366,6 +366,20 @@ class SpacesContext implements Context {
 		return \str_replace('"', '\"', $fileData["Etag"][0]);
 	}
 
+	/**
+	 * using method from core to set share data
+	 *
+	 * @return void
+	 */
+	public function setLastShareData(): void {
+		// set last response as PublicShareData
+		$this->featureContext->setLastPublicShareData($this->featureContext->getResponseXml(null, __METHOD__));
+		// set last shareId if ShareData exists
+		if (isset($this->featureContext->getLastPublicShareData()->data)) {
+			$this->featureContext->setLastPublicLinkShareId((string) $this->featureContext->getLastPublicShareData()->data[0]->id);
+		}
+	}
+
 	/**
 	 * @BeforeScenario
 	 *
@@ -1913,8 +1927,7 @@ class SpacesContext implements Context {
 				$body
 			)
 		);
-		$response = $this->featureContext->getResponseXml(null, __METHOD__);
-		$this->featureContext->addToCreatedPublicShares($response->data);
+		$this->setLastShareData();
 	}
 
 	/**
@@ -1967,7 +1980,7 @@ class SpacesContext implements Context {
 	 * @throws JsonException
 	 */
 	public function updateSharedResource(string $user, array $rows):ResponseInterface {
-		$shareId = (string) $this->featureContext->getLastCreatedPublicShare()->id;
+		$shareId = $this->featureContext->getLastPublicLinkShareId();
 		$fullUrl = $this->baseUrl . $this->ocsApiUrl . '/' . $shareId;
 		return  HttpRequestHelper::sendRequest(
 			$fullUrl,
@@ -2037,8 +2050,7 @@ class SpacesContext implements Context {
 			)
 		);
 
-		$response = $this->featureContext->getResponseXml(null, __METHOD__);
-		$this->featureContext->addToCreatedPublicShares($response->data);
+		$this->setLastShareData();
 	}
 
 	/**
@@ -2870,8 +2882,7 @@ class SpacesContext implements Context {
 			)
 		);
 
-		$response = $this->featureContext->getResponseXml(null, __METHOD__);
-		$this->featureContext->addToCreatedPublicShares($response->data);
+		$this->setLastShareData();
 	}
 
 	/**
@@ -2896,6 +2907,7 @@ class SpacesContext implements Context {
 			$expectedHTTPStatus,
 			"Expected response status code should be $expectedHTTPStatus"
 		);
+		$this->featureContext->setLastPublicLinkShareId((string) $this->featureContext->getLastPublicShareData()->data[0]->id);
 	}
 
 	/**
@@ -2940,7 +2952,7 @@ class SpacesContext implements Context {
 		if ($should) {
 			Assert::assertNotEmpty($responseArray, __METHOD__ . ' Response should contain a link, but it is empty');
 			foreach ($responseArray as $element) {
-				$expectedLinkId = (string) $this->featureContext->getLastCreatedPublicShare()->id;
+				$expectedLinkId = $this->featureContext->getLastPublicLinkShareId();
 				Assert::assertEquals($element["id"], $expectedLinkId, "link IDs are different");
 			}
 		} else {
@@ -3219,7 +3231,7 @@ class SpacesContext implements Context {
 	 * @throws GuzzleException|JsonException
 	 */
 	public function publicDownloadsTheFolderFromTheLastCreatedPublicLink(string $resource) {
-		$token = $this->featureContext->getLastCreatedPublicShareToken();
+		$token = $this->featureContext->getLastPublicShareToken();
 		$response = $this->featureContext->listFolderAndReturnResponseXml(
 			$token,
 			$resource,
diff --git a/tests/acceptance/features/bootstrap/WebDav.php b/tests/acceptance/features/bootstrap/WebDav.php
index dbdef1e07b9..5088a8ef5f5 100644
--- a/tests/acceptance/features/bootstrap/WebDav.php
+++ b/tests/acceptance/features/bootstrap/WebDav.php
@@ -1499,7 +1499,7 @@ trait WebDav {
 	 * @throws Exception
 	 */
 	public function publicGetsSizeOfLastSharedPublicLinkUsingTheWebdavApi():void {
-		$tokenArray = $this->getLastCreatedPublicShare()->token;
+		$tokenArray = $this->getLastPublicShareData()->data->token;
 		$token = (string)$tokenArray[0];
 		$url = $this->getBaseUrl() . "/remote.php/dav/public-files/$token";
 		$this->response = HttpRequestHelper::sendRequest(
@@ -5117,7 +5117,7 @@ trait WebDav {
 	 * @throws Exception
 	 */
 	public function theLastPublicDavResponseShouldContainTheseNodes(TableNode $table):void {
-		$user = $this->getLastCreatedPublicShareToken();
+		$user = $this->getLastPublicShareToken();
 		$this->verifyTableNodeColumns($table, ["name"]);
 		$type = $this->usingOldDavPath ? "public-files" : "public-files-new";
 		foreach ($table->getHash() as $row) {
@@ -5136,7 +5136,7 @@ trait WebDav {
 	 * @throws Exception
 	 */
 	public function theLastPublicDavResponseShouldNotContainTheseNodes(TableNode $table):void {
-		$user = $this->getLastCreatedPublicShareToken();
+		$user = $this->getLastPublicShareToken();
 		$this->verifyTableNodeColumns($table, ["name"]);
 		$type = $this->usingOldDavPath ? "public-files" : "public-files-new";
 		foreach ($table->getHash() as $row) {
@@ -5155,7 +5155,7 @@ trait WebDav {
 	 * @throws Exception
 	 */
 	public function thePublicListsTheResourcesInTheLastCreatedPublicLinkWithDepthUsingTheWebdavApi(string $depth):void {
-		$user = $this->getLastCreatedPublicShareToken();
+		$user = $this->getLastPublicShareToken();
 		$response = $this->listFolder(
 			$user,
 			'/',
diff --git a/tests/acceptance/features/bootstrap/WebDavLockingContext.php b/tests/acceptance/features/bootstrap/WebDavLockingContext.php
index df721cd645f..fd8c39f714c 100644
--- a/tests/acceptance/features/bootstrap/WebDavLockingContext.php
+++ b/tests/acceptance/features/bootstrap/WebDavLockingContext.php
@@ -148,7 +148,7 @@ class WebDavLockingContext implements Context {
 	 */
 	public function publicHasLockedLastSharedFile(TableNode $properties) {
 		$this->lockFile(
-			$this->featureContext->getLastCreatedPublicShareToken(),
+			$this->featureContext->getLastPublicShareToken(),
 			"/",
 			$properties,
 			true
@@ -164,7 +164,7 @@ class WebDavLockingContext implements Context {
 	 */
 	public function publicLocksLastSharedFile(TableNode $properties) {
 		$this->lockFile(
-			$this->featureContext->getLastCreatedPublicShareToken(),
+			$this->featureContext->getLastPublicShareToken(),
 			"/",
 			$properties,
 			true,
@@ -185,7 +185,7 @@ class WebDavLockingContext implements Context {
 		TableNode $properties
 	) {
 		$this->lockFile(
-			$this->featureContext->getLastCreatedPublicShareToken(),
+			$this->featureContext->getLastPublicShareToken(),
 			$file,
 			$properties,
 			true
@@ -207,7 +207,7 @@ class WebDavLockingContext implements Context {
 		TableNode $properties
 	) {
 		$this->lockFile(
-			$this->featureContext->getLastCreatedPublicShareToken(),
+			$this->featureContext->getLastPublicShareToken(),
 			$file,
 			$properties,
 			true,
@@ -268,7 +268,7 @@ class WebDavLockingContext implements Context {
 		string $itemToUnlock,
 		string $itemToUseLockOf
 	) {
-		$lockOwner = $this->featureContext->getLastCreatedPublicShareToken();
+		$lockOwner = $this->featureContext->getLastPublicShareToken();
 		$this->unlockItemWithLastLockOfUserAndItemUsingWebDavAPI(
 			$user,
 			$itemToUnlock,
@@ -419,7 +419,7 @@ class WebDavLockingContext implements Context {
 		string $lockOwner,
 		string $itemToUseLockOf
 	) {
-		$user = $this->featureContext->getLastCreatedPublicShareToken();
+		$user = $this->featureContext->getLastPublicShareToken();
 		$this->unlockItemWithLastLockOfUserAndItemUsingWebDavAPI(
 			$user,
 			$itemToUnlock,
@@ -437,7 +437,7 @@ class WebDavLockingContext implements Context {
 	 * @return void
 	 */
 	public function unlockItemAsPublicUsingWebDavAPI(string $itemToUnlock) {
-		$user = $this->featureContext->getLastCreatedPublicShareToken();
+		$user = $this->featureContext->getLastPublicShareToken();
 		$this->unlockItemWithLastLockOfUserAndItemUsingWebDavAPI(
 			$user,
 			$itemToUnlock,
@@ -592,7 +592,7 @@ class WebDavLockingContext implements Context {
 		string $itemToUseLockOf,
 		string $publicWebDAVAPIVersion
 	) {
-		$lockOwner = $this->featureContext->getLastCreatedPublicShareToken();
+		$lockOwner = $this->featureContext->getLastPublicShareToken();
 		$this->publicUploadFileSendingLockTokenOfUser(
 			$filename,
 			$content,
diff --git a/tests/acceptance/features/bootstrap/WebDavPropertiesContext.php b/tests/acceptance/features/bootstrap/WebDavPropertiesContext.php
index 3abf131b89c..beef5ebe5de 100644
--- a/tests/acceptance/features/bootstrap/WebDavPropertiesContext.php
+++ b/tests/acceptance/features/bootstrap/WebDavPropertiesContext.php
@@ -289,7 +289,7 @@ class WebDavPropertiesContext implements Context {
 	 * @throws Exception
 	 */
 	public function publicGetsThePropertiesOfFolder(string $path, TableNode $propertiesTable):void {
-		$user = $this->featureContext->getLastCreatedPublicShareToken();
+		$user = $this->featureContext->getLastPublicShareToken();
 		$properties = null;
 		foreach ($propertiesTable->getRows() as $row) {
 			$properties[] = $row[0];
@@ -890,7 +890,7 @@ class WebDavPropertiesContext implements Context {
 				[
 					"code" => "%public_token%",
 					"function" =>
-					[$this->featureContext, "getLastCreatedPublicShareToken"],
+					[$this->featureContext, "getLastPublicShareToken"],
 					"parameter" => []
 				],
 			]