Repository: owncloud/ocis
Pull request #9686 (2024-07-24 21:19:24)

feat: Microsoft office 365 Cloud and Office Online Server support
<!--
Thanks for submitting a change to ownCloud!

This is the bug tracker for the oCIS component. Find other components at https://github.com/owncloud/core/blob/master/.github/CONTRIBUTING.md#guidelines

For fixing potential security issues please see https://owncloud.org/security/

To make it possible for us to get your change reviewed and merged please carefully fill out the requested information below.

Please note that any kind of change needs first be submitted to the master branch which holds the next version of oCIS.

Please set the following labels:

- Set label "Status:Needs-Review" for review or "Status:In-Progress" if the PR still has open tasks.
- Assignment: assign to self
- Reviewers: pick at least one
-->

## Description

1. Add Support for MS office365 Cloud via the Cloud Provider Program
2. Add Support for MS office365 Cloud via Proxy
3. Add Support for MS Office Online Server on Premises

## Related Issue

- Fixes [Entreprise #6790](https://github.com/owncloud/enterprise/issues/6790)

## Motivation and Context

Web Office Integration

## How Has This Been Tested?
- test environment: Office Online Server 
- test case Office 365 Cloud

## Screenshots (if appropriate):
### Word 
![1237846198723127](https://github.com/user-attachments/assets/bd4e8644-fb3d-45f8-991c-e0e74b123aae)
### Excel
![242352342342324](https://github.com/user-attachments/assets/166ae10e-34eb-4c49-8d53-c75197b52799)
### Powerpoint

![126351267351923](https://github.com/user-attachments/assets/76678a4d-c6c0-4152-bfb6-36805163b9ac)


## 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.

sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [1 New Bugs](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) (required ≤ 0)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)

##   
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png '') Catch issues before they fail your Quality Gate with our IDE extension ![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png '') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=pull-request)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [2.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [2.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [2.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [5 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [20.1% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [5 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [18.7% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [4 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [13.6% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [4 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [13.6% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [4 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [13.6% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [4 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [13.6% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [5 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [11.5% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [5 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [9.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [5 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [5.4% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


micbar: @JammingBen 
I will not finish that before my vacation.

## Deliverables from Web

To continue, i would need some information from the web team

1. HostEditUrl and HostViewUrl - these urls are the Web Urls where we can access the opened editor in the correct view mode. Like <ocis-url>/external-office365 . @JammingBen has pointed me to https://github.com/owncloud/web/pull/9879/
2. Sharing URL which opens the Web App with the opened sidebar ready for sharing

## Language codes

The language codes which are accepted by MS are only the long ones like `de-DE` and `en-US`. Currently this is implemented as a "workaround" in this PR. Would be nice to get this sorted out with web.
mmattel: With regards to the proxy envvars, do we have a similar situation as described in
[Microsoft Office Online / WOPI Integration](https://doc.owncloud.com/server/10.15/admin_manual/enterprise/collaboration/msoffice-wopi-integration.html)?
We should in any case do some doc work in the collaboration readme...
jvillafanez: Linking with https://github.com/owncloud/ocis/pull/9827 because it probably should be applied before this PR. In addition to simplify the code, it removes the `HostViewURL` and `HostEditURL` from the WOPI context. They're expected to be re-added at some point (probably with this PR).

In addition to that, the connector's methods are now returning a `ConnectorResponse` which basically includes the expected HTTP status, headers and body (the body is expected to be json-encoded later). New headers added in this PR are expected to be handled within the connector, so the httpAdapter just needs to forward the data. We need to re-adjust the PR to fit it.

For the wopisrc package, note that the `PutRelativeFile` operation needs to generate a new wopisrc link to the new file. This is currently done in the `FileConnector` object, but we'll need merge it with the new code in this PR in order to support the wopi proxy. Also note that the `PutFile` operation is used in the `PutRelativeFile` operation; I don't think this PR will affect it (we probably don't need to check new information), but we should double-check.
sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [12 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [5.3% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [12 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [5.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [12 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [5.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [3 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [6.1% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [2 New Bugs](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) (required ≤ 0)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)

##   
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png '') Catch issues before they fail your Quality Gate with our IDE extension ![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png '') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=pull-request)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [3 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [63.7% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [6.1% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [2 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [63.5% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [4.5% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [2 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [64.1% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [4.3% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [2 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [63.5% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [4.3% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [2 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [4.3% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [2 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [4.3% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [2 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [4.3% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [2 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [4.3% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [2 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9686&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9686&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [4.3% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9686&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9686)



Patch:
diff --git a/changelog/unreleased/microsoft-cloud-on-prem.md b/changelog/unreleased/microsoft-cloud-on-prem.md
new file mode 100644
index 00000000000..3013a325b5d
--- /dev/null
+++ b/changelog/unreleased/microsoft-cloud-on-prem.md
@@ -0,0 +1,6 @@
+Enhancement: Microsoft Office365 and Office Online support
+
+Add support for Microsoft Office365 Cloud and Microsoft Office Online on premises. You can use the cloud feature either within a Microsoft [CSP](https://learn.microsoft.com/en-us/partner-center/enroll/csp-overview) partnership or via the ownCloud office365 proxy subscription.
+Please contact sales@owncloud.com to get more information about the ownCloud office365 proxy subscription.
+
+https://github.com/owncloud/ocis/pull/9686
diff --git a/services/collaboration/README.md b/services/collaboration/README.md
index 205c54e4800..352d9ef9df5 100644
--- a/services/collaboration/README.md
+++ b/services/collaboration/README.md
@@ -4,6 +4,8 @@ The collaboration service connects ocis with document servers such as Collabora
 
 Since this service requires an external document server, it won't start by default when using `ocis server`. You must start it manually with the `ocis collaboration server` command.
 
+This service needs to be part of the ocis service mesh. It is not intended to be used as a standalone service. You must share the common config variables like OCIS_URL, OCIS_JWT_SECRET and OCIS_REVA_GATEWAY betweed this service and the other ocis services. In addition to that, MICRO_REGISTRY_ADDRESS should point to the main ocis service registry.
+
 ## Requirements
 
 The collaboration service requires the target document server (ONLYOFFICE, Collabora, etc.) to be up and running. Additionally, some Infinite Scale services are also required to be running in order to register the GRPC service for the `open in app` action in the webUI. The following internal and external services need to be available:
@@ -18,6 +20,9 @@ If any of the named services above have not been started or are not reachable, t
 
 There are a few variables that you need to set:
 
+* `COLLABORATION_APP_NAME`:\
+  The name of the connected WebOffice app, either `Collabora`, `OnlyOffice`, `Microsoft365` or `MicrosoftOfficeOnline`.
+
 * `COLLABORATION_APP_ADDR`:\
   The URL of the collaborative editing app (onlyoffice, collabora, etc).\
   For example: `https://office.example.com`.
diff --git a/services/collaboration/mocks/content_connector_service.go b/services/collaboration/mocks/content_connector_service.go
index da6ff575cff..887c9de10cd 100644
--- a/services/collaboration/mocks/content_connector_service.go
+++ b/services/collaboration/mocks/content_connector_service.go
@@ -7,6 +7,8 @@ import (
 
 	connector "github.com/owncloud/ocis/v2/services/collaboration/pkg/connector"
 
+	http "net/http"
+
 	io "io"
 
 	mock "github.com/stretchr/testify/mock"
@@ -25,17 +27,17 @@ func (_m *ContentConnectorService) EXPECT() *ContentConnectorService_Expecter {
 	return &ContentConnectorService_Expecter{mock: &_m.Mock}
 }
 
-// GetFile provides a mock function with given fields: ctx, writer
-func (_m *ContentConnectorService) GetFile(ctx context.Context, writer io.Writer) error {
-	ret := _m.Called(ctx, writer)
+// GetFile provides a mock function with given fields: ctx, w
+func (_m *ContentConnectorService) GetFile(ctx context.Context, w http.ResponseWriter) error {
+	ret := _m.Called(ctx, w)
 
 	if len(ret) == 0 {
 		panic("no return value specified for GetFile")
 	}
 
 	var r0 error
-	if rf, ok := ret.Get(0).(func(context.Context, io.Writer) error); ok {
-		r0 = rf(ctx, writer)
+	if rf, ok := ret.Get(0).(func(context.Context, http.ResponseWriter) error); ok {
+		r0 = rf(ctx, w)
 	} else {
 		r0 = ret.Error(0)
 	}
@@ -50,14 +52,14 @@ type ContentConnectorService_GetFile_Call struct {
 
 // GetFile is a helper method to define mock.On call
 //   - ctx context.Context
-//   - writer io.Writer
-func (_e *ContentConnectorService_Expecter) GetFile(ctx interface{}, writer interface{}) *ContentConnectorService_GetFile_Call {
-	return &ContentConnectorService_GetFile_Call{Call: _e.mock.On("GetFile", ctx, writer)}
+//   - w http.ResponseWriter
+func (_e *ContentConnectorService_Expecter) GetFile(ctx interface{}, w interface{}) *ContentConnectorService_GetFile_Call {
+	return &ContentConnectorService_GetFile_Call{Call: _e.mock.On("GetFile", ctx, w)}
 }
 
-func (_c *ContentConnectorService_GetFile_Call) Run(run func(ctx context.Context, writer io.Writer)) *ContentConnectorService_GetFile_Call {
+func (_c *ContentConnectorService_GetFile_Call) Run(run func(ctx context.Context, w http.ResponseWriter)) *ContentConnectorService_GetFile_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(io.Writer))
+		run(args[0].(context.Context), args[1].(http.ResponseWriter))
 	})
 	return _c
 }
@@ -67,7 +69,7 @@ func (_c *ContentConnectorService_GetFile_Call) Return(_a0 error) *ContentConnec
 	return _c
 }
 
-func (_c *ContentConnectorService_GetFile_Call) RunAndReturn(run func(context.Context, io.Writer) error) *ContentConnectorService_GetFile_Call {
+func (_c *ContentConnectorService_GetFile_Call) RunAndReturn(run func(context.Context, http.ResponseWriter) error) *ContentConnectorService_GetFile_Call {
 	_c.Call.Return(run)
 	return _c
 }
diff --git a/services/collaboration/pkg/config/app.go b/services/collaboration/pkg/config/app.go
index ee569189a65..222f9b38f78 100644
--- a/services/collaboration/pkg/config/app.go
+++ b/services/collaboration/pkg/config/app.go
@@ -10,7 +10,8 @@ type App struct {
 	Addr     string `yaml:"addr" env:"COLLABORATION_APP_ADDR" desc:"The URL where the WOPI app is located, such as https://127.0.0.1:8080." introductionVersion:"6.0.0"`
 	Insecure bool   `yaml:"insecure" env:"COLLABORATION_APP_INSECURE" desc:"Skip TLS certificate verification when connecting to the WOPI app" introductionVersion:"6.0.0"`
 
-	ProofKeys ProofKeys `yaml:"proofkeys"`
+	ProofKeys          ProofKeys `yaml:"proofkeys"`
+	LicenseCheckEnable bool      `yaml:"licensecheckenable" env:"COLLABORATION_APP_LICENSE_CHECK_ENABLE" desc:"Enable license check for edit. Needs to be enabled when using Microsoft365 with the business flow." introductionVersion:"%%NEXT%%"`
 }
 
 type ProofKeys struct {
diff --git a/services/collaboration/pkg/config/wopi.go b/services/collaboration/pkg/config/wopi.go
index 51e4caa3bd5..aa5bc342358 100644
--- a/services/collaboration/pkg/config/wopi.go
+++ b/services/collaboration/pkg/config/wopi.go
@@ -4,5 +4,7 @@ package config
 type Wopi struct {
 	WopiSrc     string `yaml:"wopisrc" env:"COLLABORATION_WOPI_SRC" desc:"The WOPISrc base URL containing schema, host and port. Set this to the schema and domain where the collaboration service is reachable for the wopi app, such as https://office.owncloud.test." introductionVersion:"6.0.0"`
 	Secret      string `yaml:"secret" env:"COLLABORATION_WOPI_SECRET" desc:"Used to mint and verify WOPI JWT tokens and encrypt and decrypt the REVA JWT token embedded in the WOPI JWT token." introductionVersion:"6.0.0"`
-	DisableChat bool   `yaml:"disable_chat" env:"COLLABORATION_WOPI_DISABLE_CHAT;OCIS_WOPI_DISABLE_CHAT" desc:"Disable chat in the frontend." introductionVersion:"%%NEXT%%"`
+	DisableChat bool   `yaml:"disable_chat" env:"COLLABORATION_WOPI_DISABLE_CHAT;OCIS_WOPI_DISABLE_CHAT" desc:"Disable chat in the frontend. This feature is available in OnlyOffice and Microsoft." introductionVersion:"%%NEXT%%"`
+	ProxyURL    string `yaml:"proxy_url" env:"COLLABORATION_WOPI_PROXY_URL" desc:"The URL to the ownCloud Office365 WOPI proxy. Optional. To use this feature, you need an office365 proxy subscription. If you become part of the Microsoft CSP program (https://learn.microsoft.com/en-us/partner-center/enroll/csp-overview), you can use the WebOffice without a proxy." introductionVersion:"%%NEXT%%"`
+	ProxySecret string `yaml:"proxy_secret" env:"COLLABORATION_WOPI_PROXY_SECRET" desc:"The secret to authenticate against the ownCloud Office365 WOPI proxy. Optional. This secret can be obtained from ownCloud via the office365 proxy subscription." introductionVersion:"%%NEXT%%"`
 }
diff --git a/services/collaboration/pkg/connector/connector.go b/services/collaboration/pkg/connector/connector.go
index 8cccc7dfbee..0e1e821bfb3 100644
--- a/services/collaboration/pkg/connector/connector.go
+++ b/services/collaboration/pkg/connector/connector.go
@@ -1,5 +1,11 @@
 package connector
 
+import (
+	"strconv"
+
+	types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
+)
+
 // ConnectorResponse represent a response from the FileConnectorService.
 // The ConnectorResponse is oriented to HTTP, so it has the Status, Headers
 // and Body that the actual HTTP response should have. This includes HTTP
@@ -33,6 +39,48 @@ func NewResponseWithLock(status int, lockID string) *ConnectorResponse {
 	}
 }
 
+// NewResponseLockConflict creates a new ConnectorResponse with the status 409
+// and the "X-WOPI-Lock" header having the value in the lockID parameter.
+//
+// This is used for conflict responses where the current lock id needs
+// to be returned, although the `GetLock` method also uses this method for a
+// successful response (with the lock id included)
+// The lockFailureReason parameter will be included in the "X-WOPI-LockFailureReason".
+func NewResponseLockConflict(lockID string, lockFailureReason string) *ConnectorResponse {
+	return &ConnectorResponse{
+		Status: 409,
+		Headers: map[string]string{
+			HeaderWopiLock:              lockID,
+			HeaderWopiLockFailureReason: lockFailureReason,
+		},
+	}
+}
+
+// NewResponseWithVersion creates a new ConnectorResponse with the specified status
+// and the "X-WOPI-ItemVersion" header having the value in the mtime parameter.
+func NewResponseWithVersion(mtime *types.Timestamp) *ConnectorResponse {
+	return &ConnectorResponse{
+		Status: 200,
+		Headers: map[string]string{
+			HeaderWopiVersion: getVersion(mtime),
+		},
+	}
+}
+
+// NewResponseWithVersionAndLock creates a new ConnectorResponse with the specified status
+// and the "X-WOPI-ItemVersion" header and the "X-WOPI-Lock" header
+// having the values in the mtime and lockID parameters.
+func NewResponseWithVersionAndLock(status int, mtime *types.Timestamp, lockID string) *ConnectorResponse {
+	r := &ConnectorResponse{
+		Status: status,
+		Headers: map[string]string{
+			HeaderWopiVersion: getVersion(mtime),
+			HeaderWopiLock:    lockID,
+		},
+	}
+	return r
+}
+
 // NewResponseSuccessBody creates a new ConnectorResponse with a fixed 200
 // (success) status and the specified body. The headers will be nil.
 //
@@ -136,3 +184,9 @@ func (c *Connector) GetFileConnector() FileConnectorService {
 func (c *Connector) GetContentConnector() ContentConnectorService {
 	return c.contentConnector
 }
+
+// getVersion returns a string representation of the timestamp
+func getVersion(timestamp *types.Timestamp) string {
+	return "v" + strconv.FormatUint(timestamp.GetSeconds(), 10) +
+		strconv.FormatUint(uint64(timestamp.GetNanos()), 10)
+}
diff --git a/services/collaboration/pkg/connector/contentconnector.go b/services/collaboration/pkg/connector/contentconnector.go
index ead86d397cf..3f4fbf0266c 100644
--- a/services/collaboration/pkg/connector/contentconnector.go
+++ b/services/collaboration/pkg/connector/contentconnector.go
@@ -29,7 +29,7 @@ import (
 // Target file is within the WOPI context
 type ContentConnectorService interface {
 	// GetFile downloads the file and write its contents in the provider writer
-	GetFile(ctx context.Context, writer io.Writer) error
+	GetFile(ctx context.Context, w http.ResponseWriter) error
 	// PutFile uploads the stream up to the stream length. The file should be
 	// locked beforehand, so the lockID needs to be provided.
 	// The current lockID will be returned ONLY if a conflict happens (the file is
@@ -61,9 +61,11 @@ func NewContentConnector(gwc gatewayv1beta1.GatewayAPIClient, cfg *config.Config
 // You can pass a pre-configured zerologger instance through the context that
 // will be used to log messages.
 //
-// The contents of the file will be written directly into the writer passed as
+// The contents of the file will be written directly into the http Response writer passed as
 // parameter.
-func (c *ContentConnector) GetFile(ctx context.Context, writer io.Writer) error {
+// Be aware that the body of the response will be written during the execution of this method.
+// Any further modifications to the response headers or body will be ignored.
+func (c *ContentConnector) GetFile(ctx context.Context, w http.ResponseWriter) error {
 	wopiContext, err := middleware.WopiContextFromCtx(ctx)
 	if err != nil {
 		return err
@@ -74,6 +76,16 @@ func (c *ContentConnector) GetFile(ctx context.Context, writer io.Writer) error
 		Logger()
 	logger.Debug().Msg("GetFile: start")
 
+	sResp, err := c.gwc.Stat(ctx, &providerv1beta1.StatRequest{
+		Ref: wopiContext.FileReference,
+	})
+	if err != nil {
+		logger.Error().Err(err).Msg("GetFile: Stat Request failed")
+		return err
+	}
+	if sResp.GetStatus().GetCode() != rpcv1beta1.Code_CODE_OK {
+		return NewConnectorError(500, sResp.GetStatus().GetCode().String()+" "+sResp.GetStatus().GetMessage())
+	}
 	// Initiate download request
 	req := &providerv1beta1.InitiateFileDownloadRequest{
 		Ref: wopiContext.FileReference,
@@ -168,13 +180,14 @@ func (c *ContentConnector) GetFile(ctx context.Context, writer io.Writer) error
 		return NewConnectorError(500, "GetFile: Downloading the file failed")
 	}
 
+	w.Header().Set(HeaderWopiVersion, getVersion(sResp.GetInfo().GetMtime()))
+
 	// Copy the download into the writer
-	_, err = io.Copy(writer, httpResp.Body)
+	_, err = io.Copy(w, httpResp.Body)
 	if err != nil {
 		logger.Error().Msg("GetFile: copying the file content to the response body failed")
 		return err
 	}
-
 	logger.Debug().Msg("GetFile: success")
 	return nil
 }
@@ -199,6 +212,8 @@ func (c *ContentConnector) GetFile(ctx context.Context, writer io.Writer) error
 // lock ID that should be used in the X-WOPI-Lock header. In other error
 // cases or if the method is successful, an empty string will be returned
 // (check for err != nil to know if something went wrong)
+//
+// On success, the method will return the new mtime of the file
 func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, streamLength int64, lockID string) (*ConnectorResponse, error) {
 	wopiContext, err := middleware.WopiContextFromCtx(ctx)
 	if err != nil {
@@ -230,13 +245,14 @@ func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, stream
 		return NewResponse(500), nil
 	}
 
+	mtime := statRes.GetInfo().GetMtime()
 	// If there is a lock and it mismatches, return 409
 	if statRes.GetInfo().GetLock() != nil && statRes.GetInfo().GetLock().GetLockId() != lockID {
 		logger.Error().
 			Str("LockID", statRes.GetInfo().GetLock().GetLockId()).
 			Msg("PutFile: wrong lock")
 		// onlyoffice says it's required to send the current lockId, MS doesn't say anything
-		return NewResponseWithLock(409, statRes.GetInfo().GetLock().GetLockId()), nil
+		return NewResponseLockConflict(statRes.GetInfo().GetLock().GetLockId(), "Lock Mismatch"), nil
 	}
 
 	// only unlocked uploads can go through if the target file is empty,
@@ -246,7 +262,7 @@ func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, stream
 	if lockID == "" && statRes.GetInfo().GetLock() == nil && statRes.GetInfo().GetSize() > 0 {
 		logger.Error().Msg("PutFile: file must be locked first")
 		// onlyoffice says to send an empty string if the file is unlocked, MS doesn't say anything
-		return NewResponseWithLock(409, ""), nil
+		return NewResponseLockConflict("", "Cannot PutFile on unlocked file"), nil
 	}
 
 	// Prepare the data to initiate the upload
@@ -367,8 +383,25 @@ func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, stream
 				Msg("UploadHelper: Put request to the upload endpoint failed with unexpected status")
 			return NewResponse(500), nil
 		}
+		// We need a stat call on the target file after the upload to get the
+		// new mtime
+		statResAfter, err := c.gwc.Stat(ctx, &providerv1beta1.StatRequest{
+			Ref: wopiContext.FileReference,
+		})
+		if err != nil {
+			logger.Error().Err(err).Msg("PutFile: stat after upload failed")
+			return nil, err
+		}
+		if statResAfter.GetStatus().GetCode() != rpcv1beta1.Code_CODE_OK {
+			logger.Error().
+				Str("StatusCode", statRes.GetStatus().GetCode().String()).
+				Str("StatusMsg", statRes.GetStatus().GetMessage()).
+				Msg("PutFile: stat after upload failed with unexpected status")
+			return NewResponse(500), nil
+		}
+		mtime = statResAfter.GetInfo().GetMtime()
 	}
 
 	logger.Debug().Msg("PutFile: success")
-	return NewResponse(200), nil
+	return NewResponseWithVersion(mtime), nil
 }
diff --git a/services/collaboration/pkg/connector/fileconnector.go b/services/collaboration/pkg/connector/fileconnector.go
index d23e93c7358..a934fb5f2e5 100644
--- a/services/collaboration/pkg/connector/fileconnector.go
+++ b/services/collaboration/pkg/connector/fileconnector.go
@@ -9,7 +9,6 @@ import (
 	"io"
 	"net/url"
 	"path"
-	"strconv"
 	"strings"
 	"time"
 
@@ -19,12 +18,15 @@ import (
 	rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
 	providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
+	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
+	"github.com/cs3org/reva/v2/pkg/storagespace"
 	"github.com/cs3org/reva/v2/pkg/utils"
 	"github.com/google/uuid"
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/connector/fileinfo"
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/helpers"
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/middleware"
+	"github.com/owncloud/ocis/v2/services/collaboration/pkg/wopisrc"
 	"github.com/rs/zerolog"
 )
 
@@ -52,7 +54,7 @@ type FileConnectorService interface {
 	// needs to be provided.
 	// The current lockID will be returned if a conflict happens
 	RefreshLock(ctx context.Context, lockID string) (*ConnectorResponse, error)
-	// Unlock will unlock the target file. The current lockID needs to be
+	// UnLock will unlock the target file. The current lockID needs to be
 	// provided.
 	// The current lockID will be returned if a conflict happens
 	UnLock(ctx context.Context, lockID string) (*ConnectorResponse, error)
@@ -172,6 +174,8 @@ func (f *FileConnector) GetLock(ctx context.Context) (*ConnectorResponse, error)
 // the method will return an empty lock id.
 //
 // For the "unlock and relock" operation, the behavior will be the same.
+//
+// On success, the mtime of the file will be returned in the X-Wopi-Version header.
 func (f *FileConnector) Lock(ctx context.Context, lockID, oldLockID string) (*ConnectorResponse, error) {
 	wopiContext, err := middleware.WopiContextFromCtx(ctx)
 	if err != nil {
@@ -236,11 +240,26 @@ func (f *FileConnector) Lock(ctx context.Context, lockID, oldLockID string) (*Co
 		setOrRefreshStatus = resp.GetStatus()
 	}
 
+	statResp, err := f.gwc.Stat(ctx, &providerv1beta1.StatRequest{
+		Ref: wopiContext.FileReference,
+	})
+	if err != nil {
+		logger.Error().Err(err).Msg("Lock failed trying to get the file info")
+		return nil, err
+	}
+	if statResp.GetStatus().GetCode() != rpcv1beta1.Code_CODE_OK {
+		logger.Error().
+			Str("StatusCode", statResp.GetStatus().GetCode().String()).
+			Str("StatusMsg", statResp.GetStatus().GetMessage()).
+			Msg("Lock failed trying to get the file info with unexpected status")
+		return NewResponse(500), nil
+	}
+
 	// we're checking the status of either the "SetLock" or "RefreshLock" operations
 	switch setOrRefreshStatus.GetCode() {
 	case rpcv1beta1.Code_CODE_OK:
 		logger.Debug().Msg("SetLock successful")
-		return NewResponse(200), nil
+		return NewResponseWithVersion(statResp.GetInfo().GetMtime()), nil
 
 	case rpcv1beta1.Code_CODE_FAILED_PRECONDITION, rpcv1beta1.Code_CODE_ABORTED:
 		// Code_CODE_FAILED_PRECONDITION -> Lock operation mismatched lock
@@ -270,7 +289,7 @@ func (f *FileConnector) Lock(ctx context.Context, lockID, oldLockID string) (*Co
 				logger.Warn().
 					Str("LockID", resp.GetLock().GetLockId()).
 					Msg("SetLock conflict")
-				return NewResponseWithLock(409, resp.GetLock().GetLockId()), nil
+				return NewResponseLockConflict(resp.GetLock().GetLockId(), "Conflicting LockID"), nil
 			}
 
 			// TODO: according to the spec we need to treat this as a RefreshLock
@@ -281,7 +300,7 @@ func (f *FileConnector) Lock(ctx context.Context, lockID, oldLockID string) (*Co
 			logger.Warn().
 				Str("LockID", resp.GetLock().GetLockId()).
 				Msg("SetLock lock refreshed instead")
-			return NewResponse(200), nil // no need to send the lockID for a 200 code
+			return NewResponseWithVersion(statResp.GetInfo().GetMtime()), nil
 		}
 
 		logger.Error().Msg("SetLock failed and could not refresh")
@@ -313,6 +332,8 @@ func (f *FileConnector) Lock(ctx context.Context, lockID, oldLockID string) (*Co
 // return an empty lock id.
 // The conflict happens if the provided lockID doesn't match the one actually
 // applied in the target file.
+//
+// On success, the mtime of the file will be returned in the X-Wopi-Version header.
 func (f *FileConnector) RefreshLock(ctx context.Context, lockID string) (*ConnectorResponse, error) {
 	wopiContext, err := middleware.WopiContextFromCtx(ctx)
 	if err != nil {
@@ -347,10 +368,27 @@ func (f *FileConnector) RefreshLock(ctx context.Context, lockID string) (*Connec
 		return nil, err
 	}
 
+	statResp, err := f.gwc.Stat(ctx, &providerv1beta1.StatRequest{
+		Ref: wopiContext.FileReference,
+	})
+	if err != nil {
+		logger.Error().Err(err).Msg("RefreshLock failed trying to get the file info")
+		return nil, err
+	}
+	if statResp.GetStatus().GetCode() != rpcv1beta1.Code_CODE_OK {
+		logger.Error().
+			Str("StatusCode", statResp.GetStatus().GetCode().String()).
+			Str("StatusMsg", statResp.GetStatus().GetMessage()).
+			Msg("RefreshLock failed trying to get the file info with unexpected status")
+		return NewResponse(500), nil
+	}
+
 	switch resp.GetStatus().GetCode() {
 	case rpcv1beta1.Code_CODE_OK:
 		logger.Debug().Msg("RefreshLock successful")
-		return NewResponse(200), nil
+		// The current lock should not be returned in the headers on success
+		// https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/refreshlock#response-headers
+		return NewResponseWithVersion(statResp.GetInfo().GetMtime()), nil
 
 	case rpcv1beta1.Code_CODE_NOT_FOUND:
 		logger.Error().
@@ -390,7 +428,7 @@ func (f *FileConnector) RefreshLock(ctx context.Context, lockID string) (*Connec
 				Str("StatusCode", resp.GetStatus().GetCode().String()).
 				Str("StatusMsg", resp.GetStatus().GetMessage()).
 				Msg("RefreshLock failed, no lock on file")
-			return NewResponseWithLock(409, ""), nil
+			return NewResponseLockConflict("", "No lock on file"), nil
 		} else {
 			// lock is different than the one requested, otherwise we wouldn't reached this point
 			logger.Error().
@@ -398,7 +436,7 @@ func (f *FileConnector) RefreshLock(ctx context.Context, lockID string) (*Connec
 				Str("StatusCode", resp.GetStatus().GetCode().String()).
 				Str("StatusMsg", resp.GetStatus().GetMessage()).
 				Msg("RefreshLock failed, lock mismatch")
-			return NewResponseWithLock(409, resp.GetLock().GetLockId()), nil
+			return NewResponseLockConflict(resp.GetLock().GetLockId(), "Lock mismatch"), nil
 		}
 	default:
 		logger.Error().
@@ -422,6 +460,8 @@ func (f *FileConnector) RefreshLock(ctx context.Context, lockID string) (*Connec
 // return an empty lock id.
 // The conflict happens if the provided lockID doesn't match the one actually
 // applied in the target file.
+//
+// On success, the mtime of the file will be returned in the X-Wopi-Version header.
 func (f *FileConnector) UnLock(ctx context.Context, lockID string) (*ConnectorResponse, error) {
 	wopiContext, err := middleware.WopiContextFromCtx(ctx)
 	if err != nil {
@@ -452,14 +492,29 @@ func (f *FileConnector) UnLock(ctx context.Context, lockID string) (*ConnectorRe
 		return nil, err
 	}
 
+	statResp, err := f.gwc.Stat(ctx, &providerv1beta1.StatRequest{
+		Ref: wopiContext.FileReference,
+	})
+	if err != nil {
+		logger.Error().Err(err).Msg("Unlock failed trying to get the file info")
+		return nil, err
+	}
+	if statResp.GetStatus().GetCode() != rpcv1beta1.Code_CODE_OK {
+		logger.Error().
+			Str("StatusCode", statResp.GetStatus().GetCode().String()).
+			Str("StatusMsg", statResp.GetStatus().GetMessage()).
+			Msg("Unlock failed trying to get the file info with unexpected status")
+		return NewResponse(500), nil
+	}
+
 	switch resp.GetStatus().GetCode() {
 	case rpcv1beta1.Code_CODE_OK:
 		logger.Debug().Msg("Unlock successful")
-		return NewResponse(200), nil
+		return NewResponseWithVersion(statResp.GetInfo().GetMtime()), nil
 	case rpcv1beta1.Code_CODE_ABORTED:
 		// File isn't locked. Need to return 409 with empty lock
 		logger.Error().Err(err).Msg("Unlock failed, file isn't locked")
-		return NewResponseWithLock(409, ""), nil
+		return NewResponseLockConflict("", "File isn't locked"), nil
 	case rpcv1beta1.Code_CODE_LOCKED:
 		// We need to return 409 with the current lock
 		req := &providerv1beta1.GetLockRequest{
@@ -496,7 +551,7 @@ func (f *FileConnector) UnLock(ctx context.Context, lockID string) (*ConnectorRe
 				Msg("Unlock failed, lock mismatch")
 			outLockId = resp.GetLock().GetLockId()
 		}
-		return NewResponseWithLock(409, outLockId), nil
+		return NewResponseLockConflict(outLockId, "Lock mismatch"), nil
 	default:
 		logger.Error().
 			Str("StatusCode", resp.GetStatus().GetCode().String()).
@@ -612,7 +667,7 @@ func (f *FileConnector) PutRelativeFileSuggested(ctx context.Context, ccs Conten
 		return nil, err
 	}
 
-	wopiSrcURL, err := f.generateWOPISrc(ctx, wopiContext, newLogger)
+	wopiSrcURL, err := f.generateWOPISrc(wopiContext, newLogger)
 	if err != nil {
 		logger.Error().Err(err).Msg("PutRelativeFileSuggested: error generating the WOPISrc parameter")
 		return nil, err
@@ -708,7 +763,7 @@ func (f *FileConnector) PutRelativeFileRelative(ctx context.Context, ccs Content
 		}
 		// if conflict generate a different name and retry.
 		// this should happen only once
-		wopiSrcURL, err2 := f.generateWOPISrc(ctx, wopiContext, newLogger)
+		wopiSrcURL, err2 := f.generateWOPISrc(wopiContext, newLogger)
 		if err2 != nil {
 			newLogger.Error().
 				Err(err2).
@@ -724,12 +779,13 @@ func (f *FileConnector) PutRelativeFileRelative(ctx context.Context, ccs Content
 			Str("LockID", lockID).
 			Msg("PutRelativeFileRelative: error conflict")
 
-			// need to build the response ourselves
+		// need to build the response ourselves
 		return &ConnectorResponse{
 			Status: 409,
 			Headers: map[string]string{
-				HeaderWopiValidRT: finalTarget,
-				HeaderWopiLock:    lockID,
+				HeaderWopiValidRT:           finalTarget,
+				HeaderWopiLock:              lockID,
+				HeaderWopiLockFailureReason: "Lock Conflict",
 			},
 			Body: map[string]interface{}{
 				"Name": target,
@@ -747,7 +803,7 @@ func (f *FileConnector) PutRelativeFileRelative(ctx context.Context, ccs Content
 		return nil, err
 	}
 
-	wopiSrcURL, err := f.generateWOPISrc(ctx, wopiContext, newLogger)
+	wopiSrcURL, err := f.generateWOPISrc(wopiContext, newLogger)
 	if err != nil {
 		newLogger.Error().Err(err).Msg("PutRelativeFileRelative: error generating the WOPISrc parameter")
 		return nil, err
@@ -798,7 +854,8 @@ func (f *FileConnector) DeleteFile(ctx context.Context, lockID string) (*Connect
 		if deleteRes.GetStatus().GetCode() == rpcv1beta1.Code_CODE_TOO_EARLY {
 			// starting from 20ms, double the waiting time for each retry
 			// capping at 5 secs
-			waitingTime := (20 * time.Millisecond) << retries
+			var waitingTime time.Duration
+			waitingTime = (20 * time.Millisecond) << retries
 			if waitingTime.Seconds() > 5 {
 				waitingTime = 5 * time.Second
 			}
@@ -849,7 +906,7 @@ func (f *FileConnector) DeleteFile(ctx context.Context, lockID string) (*Connect
 			logger.Error().
 				Str("LockID", resp.GetLock().GetLockId()).
 				Msg("DeleteFile: file is locked")
-			return NewResponseWithLock(409, resp.GetLock().GetLockId()), nil
+			return NewResponseLockConflict(resp.GetLock().GetLockId(), "File is locked"), nil
 		} else {
 			// return the original error since the file isn't locked
 			logger.Error().Msg("DeleteFile: delete failed on unlocked file")
@@ -942,7 +999,7 @@ func (f *FileConnector) RenameFile(ctx context.Context, lockID, target string) (
 					Str("StatusCode", moveRes.GetStatus().GetCode().String()).
 					Str("StatusMsg", moveRes.GetStatus().GetMessage()).
 					Msg("RenameFile: conflict")
-				return NewResponseWithLock(409, currentLockID), nil
+				return NewResponseLockConflict(currentLockID, "Lock Conflict"), nil
 			}
 
 			if moveRes.GetStatus().GetCode() == rpcv1beta1.Code_CODE_ALREADY_EXISTS {
@@ -1018,7 +1075,6 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse,
 	}
 
 	hexEncodedOwnerId := hex.EncodeToString([]byte(statRes.GetInfo().GetOwner().GetOpaqueId() + "@" + statRes.GetInfo().GetOwner().GetIdp()))
-	version := strconv.FormatUint(statRes.GetInfo().GetMtime().GetSeconds(), 10) + "." + strconv.FormatUint(uint64(statRes.GetInfo().GetMtime().GetNanos()), 10)
 
 	// UserId must use only alphanumeric chars (https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/checkfileinfo/checkfileinfo-response#requirements-for-user-identity-properties)
 	// assign userId, userFriendlyName and isAnonymousUser
@@ -1029,30 +1085,44 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse,
 	isAnonymousUser := true
 
 	isPublicShare := false
-	if wopiContext.User != nil {
+	user := ctxpkg.ContextMustGetUser(ctx)
+	if user.String() != "" {
 		// if we have a wopiContext.User
-		isPublicShare = utils.ExistsInOpaque(wopiContext.User.GetOpaque(), "public-share-role")
+		isPublicShare = utils.ExistsInOpaque(user.GetOpaque(), "public-share-role")
 		if !isPublicShare {
-			hexEncodedWopiUserId := hex.EncodeToString([]byte(wopiContext.User.GetId().GetOpaqueId() + "@" + wopiContext.User.GetId().GetIdp()))
+			hexEncodedWopiUserId := hex.EncodeToString([]byte(user.GetId().GetOpaqueId() + "@" + user.GetId().GetIdp()))
 			isAnonymousUser = false
-			userFriendlyName = wopiContext.User.GetDisplayName()
+			userFriendlyName = user.GetDisplayName()
 			userId = hexEncodedWopiUserId
 		}
 	}
 
+	breadcrumbFolderName := path.Dir(statRes.Info.Path)
+	if breadcrumbFolderName == "." || breadcrumbFolderName == "" {
+		breadcrumbFolderName = statRes.GetInfo().GetSpace().GetName()
+	}
+
+	ocisUrl, err := url.Parse(f.cfg.Commons.OcisURL)
+	if err != nil {
+		return nil, err
+	}
+	breadcrumbFolderURL, viewAppUrl, editAppUrl := *ocisUrl, *ocisUrl, *ocisUrl
+	breadcrumbFolderURL.Path = path.Join(breadcrumbFolderURL.Path, "f", storagespace.FormatResourceID(statRes.GetInfo().GetId()))
+	viewAppUrl.Path = path.Join(viewAppUrl.Path, "external"+strings.ToLower(f.cfg.App.Name))
+	editAppUrl.Path = path.Join(editAppUrl.Path, "external"+strings.ToLower(f.cfg.App.Name))
 	// fileinfo map
 	infoMap := map[string]interface{}{
 		fileinfo.KeyOwnerID:           hexEncodedOwnerId,
 		fileinfo.KeySize:              int64(statRes.GetInfo().GetSize()),
-		fileinfo.KeyVersion:           version,
+		fileinfo.KeyVersion:           getVersion(statRes.GetInfo().GetMtime()),
 		fileinfo.KeyBaseFileName:      path.Base(statRes.GetInfo().GetPath()),
 		fileinfo.KeyBreadcrumbDocName: path.Base(statRes.GetInfo().GetPath()),
 		// to get the folder we actually need to do a GetPath() request
-		//BreadcrumbFolderName: path.Dir(statRes.Info.Path),
+		fileinfo.KeyBreadcrumbFolderName: breadcrumbFolderName,
+		fileinfo.KeyBreadcrumbFolderURL:  breadcrumbFolderURL.String(),
 
-		// TODO: these URLs must point to ocis, which is hosting the editor's iframe
-		//fileinfo.KeyHostViewURL: wopiContext.ViewAppUrl,
-		//fileinfo.KeyHostEditURL: wopiContext.EditAppUrl,
+		//fileinfo.KeyHostViewURL: viewAppUrl.String(),
+		//fileinfo.KeyHostEditURL: editAppUrl.String(),
 
 		fileinfo.KeyEnableOwnerTermination:     true, // only for collabora
 		fileinfo.KeySupportsExtendedLockLength: true,
@@ -1066,7 +1136,8 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse,
 		fileinfo.KeyUserFriendlyName: userFriendlyName,
 		fileinfo.KeyUserID:           userId,
 
-		fileinfo.KeyPostMessageOrigin: f.cfg.Commons.OcisURL,
+		fileinfo.KeyPostMessageOrigin:            f.cfg.Commons.OcisURL,
+		fileinfo.KeyLicenseCheckForEditIsEnabled: f.cfg.App.LicenseCheckEnable,
 	}
 
 	switch wopiContext.ViewMode {
@@ -1082,7 +1153,7 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse,
 		infoMap[fileinfo.KeyDisableCopy] = true   // only for collabora
 		infoMap[fileinfo.KeyDisablePrint] = true
 		if !isPublicShare {
-			infoMap[fileinfo.KeyWatermarkText] = f.watermarkText(wopiContext.User) // only for collabora
+			infoMap[fileinfo.KeyWatermarkText] = f.watermarkText(user) // only for collabora
 		}
 	}
 
@@ -1162,7 +1233,7 @@ func (f *FileConnector) generatePrefix() string {
 // contains the resource id of the target file without the path
 // (storage, opaque and space points directly to the file). The path component
 // will be ignored
-func (f *FileConnector) generateWOPISrc(ctx context.Context, wopiContext middleware.WopiContext, logger zerolog.Logger) (*url.URL, error) {
+func (f *FileConnector) generateWOPISrc(wopiContext middleware.WopiContext, logger zerolog.Logger) (*url.URL, error) {
 	// get the WOPI token for the new file
 	accessToken, _, err := middleware.GenerateWopiToken(wopiContext, f.cfg)
 	if err != nil {
@@ -1174,16 +1245,14 @@ func (f *FileConnector) generateWOPISrc(ctx context.Context, wopiContext middlew
 	fileRef := helpers.HashResourceId(wopiContext.FileReference.GetResourceId())
 
 	// generate the URL for the WOPI app to access the new created file
-	wopiSrcURL, err := url.Parse(f.cfg.Wopi.WopiSrc)
+	wopiSrcURL, err := wopisrc.GenerateWopiSrc(fileRef, f.cfg)
 	if err != nil {
 		logger.Error().Err(err).Msg("generateWOPISrc: failed to generate WOPISrc URL for the new file")
 		return nil, err
 	}
-	wopiSrcURL.Path = path.Join("wopi", "files", fileRef)
 	q := wopiSrcURL.Query()
 	q.Add("access_token", accessToken)
 	wopiSrcURL.RawQuery = q.Encode()
-
 	return wopiSrcURL, nil
 }
 
diff --git a/services/collaboration/pkg/connector/fileinfo/microsoft.go b/services/collaboration/pkg/connector/fileinfo/microsoft.go
index 66be0c0f096..6f30d2d8fc2 100644
--- a/services/collaboration/pkg/connector/fileinfo/microsoft.go
+++ b/services/collaboration/pkg/connector/fileinfo/microsoft.go
@@ -60,7 +60,7 @@ type Microsoft struct {
 	// A Boolean value indicating whether the user is an education user or not.
 	IsEduUser bool `json:"IsEduUser,omitempty"`
 	// A Boolean value indicating whether the user is a business user or not.
-	LicenseCheckForEditIsEnabled bool `json:"LicenseCheckForEditIsEnabled,omitempty"`
+	LicenseCheckForEditIsEnabled bool `json:"LicenseCheckForEditIsEnabled"`
 	// A string that is the name of the user, suitable for displaying in UI.
 	UserFriendlyName string `json:"UserFriendlyName,omitempty"`
 	// A string value containing information about the user. This string can be passed from a WOPI client to the host by means of a PutUserInfo operation. If the host has a UserInfo string for the user, they must include it in this property. See the PutUserInfo documentation for more details.
diff --git a/services/collaboration/pkg/connector/httpadapter.go b/services/collaboration/pkg/connector/httpadapter.go
index 20c820dc0d4..b0a42794525 100644
--- a/services/collaboration/pkg/connector/httpadapter.go
+++ b/services/collaboration/pkg/connector/httpadapter.go
@@ -5,7 +5,6 @@ import (
 	"errors"
 	"net/http"
 	"strconv"
-	"strings"
 
 	gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
@@ -15,16 +14,18 @@ import (
 )
 
 const (
-	HeaderWopiLock          string = "X-WOPI-Lock"
-	HeaderWopiOldLock       string = "X-WOPI-OldLock"
-	HeaderWopiST            string = "X-WOPI-SuggestedTarget"
-	HeaderWopiRT            string = "X-WOPI-RelativeTarget"
-	HeaderWopiOverwriteRT   string = "X-WOPI-OverwriteRelativeTarget"
-	HeaderWopiSize          string = "X-WOPI-Size"
-	HeaderWopiValidRT       string = "X-WOPI-ValidRelativeTarget"
-	HeaderWopiRequestedName string = "X-WOPI-RequestedName"
-	HeaderContentLength     string = "Content-Length"
-	HeaderContentType       string = "Content-Type"
+	HeaderWopiLock              string = "X-WOPI-Lock"
+	HeaderWopiOldLock           string = "X-WOPI-OldLock"
+	HeaderWopiLockFailureReason string = "X-WOPI-LockFailureReason"
+	HeaderWopiST                string = "X-WOPI-SuggestedTarget"
+	HeaderWopiRT                string = "X-WOPI-RelativeTarget"
+	HeaderWopiOverwriteRT       string = "X-WOPI-OverwriteRelativeTarget"
+	HeaderWopiSize              string = "X-WOPI-Size"
+	HeaderWopiValidRT           string = "X-WOPI-ValidRelativeTarget"
+	HeaderWopiRequestedName     string = "X-WOPI-RequestedName"
+	HeaderContentLength         string = "Content-Length"
+	HeaderContentType           string = "Content-Type"
+	HeaderWopiVersion           string = "X-WOPI-ItemVersion"
 )
 
 // HttpAdapter will adapt the responses from the connector to HTTP.
@@ -50,10 +51,8 @@ func NewHttpAdapter(gwc gatewayv1beta1.GatewayAPIClient, cfg *config.Config) *Ht
 		),
 	}
 
+	// TODO: check if we can get rid of custom log parsing completely
 	httpAdapter.locks = &locks.NoopLockParser{}
-	if strings.ToLower(cfg.App.Name) == "microsoftofficeonline" {
-		httpAdapter.locks = &locks.LegacyLockParser{}
-	}
 	return httpAdapter
 }
 
diff --git a/services/collaboration/pkg/locks/parser.go b/services/collaboration/pkg/locks/parser.go
index 022e59adff7..e1606e20667 100644
--- a/services/collaboration/pkg/locks/parser.go
+++ b/services/collaboration/pkg/locks/parser.go
@@ -7,7 +7,6 @@ package locks
 
 import (
 	"encoding/json"
-	"strings"
 )
 
 // LockParser is the interface that wraps the ParseLock method
@@ -54,7 +53,7 @@ func (*NoopLockParser) ParseLock(id string) string {
 // If the JSON string is not in the expected format, the original lockID will be returned.
 func (*LegacyLockParser) ParseLock(id string) string {
 	var decodedValues map[string]interface{}
-	err := json.NewDecoder(strings.NewReader(id)).Decode(&decodedValues)
+	err := json.Unmarshal([]byte(id), &decodedValues)
 	if err != nil || len(decodedValues) == 0 {
 		return id
 	}
diff --git a/services/collaboration/pkg/middleware/tracing.go b/services/collaboration/pkg/middleware/tracing.go
index 816f81e129b..4b78352370e 100644
--- a/services/collaboration/pkg/middleware/tracing.go
+++ b/services/collaboration/pkg/middleware/tracing.go
@@ -3,6 +3,7 @@ package middleware
 import (
 	"net/http"
 
+	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
 	"go.opentelemetry.io/otel/attribute"
 	"go.opentelemetry.io/otel/trace"
 )
@@ -27,7 +28,6 @@ func CollaborationTracingMiddleware(next http.Handler) http.Handler {
 		wopiMethod := r.Header.Get("X-WOPI-Override")
 
 		wopiFile := wopiContext.FileReference
-		wopiUser := wopiContext.User.GetId()
 
 		attrs := []attribute.KeyValue{
 			attribute.String("ocis.wopi.sessionid", r.Header.Get("X-WOPI-SessionId")),
@@ -36,9 +36,14 @@ func CollaborationTracingMiddleware(next http.Handler) http.Handler {
 			attribute.String("ocis.wopi.resource.id.opaque", wopiFile.GetResourceId().GetOpaqueId()),
 			attribute.String("ocis.wopi.resource.id.space", wopiFile.GetResourceId().GetSpaceId()),
 			attribute.String("ocis.wopi.resource.path", wopiFile.GetPath()),
-			attribute.String("ocis.wopi.user.idp", wopiUser.GetIdp()),
-			attribute.String("ocis.wopi.user.opaque", wopiUser.GetOpaqueId()),
-			attribute.String("ocis.wopi.user.type", wopiUser.GetType().String()),
+		}
+
+		if wopiUser, ok := ctxpkg.ContextGetUser(r.Context()); ok {
+			attrs = append(attrs, []attribute.KeyValue{
+				attribute.String("ocis.wopi.user.idp", wopiUser.GetId().GetIdp()),
+				attribute.String("ocis.wopi.user.opaque", wopiUser.GetId().GetOpaqueId()),
+				attribute.String("ocis.wopi.user.type", wopiUser.GetId().GetType().String()),
+			}...)
 		}
 		span.SetAttributes(attrs...)
 
diff --git a/services/collaboration/pkg/middleware/wopicontext.go b/services/collaboration/pkg/middleware/wopicontext.go
index 22dcdf0089c..c69308a4c12 100644
--- a/services/collaboration/pkg/middleware/wopicontext.go
+++ b/services/collaboration/pkg/middleware/wopicontext.go
@@ -5,12 +5,12 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
-	"regexp"
+	"strings"
 
 	appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
-	userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
 	providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
+	rjwt "github.com/cs3org/reva/v2/pkg/token/manager/jwt"
 	"github.com/golang-jwt/jwt/v5"
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/helpers"
@@ -29,7 +29,6 @@ type WopiContext struct {
 	AccessToken   string
 	ViewOnlyToken string
 	FileReference *providerv1beta1.Reference
-	User          *userv1beta1.User
 	ViewMode      appproviderv1beta1.ViewMode
 }
 
@@ -45,8 +44,6 @@ type WopiContext struct {
 // * A contextual zerologger containing information about the request
 // and the WopiContext
 func WopiContextAuthMiddleware(cfg *config.Config, next http.Handler) http.Handler {
-	// compile a regexp here to extract the fileid from the URL
-	fileIDregexp := regexp.MustCompile(`^/wopi/files/([0-9a-f]{64})(/.*)?$`)
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		accessToken := r.URL.Query().Get("access_token")
 		if accessToken == "" {
@@ -76,11 +73,25 @@ func WopiContextAuthMiddleware(cfg *config.Config, next http.Handler) http.Handl
 			http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
 			return
 		}
+		tokenManager, err := rjwt.New(map[string]interface{}{
+			"secret":  cfg.TokenManager.JWTSecret,
+			"expires": int64(24 * 60 * 60),
+		})
+		if err != nil {
+			http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+			return
+		}
+		user, _, err := tokenManager.DismantleToken(ctx, wopiContextAccessToken)
+		if err != nil {
+			http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+			return
+		}
 		claims.WopiContext.AccessToken = wopiContextAccessToken
 
 		ctx = context.WithValue(ctx, wopiContextKey, claims.WopiContext)
 		// authentication for the CS3 api
 		ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, claims.WopiContext.AccessToken)
+		ctx = ctxpkg.ContextSetUser(ctx, user)
 
 		// include additional info in the context's logger
 		// we might need to check https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/common-headers
@@ -94,13 +105,13 @@ func WopiContextAuthMiddleware(cfg *config.Config, next http.Handler) http.Handl
 			Str("WopiStamp", r.Header.Get("X-WOPI-TimeStamp")).
 			Str("FileReference", claims.WopiContext.FileReference.String()).
 			Str("ViewMode", claims.WopiContext.ViewMode.String()).
-			Str("Requester", claims.WopiContext.User.GetId().String()).
+			Str("Requester", user.GetId().String()).
 			Logger()
 		ctx = wopiLogger.WithContext(ctx)
 
 		hashedRef := helpers.HashResourceId(claims.WopiContext.FileReference.GetResourceId())
-		matches := fileIDregexp.FindStringSubmatch(r.URL.Path)
-		if len(matches) < 2 || matches[1] != hashedRef {
+		fileID := parseWopiFileID(cfg, r.URL.Path)
+		if fileID != hashedRef {
 			wopiLogger.Error().Msg("file reference in the URL doesn't match the one inside the access token")
 			http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
 			return
@@ -157,3 +168,36 @@ func GenerateWopiToken(wopiContext WopiContext, cfg *config.Config) (string, int
 
 	return accessToken, claims.ExpiresAt.UnixMilli(), err
 }
+
+// parseWopiFileID extracts the file id from a wopi path
+//
+// If the file id is a jwt, it will be decoded and the file id will be extracted from the jwt claims.
+// If the file id is not a jwt, it will be returned as is.
+func parseWopiFileID(cfg *config.Config, path string) string {
+	s := strings.Split(path, "/")
+	if len(s) < 4 || (s[1] != "wopi" && s[2] != "files") {
+		return path
+	}
+	// check if the fileid is a jwt
+	if strings.Contains(s[3], ".") {
+		token, err := jwt.Parse(s[3], func(_ *jwt.Token) (interface{}, error) {
+			return []byte(cfg.Wopi.ProxySecret), nil
+		})
+		if err != nil {
+			return s[3]
+		}
+
+		claims, ok := token.Claims.(jwt.MapClaims)
+		if !ok {
+			return s[3]
+		}
+
+		f, ok := claims["f"].(string)
+		if !ok {
+			return s[3]
+		}
+		return f
+	}
+	// fileid is not a jwt
+	return s[3]
+}
diff --git a/services/collaboration/pkg/service/grpc/v0/service.go b/services/collaboration/pkg/service/grpc/v0/service.go
index 1458a4e27e7..c2cb6308958 100644
--- a/services/collaboration/pkg/service/grpc/v0/service.go
+++ b/services/collaboration/pkg/service/grpc/v0/service.go
@@ -15,6 +15,7 @@ import (
 	providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
 	"github.com/cs3org/reva/v2/pkg/utils"
+	"github.com/owncloud/ocis/v2/services/collaboration/pkg/wopisrc"
 
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
@@ -109,7 +110,6 @@ func (s *Service) OpenInApp(
 		AccessToken:   req.GetAccessToken(), // it will be encrypted
 		ViewOnlyToken: utils.ReadPlainFromOpaque(req.GetOpaque(), "viewOnlyToken"),
 		FileReference: &providerFileRef,
-		User:          user,
 		ViewMode:      req.GetViewMode(),
 	}
 
@@ -201,11 +201,10 @@ func (s *Service) addQueryToURL(baseURL string, req *appproviderv1beta1.OpenInAp
 	// so that all sessions on one file end on the same office server
 	fileRef := helpers.HashResourceId(req.GetResourceInfo().GetId())
 
-	wopiSrcURL, err := url.Parse(s.config.Wopi.WopiSrc)
+	wopiSrcURL, err := wopisrc.GenerateWopiSrc(fileRef, s.config)
 	if err != nil {
 		return "", err
 	}
-	wopiSrcURL.Path = path.Join("wopi", "files", fileRef)
 
 	q := u.Query()
 	q.Add("WOPISrc", wopiSrcURL.String())
@@ -216,6 +215,38 @@ func (s *Service) addQueryToURL(baseURL string, req *appproviderv1beta1.OpenInAp
 
 	lang := utils.ReadPlainFromOpaque(req.GetOpaque(), "lang")
 
+	// @TODO: this is a temporary solution until we figure out how to send these from oc web
+	switch lang {
+	case "bg":
+		lang = "bg-BG"
+	case "cs":
+		lang = "cs-CZ"
+	case "de":
+		lang = "de-DE"
+	case "en":
+		lang = "en-US"
+	case "es":
+		lang = "es-ES"
+	case "fr":
+		lang = "fr-FR"
+	case "gl":
+		lang = "gl-ES"
+	case "it":
+		lang = "it-IT"
+	case "nl":
+		lang = "nl-NL"
+	case "ko":
+		lang = "ko-KR"
+	case "sq":
+		lang = "sq-AL"
+	case "sv":
+		lang = "sv-SE"
+	case "tr":
+		lang = "tr-TR"
+	case "zh":
+		lang = "zh-CN"
+	}
+
 	if lang != "" {
 		switch strings.ToLower(s.config.App.Name) {
 		case "collabora":
diff --git a/services/collaboration/pkg/wopisrc/wopisrc.go b/services/collaboration/pkg/wopisrc/wopisrc.go
new file mode 100644
index 00000000000..677f9f8b0c6
--- /dev/null
+++ b/services/collaboration/pkg/wopisrc/wopisrc.go
@@ -0,0 +1,72 @@
+package wopisrc
+
+import (
+	"errors"
+	"net/url"
+	"path"
+
+	"github.com/golang-jwt/jwt/v4"
+	"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
+)
+
+// GenerateWopiSrc generates a WOPI src URL for the given file reference.
+// If a proxy URL and proxy secret are configured, the URL will be generated
+// as a jwt token that is signed with the proxy secret and contains the file reference
+// and the WOPI src URL.
+// Example:
+// https://cloud.proxy.com/wopi/files/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1IjoiaHR0cHM6Ly9vY2lzLnRlYW0vd29waS9maWxlcy8iLCJmIjoiMTIzNDU2In0.6ol9PQXGKktKfAri8tsJ4X_a9rIeosJ7id6KTQW6Ui0
+//
+// If no proxy URL and proxy secret are configured, the URL will be generated
+// as a direct URL that contains the file reference.
+// Example:
+// https:/ocis.team/wopi/files/12312678470610632091729803710923
+func GenerateWopiSrc(fileRef string, cfg *config.Config) (*url.URL, error) {
+	wopiSrcURL, err := url.Parse(cfg.Wopi.WopiSrc)
+	if err != nil {
+		return nil, err
+	}
+	if wopiSrcURL.Host == "" {
+		return nil, errors.New("invalid WopiSrc URL")
+	}
+
+	if cfg.Wopi.ProxyURL != "" && cfg.Wopi.ProxySecret != "" {
+		return generateProxySrc(fileRef, cfg.Wopi.ProxyURL, cfg.Wopi.ProxySecret, wopiSrcURL)
+	}
+
+	return generateDirectSrc(fileRef, wopiSrcURL)
+}
+
+func generateDirectSrc(fileRef string, wopiSrcURL *url.URL) (*url.URL, error) {
+	wopiSrcURL.Path = path.Join("wopi", "files", fileRef)
+	return wopiSrcURL, nil
+}
+
+func generateProxySrc(fileRef string, proxyUrl string, proxySecret string, wopiSrcURL *url.URL) (*url.URL, error) {
+	proxyURL, err := url.Parse(proxyUrl)
+	if err != nil {
+		return nil, err
+	}
+	if proxyURL.Host == "" {
+		return nil, errors.New("invalid proxy URL")
+	}
+
+	wopiSrcURL.Path = path.Join("wopi", "files")
+
+	type tokenClaims struct {
+		URL    string `json:"u"`
+		FileID string `json:"f"`
+		jwt.RegisteredClaims
+	}
+	token := jwt.NewWithClaims(jwt.SigningMethodHS256, tokenClaims{
+		FileID: fileRef,
+		// the string value from the URL package always ends with a slash
+		// the office365 proxy assumes that we have a trailing slash
+		URL: wopiSrcURL.String() + "/",
+	})
+	tokenString, err := token.SignedString([]byte(proxySecret))
+	if err != nil {
+		return nil, err
+	}
+	proxyURL.Path = path.Join("wopi", "files", tokenString)
+	return proxyURL, nil
+}


Test patch:
diff --git a/services/collaboration/pkg/connector/contentconnector_test.go b/services/collaboration/pkg/connector/contentconnector_test.go
index 82ba9e9c42f..6081ca84ca4 100644
--- a/services/collaboration/pkg/connector/contentconnector_test.go
+++ b/services/collaboration/pkg/connector/contentconnector_test.go
@@ -6,14 +6,15 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"strings"
+	"time"
 
+	"github.com/cs3org/reva/v2/pkg/utils"
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	"github.com/stretchr/testify/mock"
 
 	appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
 	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
-	userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
 	providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	revactx "github.com/cs3org/reva/v2/pkg/ctx"
 	"github.com/cs3org/reva/v2/pkg/rgrpc/status"
@@ -52,7 +53,6 @@ var _ = Describe("ContentConnector", func() {
 				},
 				Path: ".",
 			},
-			User:     &userv1beta1.User{}, // Not used for now
 			ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
 		}
 
@@ -77,15 +77,28 @@ var _ = Describe("ContentConnector", func() {
 	})
 
 	Describe("GetFile", func() {
+		BeforeEach(func() {
+			gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).Return(&providerv1beta1.StatResponse{
+				Status: status.NewOK(context.Background()),
+				Info: &providerv1beta1.ResourceInfo{
+					Id: &providerv1beta1.ResourceId{
+						StorageId: "abc",
+						OpaqueId:  "12345",
+						SpaceId:   "zzz",
+					},
+					Path: ".",
+				},
+			}, nil)
+		})
 		It("No valid context", func() {
-			sb := &strings.Builder{}
+			sb := httptest.NewRecorder()
 			ctx := context.Background()
 			err := cc.GetFile(ctx, sb)
 			Expect(err).To(HaveOccurred())
 		})
 
 		It("Initiate download failed", func() {
-			sb := &strings.Builder{}
+			sb := httptest.NewRecorder()
 			ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
 
 			targetErr := errors.New("Something went wrong")
@@ -98,7 +111,7 @@ var _ = Describe("ContentConnector", func() {
 		})
 
 		It("Initiate download status not ok", func() {
-			sb := &strings.Builder{}
+			sb := httptest.NewRecorder()
 			ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
 
 			gatewayClient.On("InitiateFileDownload", mock.Anything, mock.Anything).Times(1).Return(&gateway.InitiateFileDownloadResponse{
@@ -112,7 +125,7 @@ var _ = Describe("ContentConnector", func() {
 		})
 
 		It("Missing download endpoint", func() {
-			sb := &strings.Builder{}
+			sb := httptest.NewRecorder()
 			ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
 
 			gatewayClient.On("InitiateFileDownload", mock.Anything, mock.Anything).Times(1).Return(&gateway.InitiateFileDownloadResponse{
@@ -126,7 +139,7 @@ var _ = Describe("ContentConnector", func() {
 		})
 
 		It("Download request failed", func() {
-			sb := &strings.Builder{}
+			sb := httptest.NewRecorder()
 			ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
 
 			gatewayClient.On("InitiateFileDownload", mock.Anything, mock.Anything).Times(1).Return(&gateway.InitiateFileDownloadResponse{
@@ -149,7 +162,7 @@ var _ = Describe("ContentConnector", func() {
 		})
 
 		It("Download request success", func() {
-			sb := &strings.Builder{}
+			sb := httptest.NewRecorder()
 			ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
 
 			gatewayClient.On("InitiateFileDownload", mock.Anything, mock.Anything).Times(1).Return(&gateway.InitiateFileDownloadResponse{
@@ -167,11 +180,11 @@ var _ = Describe("ContentConnector", func() {
 			Expect(srvReqHeader.Get("X-Access-Token")).To(Equal(wopiCtx.AccessToken))
 			Expect(srvReqHeader.Get("X-Reva-Transfer")).To(Equal("MyDownloadToken"))
 			Expect(err).To(Succeed())
-			Expect(sb.String()).To(Equal(randomContent))
+			Expect(sb.Body.String()).To(Equal(randomContent))
 		})
 
 		It("ViewOnlyMode Download request success", func() {
-			sb := &strings.Builder{}
+			sb := httptest.NewRecorder()
 
 			wopiCtx = middleware.WopiContext{
 				AccessToken:   "abcdef123456",
@@ -184,7 +197,6 @@ var _ = Describe("ContentConnector", func() {
 					},
 					Path: ".",
 				},
-				User:     &userv1beta1.User{}, // Not used for now
 				ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_VIEW_ONLY,
 			}
 
@@ -208,7 +220,7 @@ var _ = Describe("ContentConnector", func() {
 			Expect(srvReqHeader.Get("X-Access-Token")).To(Equal(wopiCtx.ViewOnlyToken))
 			Expect(srvReqHeader.Get("X-Reva-Transfer")).To(Equal("MyDownloadToken"))
 			Expect(err).To(Succeed())
-			Expect(sb.String()).To(Equal(randomContent))
+			Expect(sb.Body.String()).To(Equal(randomContent))
 		})
 	})
 
@@ -244,7 +256,7 @@ var _ = Describe("ContentConnector", func() {
 			}, nil)
 
 			response, err := cc.PutFile(ctx, reader, reader.Size(), "notARandomLockId")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(500))
 			Expect(response.Headers).To(BeNil())
 		})
@@ -264,9 +276,11 @@ var _ = Describe("ContentConnector", func() {
 			}, nil)
 
 			response, err := cc.PutFile(ctx, reader, reader.Size(), "notARandomLockId")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(409))
+			Expect(response.Headers).To(HaveLen(2))
 			Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("goodAndValidLock"))
+			Expect(response.Headers[connector.HeaderWopiLockFailureReason]).To(Equal("Lock Mismatch"))
 		})
 
 		It("Upload without lockId but on a non empty file", func() {
@@ -282,7 +296,7 @@ var _ = Describe("ContentConnector", func() {
 			}, nil)
 
 			response, err := cc.PutFile(ctx, reader, reader.Size(), "")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(409))
 			Expect(response.Headers[connector.HeaderWopiLock]).To(Equal(""))
 		})
@@ -332,7 +346,7 @@ var _ = Describe("ContentConnector", func() {
 			}, nil)
 
 			response, err := cc.PutFile(ctx, reader, reader.Size(), "goodAndValidLock")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(500))
 			Expect(response.Headers).To(BeNil())
 		})
@@ -348,7 +362,8 @@ var _ = Describe("ContentConnector", func() {
 						LockId: "goodAndValidLock",
 						Type:   providerv1beta1.LockType_LOCK_TYPE_WRITE,
 					},
-					Size: uint64(123456789),
+					Size:  uint64(123456789),
+					Mtime: utils.TimeToTS(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)),
 				},
 			}, nil)
 
@@ -357,9 +372,10 @@ var _ = Describe("ContentConnector", func() {
 			}, nil)
 
 			response, err := cc.PutFile(ctx, reader, reader.Size(), "goodAndValidLock")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(200))
-			Expect(response.Headers).To(BeNil())
+			Expect(response.Headers).To(HaveLen(1))
+			Expect(response.Headers[connector.HeaderWopiVersion]).To(Equal("v16094592000"))
 		})
 
 		It("Missing upload endpoint", func() {
@@ -382,7 +398,7 @@ var _ = Describe("ContentConnector", func() {
 			}, nil)
 
 			response, err := cc.PutFile(ctx, reader, reader.Size(), "goodAndValidLock")
-			Expect(err).To(BeNil())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(500))
 			Expect(response.Headers).To(BeNil())
 		})
@@ -414,7 +430,7 @@ var _ = Describe("ContentConnector", func() {
 
 			response, err := cc.PutFile(ctx, reader, reader.Size(), "goodAndValidLock")
 			Expect(srvReqHeader.Get("X-Access-Token")).To(Equal(wopiCtx.AccessToken))
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(500))
 			Expect(response.Headers).To(BeNil())
 		})
@@ -434,6 +450,23 @@ var _ = Describe("ContentConnector", func() {
 				},
 			}, nil)
 
+			gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).Times(1).Return(&providerv1beta1.StatResponse{
+				Status: status.NewOK(ctx),
+				Info: &providerv1beta1.ResourceInfo{
+					Lock: &providerv1beta1.Lock{
+						LockId: "goodAndValidLock",
+						Type:   providerv1beta1.LockType_LOCK_TYPE_WRITE,
+					},
+					Size: uint64(123456789),
+					Id: &providerv1beta1.ResourceId{
+						StorageId: "storageID",
+						OpaqueId:  "opaqueID",
+						SpaceId:   "spaceID",
+					},
+					Mtime: utils.TimeToTS(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)),
+				},
+			}, nil)
+
 			gatewayClient.On("InitiateFileUpload", mock.Anything, mock.Anything).Times(1).Return(&gateway.InitiateFileUploadResponse{
 				Status: status.NewOK(ctx),
 				Protocols: []*gateway.FileUploadProtocol{
@@ -446,9 +479,10 @@ var _ = Describe("ContentConnector", func() {
 
 			response, err := cc.PutFile(ctx, reader, reader.Size(), "goodAndValidLock")
 			Expect(srvReqHeader.Get("X-Access-Token")).To(Equal(wopiCtx.AccessToken))
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(200))
-			Expect(response.Headers).To(BeNil())
+			Expect(response.Headers).To(HaveLen(1))
+			Expect(response.Headers[connector.HeaderWopiVersion]).To(Equal("v16094592000"))
 		})
 	})
 })
diff --git a/services/collaboration/pkg/connector/fileconnector_test.go b/services/collaboration/pkg/connector/fileconnector_test.go
index eb1ec5462c2..8dfa4961f57 100644
--- a/services/collaboration/pkg/connector/fileconnector_test.go
+++ b/services/collaboration/pkg/connector/fileconnector_test.go
@@ -12,6 +12,7 @@ import (
 	userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
 	providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
+	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
 	"github.com/cs3org/reva/v2/pkg/rgrpc/status"
 	cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks"
 	. "github.com/onsi/ginkgo/v2"
@@ -63,25 +64,6 @@ var _ = Describe("FileConnector", func() {
 				},
 				Path: ".",
 			},
-			User: &userv1beta1.User{
-				Id: &userv1beta1.UserId{
-					Idp:      "inmemory",
-					OpaqueId: "opaqueId",
-					Type:     userv1beta1.UserType_USER_TYPE_PRIMARY,
-				},
-				Username:    "Shaft",
-				DisplayName: "Pet Shaft",
-				Mail:        "shaft@example.com",
-				// Opaque is here for reference, not used by default but might be needed for some tests
-				//Opaque: &typesv1beta1.Opaque{
-				//	Map: map[string]*typesv1beta1.OpaqueEntry{
-				//		"public-share-role": &typesv1beta1.OpaqueEntry{
-				//			Decoder: "plain",
-				//			Value:   []byte("viewer"),
-				//		},
-				//	},
-				//},
-			},
 			ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
 		}
 	})
@@ -116,7 +98,7 @@ var _ = Describe("FileConnector", func() {
 			}, nil)
 
 			response, err := fc.GetLock(ctx)
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(404))
 			Expect(response.Headers).To(BeNil())
 		})
@@ -134,7 +116,7 @@ var _ = Describe("FileConnector", func() {
 			}, nil)
 
 			response, err := fc.GetLock(ctx)
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(200))
 			Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
 		})
@@ -153,7 +135,7 @@ var _ = Describe("FileConnector", func() {
 				ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
 
 				response, err := fc.Lock(ctx, "", "")
-				Expect(err).To(Succeed())
+				Expect(err).ToNot(HaveOccurred())
 				Expect(response.Status).To(Equal(400))
 				Expect(response.Headers).To(BeNil())
 			})
@@ -179,10 +161,25 @@ var _ = Describe("FileConnector", func() {
 					Status: status.NewOK(ctx),
 				}, nil)
 
+				gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+					Return(
+						&providerv1beta1.StatResponse{
+							Status: status.NewOK(ctx),
+							Info: &providerv1beta1.ResourceInfo{
+								Mtime: &typesv1beta1.Timestamp{
+									Seconds: 12345,
+									Nanos:   6789,
+								},
+							},
+						},
+						nil,
+					)
+
 				response, err := fc.Lock(ctx, "abcdef123", "")
-				Expect(err).To(Succeed())
+				Expect(err).ToNot(HaveOccurred())
 				Expect(response.Status).To(Equal(200))
-				Expect(response.Headers).To(BeNil())
+				Expect(response.Headers).To(HaveLen(1))
+				Expect(response.Headers[connector.HeaderWopiVersion]).To(Equal("v123456789"))
 			})
 
 			It("Set lock mismatches error getting lock", func() {
@@ -197,6 +194,9 @@ var _ = Describe("FileConnector", func() {
 					Status: status.NewInternal(ctx, "lock mismatch"),
 				}, targetErr)
 
+				gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+					Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
+
 				response, err := fc.Lock(ctx, "abcdef123", "")
 				Expect(err).To(HaveOccurred())
 				Expect(err).To(Equal(targetErr))
@@ -218,10 +218,15 @@ var _ = Describe("FileConnector", func() {
 					},
 				}, nil)
 
+				gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+					Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
+
 				response, err := fc.Lock(ctx, "abcdef123", "")
-				Expect(err).To(Succeed())
+				Expect(err).ToNot(HaveOccurred())
 				Expect(response.Status).To(Equal(409))
+				Expect(response.Headers).To(HaveLen(2))
 				Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
+				Expect(response.Headers[connector.HeaderWopiLockFailureReason]).To(Equal("Conflicting LockID"))
 			})
 
 			It("Set lock mismatches but get lock matches", func() {
@@ -239,10 +244,19 @@ var _ = Describe("FileConnector", func() {
 					},
 				}, nil)
 
+				gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+					Return(&providerv1beta1.StatResponse{
+						Status: status.NewOK(ctx),
+						Info: &providerv1beta1.ResourceInfo{
+							Mtime: &typesv1beta1.Timestamp{Seconds: uint64(12345), Nanos: uint32(6789)},
+						},
+					}, nil)
+
 				response, err := fc.Lock(ctx, "abcdef123", "")
-				Expect(err).To(Succeed())
+				Expect(err).ToNot(HaveOccurred())
 				Expect(response.Status).To(Equal(200))
-				Expect(response.Headers).To(BeNil())
+				Expect(response.Headers).To(HaveLen(1))
+				Expect(response.Headers[connector.HeaderWopiVersion]).To(Equal("v123456789"))
 			})
 
 			It("Set lock mismatches but get lock doesn't return lockId", func() {
@@ -256,8 +270,11 @@ var _ = Describe("FileConnector", func() {
 					Status: status.NewOK(ctx),
 				}, nil)
 
+				gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+					Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
+
 				response, err := fc.Lock(ctx, "abcdef123", "")
-				Expect(err).To(Succeed())
+				Expect(err).ToNot(HaveOccurred())
 				Expect(response.Status).To(Equal(500))
 				Expect(response.Headers).To(BeNil())
 			})
@@ -269,8 +286,11 @@ var _ = Describe("FileConnector", func() {
 					Status: status.NewNotFound(ctx, "file not found"),
 				}, nil)
 
+				gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+					Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
+
 				response, err := fc.Lock(ctx, "abcdef123", "")
-				Expect(err).To(Succeed())
+				Expect(err).ToNot(HaveOccurred())
 				Expect(response.Status).To(Equal(404))
 				Expect(response.Headers).To(BeNil())
 			})
@@ -282,8 +302,11 @@ var _ = Describe("FileConnector", func() {
 					Status: status.NewInsufficientStorage(ctx, nil, "file too big"),
 				}, nil)
 
+				gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+					Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
+
 				response, err := fc.Lock(ctx, "abcdef123", "")
-				Expect(err).To(Succeed())
+				Expect(err).ToNot(HaveOccurred())
 				Expect(response.Status).To(Equal(500))
 				Expect(response.Headers).To(BeNil())
 			})
@@ -301,7 +324,7 @@ var _ = Describe("FileConnector", func() {
 				ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
 
 				response, err := fc.Lock(ctx, "", "oldLock")
-				Expect(err).To(Succeed())
+				Expect(err).ToNot(HaveOccurred())
 				Expect(response.Status).To(Equal(400))
 				Expect(response.Headers).To(BeNil())
 			})
@@ -327,10 +350,19 @@ var _ = Describe("FileConnector", func() {
 					Status: status.NewOK(ctx),
 				}, nil)
 
+				gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+					Return(&providerv1beta1.StatResponse{
+						Status: status.NewOK(ctx),
+						Info: &providerv1beta1.ResourceInfo{
+							Mtime: &typesv1beta1.Timestamp{Seconds: uint64(12345), Nanos: uint32(6789)},
+						},
+					}, nil)
+
 				response, err := fc.Lock(ctx, "abcdef123", "oldLock")
-				Expect(err).To(Succeed())
+				Expect(err).ToNot(HaveOccurred())
 				Expect(response.Status).To(Equal(200))
-				Expect(response.Headers).To(BeNil())
+				Expect(response.Headers).To(HaveLen(1))
+				Expect(response.Headers[connector.HeaderWopiVersion]).To(Equal("v123456789"))
 			})
 
 			It("Refresh lock mismatches error getting lock", func() {
@@ -345,6 +377,9 @@ var _ = Describe("FileConnector", func() {
 					Status: status.NewInternal(ctx, "lock mismatch"),
 				}, targetErr)
 
+				gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+					Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
+
 				response, err := fc.Lock(ctx, "abcdef123", "112233")
 				Expect(err).To(HaveOccurred())
 				Expect(err).To(Equal(targetErr))
@@ -366,8 +401,11 @@ var _ = Describe("FileConnector", func() {
 					},
 				}, nil)
 
+				gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+					Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
+
 				response, err := fc.Lock(ctx, "abcdef123", "112233")
-				Expect(err).To(Succeed())
+				Expect(err).ToNot(HaveOccurred())
 				Expect(response.Status).To(Equal(409))
 				Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
 			})
@@ -387,10 +425,19 @@ var _ = Describe("FileConnector", func() {
 					},
 				}, nil)
 
+				gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+					Return(&providerv1beta1.StatResponse{
+						Status: status.NewOK(ctx),
+						Info: &providerv1beta1.ResourceInfo{
+							Mtime: &typesv1beta1.Timestamp{Seconds: uint64(12345), Nanos: uint32(6789)},
+						},
+					}, nil)
+
 				response, err := fc.Lock(ctx, "abcdef123", "112233")
-				Expect(err).To(Succeed())
+				Expect(err).ToNot(HaveOccurred())
 				Expect(response.Status).To(Equal(200))
-				Expect(response.Headers).To(BeNil())
+				Expect(response.Headers).To(HaveLen(1))
+				Expect(response.Headers[connector.HeaderWopiVersion]).To(Equal("v123456789"))
 			})
 
 			It("Refresh lock mismatches but get lock doesn't return lockId", func() {
@@ -404,8 +451,11 @@ var _ = Describe("FileConnector", func() {
 					Status: status.NewOK(ctx),
 				}, nil)
 
+				gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+					Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
+
 				response, err := fc.Lock(ctx, "abcdef123", "112233")
-				Expect(err).To(Succeed())
+				Expect(err).ToNot(HaveOccurred())
 				Expect(response.Status).To(Equal(500))
 				Expect(response.Headers).To(BeNil())
 			})
@@ -417,8 +467,11 @@ var _ = Describe("FileConnector", func() {
 					Status: status.NewNotFound(ctx, "file not found"),
 				}, nil)
 
+				gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+					Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
+
 				response, err := fc.Lock(ctx, "abcdef123", "112233")
-				Expect(err).To(Succeed())
+				Expect(err).ToNot(HaveOccurred())
 				Expect(response.Status).To(Equal(404))
 				Expect(response.Headers).To(BeNil())
 			})
@@ -430,8 +483,11 @@ var _ = Describe("FileConnector", func() {
 					Status: status.NewInsufficientStorage(ctx, nil, "file too big"),
 				}, nil)
 
+				gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+					Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
+
 				response, err := fc.Lock(ctx, "abcdef123", "112233")
-				Expect(err).To(Succeed())
+				Expect(err).ToNot(HaveOccurred())
 				Expect(response.Status).To(Equal(500))
 				Expect(response.Headers).To(BeNil())
 			})
@@ -441,7 +497,8 @@ var _ = Describe("FileConnector", func() {
 	Describe("RefreshLock", func() {
 		It("No valid context", func() {
 			ctx := context.Background()
-			response, err := fc.RefreshLock(ctx, "newLock")
+
+			response, err := fc.RefreshLock(ctx, "")
 			Expect(err).To(HaveOccurred())
 			Expect(response).To(BeNil())
 		})
@@ -450,7 +507,7 @@ var _ = Describe("FileConnector", func() {
 			ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
 
 			response, err := fc.RefreshLock(ctx, "")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(400))
 			Expect(response.Headers).To(BeNil())
 		})
@@ -476,10 +533,19 @@ var _ = Describe("FileConnector", func() {
 				Status: status.NewOK(ctx),
 			}, nil)
 
+			gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+				Return(&providerv1beta1.StatResponse{
+					Status: status.NewOK(ctx),
+					Info: &providerv1beta1.ResourceInfo{
+						Mtime: &typesv1beta1.Timestamp{Seconds: uint64(12345), Nanos: uint32(6789)},
+					},
+				}, nil)
+
 			response, err := fc.RefreshLock(ctx, "abcdef123")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(200))
-			Expect(response.Headers).To(BeNil())
+			Expect(response.Headers).To(HaveLen(1))
+			Expect(response.Headers[connector.HeaderWopiVersion]).To(Equal("v123456789"))
 		})
 
 		It("Refresh lock file not found", func() {
@@ -489,8 +555,11 @@ var _ = Describe("FileConnector", func() {
 				Status: status.NewNotFound(ctx, "file not found"),
 			}, nil)
 
+			gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+				Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
+
 			response, err := fc.RefreshLock(ctx, "abcdef123")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(404))
 			Expect(response.Headers).To(BeNil())
 		})
@@ -507,6 +576,9 @@ var _ = Describe("FileConnector", func() {
 				Status: status.NewConflict(ctx, nil, "lock mismatch"),
 			}, targetErr)
 
+			gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+				Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
+
 			response, err := fc.RefreshLock(ctx, "abcdef123")
 			Expect(err).To(HaveOccurred())
 			Expect(err).To(Equal(targetErr))
@@ -524,8 +596,11 @@ var _ = Describe("FileConnector", func() {
 				Status: status.NewInternal(ctx, "lock mismatch"),
 			}, nil)
 
+			gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+				Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
+
 			response, err := fc.RefreshLock(ctx, "abcdef123")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(500))
 			Expect(response.Headers).To(BeNil())
 		})
@@ -541,8 +616,11 @@ var _ = Describe("FileConnector", func() {
 				Status: status.NewOK(ctx),
 			}, nil)
 
+			gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+				Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
+
 			response, err := fc.RefreshLock(ctx, "abcdef123")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(409))
 			Expect(response.Headers[connector.HeaderWopiLock]).To(Equal(""))
 		})
@@ -562,8 +640,11 @@ var _ = Describe("FileConnector", func() {
 				},
 			}, nil)
 
+			gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+				Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
+
 			response, err := fc.RefreshLock(ctx, "abcdef123")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(409))
 			Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
 		})
@@ -575,8 +656,11 @@ var _ = Describe("FileConnector", func() {
 				Status: status.NewInsufficientStorage(ctx, nil, "file too big"),
 			}, nil)
 
+			gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+				Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
+
 			response, err := fc.RefreshLock(ctx, "abcdef123")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(500))
 			Expect(response.Headers).To(BeNil())
 		})
@@ -585,7 +669,8 @@ var _ = Describe("FileConnector", func() {
 	Describe("Unlock", func() {
 		It("No valid context", func() {
 			ctx := context.Background()
-			response, err := fc.UnLock(ctx, "newLock")
+
+			response, err := fc.UnLock(ctx, "")
 			Expect(err).To(HaveOccurred())
 			Expect(response).To(BeNil())
 		})
@@ -594,7 +679,7 @@ var _ = Describe("FileConnector", func() {
 			ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
 
 			response, err := fc.UnLock(ctx, "")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(400))
 			Expect(response.Headers).To(BeNil())
 		})
@@ -620,10 +705,19 @@ var _ = Describe("FileConnector", func() {
 				Status: status.NewOK(ctx),
 			}, nil)
 
+			gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+				Return(&providerv1beta1.StatResponse{
+					Status: status.NewOK(ctx),
+					Info: &providerv1beta1.ResourceInfo{
+						Mtime: &typesv1beta1.Timestamp{Seconds: uint64(12345), Nanos: uint32(6789)},
+					},
+				}, nil)
+
 			response, err := fc.UnLock(ctx, "abcdef123")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(200))
-			Expect(response.Headers).To(BeNil())
+			Expect(response.Headers).To(HaveLen(1))
+			Expect(response.Headers[connector.HeaderWopiVersion]).To(Equal("v123456789"))
 		})
 
 		It("Unlock file isn't locked", func() {
@@ -633,8 +727,16 @@ var _ = Describe("FileConnector", func() {
 				Status: status.NewConflict(ctx, nil, "lock mismatch"),
 			}, nil)
 
+			gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+				Return(&providerv1beta1.StatResponse{
+					Status: status.NewOK(ctx),
+					Info: &providerv1beta1.ResourceInfo{
+						Mtime: &typesv1beta1.Timestamp{Seconds: uint64(12345), Nanos: uint32(6789)},
+					},
+				}, nil)
+
 			response, err := fc.UnLock(ctx, "abcdef123")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(409))
 			Expect(response.Headers[connector.HeaderWopiLock]).To(Equal(""))
 		})
@@ -651,6 +753,9 @@ var _ = Describe("FileConnector", func() {
 				Status: status.NewInternal(ctx, "something failed"),
 			}, targetErr)
 
+			gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+				Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
+
 			response, err := fc.UnLock(ctx, "abcdef123")
 			Expect(err).To(HaveOccurred())
 			Expect(err).To(Equal(targetErr))
@@ -668,8 +773,11 @@ var _ = Describe("FileConnector", func() {
 				Status: status.NewInternal(ctx, "something failed"),
 			}, nil)
 
+			gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+				Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
+
 			response, err := fc.UnLock(ctx, "abcdef123")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(500))
 			Expect(response.Headers).To(BeNil())
 		})
@@ -685,8 +793,11 @@ var _ = Describe("FileConnector", func() {
 				Status: status.NewOK(ctx),
 			}, nil)
 
+			gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+				Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
+
 			response, err := fc.UnLock(ctx, "abcdef123")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(409))
 			Expect(response.Headers[connector.HeaderWopiLock]).To(Equal(""))
 		})
@@ -706,8 +817,11 @@ var _ = Describe("FileConnector", func() {
 				},
 			}, nil)
 
+			gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+				Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
+
 			response, err := fc.UnLock(ctx, "abcdef123")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(409))
 			Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
 		})
@@ -719,8 +833,11 @@ var _ = Describe("FileConnector", func() {
 				Status: status.NewInsufficientStorage(ctx, nil, "file too big"),
 			}, nil)
 
+			gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
+				Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
+
 			response, err := fc.UnLock(ctx, "abcdef123")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(500))
 			Expect(response.Headers).To(BeNil())
 		})
@@ -759,7 +876,7 @@ var _ = Describe("FileConnector", func() {
 
 			stream := strings.NewReader("This is the content of a file")
 			response, err := fc.PutRelativeFileSuggested(ctx, ccs, stream, int64(stream.Len()), "newFile.txt")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(500))
 			Expect(response.Headers).To(BeNil())
 			Expect(response.Body).To(BeNil())
@@ -813,7 +930,7 @@ var _ = Describe("FileConnector", func() {
 			}, nil)
 
 			response, err := fc.PutRelativeFileSuggested(ctx, ccs, stream, int64(stream.Len()), "newDocument.docx")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(200))
 			Expect(response.Headers).To(BeNil())
 			rBody := response.Body.(map[string]interface{})
@@ -869,7 +986,7 @@ var _ = Describe("FileConnector", func() {
 			}, nil)
 
 			response, err := fc.PutRelativeFileSuggested(ctx, ccs, stream, int64(stream.Len()), ".pdf")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(200))
 			Expect(response.Headers).To(BeNil())
 			rBody := response.Body.(map[string]interface{})
@@ -938,7 +1055,7 @@ var _ = Describe("FileConnector", func() {
 			}, nil)
 
 			response, err := fc.PutRelativeFileSuggested(ctx, ccs, stream, int64(stream.Len()), ".pdf")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(200))
 			Expect(response.Headers).To(BeNil())
 			rBody := response.Body.(map[string]interface{})
@@ -972,7 +1089,7 @@ var _ = Describe("FileConnector", func() {
 			ccs.On("PutFile", mock.Anything, stream, int64(stream.Len()), "").Times(1).Return(connector.NewResponse(500), nil)
 
 			response, err := fc.PutRelativeFileSuggested(ctx, ccs, stream, int64(stream.Len()), ".pdf")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(500))
 			Expect(response.Headers).To(BeNil())
 			Expect(response.Body).To(BeNil())
@@ -1012,7 +1129,7 @@ var _ = Describe("FileConnector", func() {
 
 			stream := strings.NewReader("This is the content of a file")
 			response, err := fc.PutRelativeFileRelative(ctx, ccs, stream, int64(stream.Len()), "newFile.txt")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(500))
 			Expect(response.Headers).To(BeNil())
 			Expect(response.Body).To(BeNil())
@@ -1065,7 +1182,7 @@ var _ = Describe("FileConnector", func() {
 			}, nil)
 
 			response, err := fc.PutRelativeFileRelative(ctx, ccs, stream, int64(stream.Len()), "newDocument.docx")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(200))
 			Expect(response.Headers).To(BeNil())
 			rBody := response.Body.(map[string]interface{})
@@ -1124,7 +1241,7 @@ var _ = Describe("FileConnector", func() {
 			}, nil)
 
 			response, err := fc.PutRelativeFileRelative(ctx, ccs, stream, int64(stream.Len()), "convFile.pdf")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(409))
 			Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
 			Expect(response.Headers[connector.HeaderWopiValidRT]).To(MatchRegexp(`[a-zA-Z0-9_-] convFile\.pdf`))
@@ -1159,7 +1276,7 @@ var _ = Describe("FileConnector", func() {
 			ccs.On("PutFile", mock.Anything, stream, int64(stream.Len()), "").Times(1).Return(connector.NewResponse(500), nil)
 
 			response, err := fc.PutRelativeFileRelative(ctx, ccs, stream, int64(stream.Len()), "convFile.pdf")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(500))
 			Expect(response.Headers).To(BeNil())
 			Expect(response.Body).To(BeNil())
@@ -1223,7 +1340,7 @@ var _ = Describe("FileConnector", func() {
 			}, targetErr)
 
 			response, err := fc.DeleteFile(ctx, "newlock")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(404))
 			Expect(response.Headers).To(BeNil())
 		})
@@ -1240,7 +1357,7 @@ var _ = Describe("FileConnector", func() {
 			}, nil)
 
 			response, err := fc.DeleteFile(ctx, "newlock")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(500))
 			Expect(response.Headers).To(BeNil())
 		})
@@ -1261,7 +1378,7 @@ var _ = Describe("FileConnector", func() {
 			}, nil)
 
 			response, err := fc.DeleteFile(ctx, "newlock")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(409))
 			Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
 		})
@@ -1278,7 +1395,7 @@ var _ = Describe("FileConnector", func() {
 			}, nil)
 
 			response, err := fc.DeleteFile(ctx, "newlock")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(500))
 			Expect(response.Headers).To(BeNil())
 		})
@@ -1291,7 +1408,7 @@ var _ = Describe("FileConnector", func() {
 			}, nil)
 
 			response, err := fc.DeleteFile(ctx, "newlock")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(200))
 			Expect(response.Headers).To(BeNil())
 		})
@@ -1327,7 +1444,7 @@ var _ = Describe("FileConnector", func() {
 			}, nil)
 
 			response, err := fc.RenameFile(ctx, "lockid", "newFile.doc")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(500))
 			Expect(response.Headers).To(BeNil())
 			Expect(response.Body).To(BeNil())
@@ -1375,7 +1492,7 @@ var _ = Describe("FileConnector", func() {
 			}, nil)
 
 			response, err := fc.RenameFile(ctx, "lockid", "newFile.doc")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(500))
 			Expect(response.Headers).To(BeNil())
 			Expect(response.Body).To(BeNil())
@@ -1399,7 +1516,7 @@ var _ = Describe("FileConnector", func() {
 			}, nil)
 
 			response, err := fc.RenameFile(ctx, "lockid", "newFile.doc")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(409))
 			Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
 			Expect(response.Body).To(BeNil())
@@ -1442,7 +1559,7 @@ var _ = Describe("FileConnector", func() {
 			}, nil).Once()
 
 			response, err := fc.RenameFile(ctx, "zzz999", "newFile.doc")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(200))
 			Expect(response.Headers).To(BeNil())
 			rBody := response.Body.(map[string]interface{})
@@ -1474,7 +1591,7 @@ var _ = Describe("FileConnector", func() {
 			}, nil).Once()
 
 			response, err := fc.RenameFile(ctx, "zzz999", "newFile.doc")
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(200))
 			Expect(response.Headers).To(BeNil())
 			rBody := response.Body.(map[string]interface{})
@@ -1512,13 +1629,21 @@ var _ = Describe("FileConnector", func() {
 			}, nil)
 
 			response, err := fc.CheckFileInfo(ctx)
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(500))
 			Expect(response.Body).To(BeNil())
 		})
 
 		It("Stat success", func() {
 			ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
+			u := &userv1beta1.User{
+				Id: &userv1beta1.UserId{
+					Idp:      "customIdp",
+					OpaqueId: "admin",
+				},
+				DisplayName: "Pet Shaft",
+			}
+			ctx = ctxpkg.ContextSetUser(ctx, u)
 
 			gatewayClient.On("Stat", mock.Anything, mock.Anything).Times(1).Return(&providerv1beta1.StatResponse{
 				Status: status.NewOK(ctx),
@@ -1533,17 +1658,25 @@ var _ = Describe("FileConnector", func() {
 						Seconds: uint64(16273849),
 					},
 					Path: "/path/to/test.txt",
-					// Other properties aren't used for now.
+					Id: &providerv1beta1.ResourceId{
+						StorageId: "storageid",
+						OpaqueId:  "opaqueid",
+						SpaceId:   "spaceid",
+					},
 				},
 			}, nil)
 
 			expectedFileInfo := &fileinfo.Microsoft{
-				OwnerID:                    "61616262636340637573746f6d496470", // hex of aabbcc@customIdp
-				Size:                       int64(998877),
-				Version:                    "16273849.0",
-				BaseFileName:               "test.txt",
-				BreadcrumbDocName:          "test.txt",
-				UserCanNotWriteRelative:    false,
+				OwnerID:                 "61616262636340637573746f6d496470", // hex of aabbcc@customIdp
+				Size:                    int64(998877),
+				Version:                 "v162738490",
+				BaseFileName:            "test.txt",
+				BreadcrumbDocName:       "test.txt",
+				BreadcrumbFolderName:    "/path/to",
+				BreadcrumbFolderURL:     "https://ocis.example.prv/f/storageid$spaceid%21opaqueid",
+				UserCanNotWriteRelative: false,
+				//HostViewURL:                "http://test.ex.prv/view",
+				//HostEditURL:                "http://test.ex.prv/edit",
 				SupportsExtendedLockLength: true,
 				SupportsGetLock:            true,
 				SupportsLocks:              true,
@@ -1552,19 +1685,20 @@ var _ = Describe("FileConnector", func() {
 				SupportsRename:             true,
 				UserCanWrite:               true,
 				UserCanRename:              true,
-				UserID:                     "6f7061717565496440696e6d656d6f7279", // hex of opaqueId@inmemory
+				UserID:                     "61646d696e40637573746f6d496470", // hex of admin@customIdp
 				UserFriendlyName:           "Pet Shaft",
 			}
 
 			response, err := fc.CheckFileInfo(ctx)
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(200))
 			Expect(response.Body.(*fileinfo.Microsoft)).To(Equal(expectedFileInfo))
 		})
 
 		It("Stat success guests", func() {
 			// add user's opaque to include public-share-role
-			wopiCtx.User.Opaque = &typesv1beta1.Opaque{
+			u := &userv1beta1.User{}
+			u.Opaque = &typesv1beta1.Opaque{
 				Map: map[string]*typesv1beta1.OpaqueEntry{
 					"public-share-role": &typesv1beta1.OpaqueEntry{
 						Decoder: "plain",
@@ -1576,6 +1710,7 @@ var _ = Describe("FileConnector", func() {
 			wopiCtx.ViewMode = appproviderv1beta1.ViewMode_VIEW_MODE_VIEW_ONLY
 
 			ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
+			ctx = ctxpkg.ContextSetUser(ctx, u)
 
 			gatewayClient.On("Stat", mock.Anything, mock.Anything).Times(1).Return(&providerv1beta1.StatResponse{
 				Status: status.NewOK(ctx),
@@ -1590,6 +1725,11 @@ var _ = Describe("FileConnector", func() {
 						Seconds: uint64(16273849),
 					},
 					Path: "/path/to/test.txt",
+					Id: &providerv1beta1.ResourceId{
+						StorageId: "storageid",
+						OpaqueId:  "opaqueid",
+						SpaceId:   "spaceid",
+					},
 					// Other properties aren't used for now.
 				},
 			}, nil)
@@ -1625,7 +1765,7 @@ var _ = Describe("FileConnector", func() {
 			response.Body.(*fileinfo.Collabora).UserID = "guest-zzz000"
 			response.Body.(*fileinfo.Collabora).UserFriendlyName = "guest zzz000"
 
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(200))
 			Expect(response.Body.(*fileinfo.Collabora)).To(Equal(expectedFileInfo))
 		})
@@ -1635,6 +1775,16 @@ var _ = Describe("FileConnector", func() {
 			wopiCtx.ViewMode = appproviderv1beta1.ViewMode_VIEW_MODE_VIEW_ONLY
 
 			ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
+			u := &userv1beta1.User{
+				Id: &userv1beta1.UserId{
+					Idp:      "example.com",
+					OpaqueId: "aabbcc",
+					Type:     userv1beta1.UserType_USER_TYPE_PRIMARY,
+				},
+				DisplayName: "Pet Shaft",
+				Mail:        "shaft@example.com",
+			}
+			ctx = ctxpkg.ContextSetUser(ctx, u)
 
 			gatewayClient.On("Stat", mock.Anything, mock.Anything).Times(1).Return(&providerv1beta1.StatResponse{
 				Status: status.NewOK(ctx),
@@ -1649,7 +1799,11 @@ var _ = Describe("FileConnector", func() {
 						Seconds: uint64(16273849),
 					},
 					Path: "/path/to/test.txt",
-					// Other properties aren't used for now.
+					Id: &providerv1beta1.ResourceId{
+						StorageId: "storageid",
+						OpaqueId:  "opaqueid",
+						SpaceId:   "spaceid",
+					},
 				},
 			}, nil)
 
@@ -1664,7 +1818,7 @@ var _ = Describe("FileConnector", func() {
 				DisableExport:           true,
 				DisableCopy:             true,
 				DisablePrint:            true,
-				UserID:                  hex.EncodeToString([]byte("opaqueId@inmemory")),
+				UserID:                  hex.EncodeToString([]byte("aabbcc@example.com")),
 				UserFriendlyName:        "Pet Shaft",
 				EnableOwnerTermination:  true,
 				WatermarkText:           "Pet Shaft shaft@example.com",
@@ -1677,7 +1831,7 @@ var _ = Describe("FileConnector", func() {
 
 			response, err := fc.CheckFileInfo(ctx)
 
-			Expect(err).To(Succeed())
+			Expect(err).ToNot(HaveOccurred())
 			Expect(response.Status).To(Equal(200))
 			Expect(response.Body.(*fileinfo.Collabora)).To(Equal(expectedFileInfo))
 		})
diff --git a/services/collaboration/pkg/connector/httpadapter_test.go b/services/collaboration/pkg/connector/httpadapter_test.go
index 64de4409b14..3d3d97c7add 100644
--- a/services/collaboration/pkg/connector/httpadapter_test.go
+++ b/services/collaboration/pkg/connector/httpadapter_test.go
@@ -7,6 +7,7 @@ import (
 	"net/http/httptest"
 	"strings"
 
+	typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	"github.com/owncloud/ocis/v2/services/collaboration/mocks"
@@ -133,12 +134,13 @@ var _ = Describe("HttpAdapter", func() {
 
 				w := httptest.NewRecorder()
 
-				fc.On("Lock", mock.Anything, "abc123", "").Times(1).Return(connector.NewResponseWithLock(409, "zzz111"), nil)
+				fc.On("Lock", mock.Anything, "abc123", "").Times(1).Return(connector.NewResponseLockConflict("zzz111", "Lock Conflict"), nil)
 
 				httpAdapter.Lock(w, req)
 				resp := w.Result()
 				Expect(resp.StatusCode).To(Equal(409))
 				Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111"))
+				Expect(resp.Header.Get(connector.HeaderWopiLockFailureReason)).To(Equal("Lock Conflict"))
 			})
 
 			It("Success", func() {
@@ -148,11 +150,18 @@ var _ = Describe("HttpAdapter", func() {
 
 				w := httptest.NewRecorder()
 
-				fc.On("Lock", mock.Anything, "abc123", "").Times(1).Return(connector.NewResponse(200), nil)
+				fc.On("Lock", mock.Anything, "abc123", "").Times(1).Return(
+					connector.NewResponseWithVersionAndLock(
+						200,
+						&typesv1beta1.Timestamp{Seconds: uint64(1234), Nanos: uint32(567)},
+						"abc123",
+					), nil)
 
 				httpAdapter.Lock(w, req)
 				resp := w.Result()
 				Expect(resp.StatusCode).To(Equal(200))
+				Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("abc123"))
+				Expect(resp.Header.Get(connector.HeaderWopiVersion)).To(Equal("v1234567"))
 			})
 		})
 
@@ -195,12 +204,13 @@ var _ = Describe("HttpAdapter", func() {
 
 				w := httptest.NewRecorder()
 
-				fc.On("Lock", mock.Anything, "abc123", "qwerty").Times(1).Return(connector.NewResponseWithLock(409, "zzz111"), nil)
+				fc.On("Lock", mock.Anything, "abc123", "qwerty").Times(1).Return(connector.NewResponseLockConflict("zzz111", "Lock Conflict"), nil)
 
 				httpAdapter.Lock(w, req)
 				resp := w.Result()
 				Expect(resp.StatusCode).To(Equal(409))
 				Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111"))
+				Expect(resp.Header.Get(connector.HeaderWopiLockFailureReason)).To(Equal("Lock Conflict"))
 			})
 
 			It("Success", func() {
@@ -211,11 +221,18 @@ var _ = Describe("HttpAdapter", func() {
 
 				w := httptest.NewRecorder()
 
-				fc.On("Lock", mock.Anything, "abc123", "qwerty").Times(1).Return(connector.NewResponse(200), nil)
+				fc.On("Lock", mock.Anything, "abc123", "qwerty").Times(1).Return(
+					connector.NewResponseWithVersionAndLock(
+						200,
+						&typesv1beta1.Timestamp{Seconds: uint64(1234), Nanos: uint32(567)},
+						"abc123",
+					), nil)
 
 				httpAdapter.Lock(w, req)
 				resp := w.Result()
 				Expect(resp.StatusCode).To(Equal(200))
+				Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("abc123"))
+				Expect(resp.Header.Get(connector.HeaderWopiVersion)).To(Equal("v1234567"))
 			})
 		})
 	})
@@ -256,12 +273,13 @@ var _ = Describe("HttpAdapter", func() {
 
 			w := httptest.NewRecorder()
 
-			fc.On("RefreshLock", mock.Anything, "abc123").Times(1).Return(connector.NewResponseWithLock(409, "zzz111"), nil)
+			fc.On("RefreshLock", mock.Anything, "abc123").Times(1).Return(connector.NewResponseLockConflict("zzz111", "Lock Conflict"), nil)
 
 			httpAdapter.RefreshLock(w, req)
 			resp := w.Result()
 			Expect(resp.StatusCode).To(Equal(409))
 			Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111"))
+			Expect(resp.Header.Get(connector.HeaderWopiLockFailureReason)).To(Equal("Lock Conflict"))
 		})
 
 		It("Success", func() {
@@ -271,11 +289,18 @@ var _ = Describe("HttpAdapter", func() {
 
 			w := httptest.NewRecorder()
 
-			fc.On("RefreshLock", mock.Anything, "abc123").Times(1).Return(connector.NewResponse(200), nil)
+			fc.On("RefreshLock", mock.Anything, "abc123").Times(1).Return(
+				connector.NewResponseWithVersionAndLock(
+					200,
+					&typesv1beta1.Timestamp{Seconds: uint64(1234), Nanos: uint32(5678)},
+					"abc123",
+				), nil)
 
 			httpAdapter.RefreshLock(w, req)
 			resp := w.Result()
 			Expect(resp.StatusCode).To(Equal(200))
+			Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("abc123"))
+			Expect(resp.Header.Get(connector.HeaderWopiVersion)).To(Equal("v12345678"))
 		})
 	})
 
@@ -315,12 +340,13 @@ var _ = Describe("HttpAdapter", func() {
 
 			w := httptest.NewRecorder()
 
-			fc.On("UnLock", mock.Anything, "abc123").Times(1).Return(connector.NewResponseWithLock(409, "zzz111"), nil)
+			fc.On("UnLock", mock.Anything, "abc123").Times(1).Return(connector.NewResponseLockConflict("zzz111", "Lock Conflict"), nil)
 
 			httpAdapter.UnLock(w, req)
 			resp := w.Result()
 			Expect(resp.StatusCode).To(Equal(409))
 			Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111"))
+			Expect(resp.Header.Get(connector.HeaderWopiLockFailureReason)).To(Equal("Lock Conflict"))
 		})
 
 		It("Success", func() {
@@ -330,11 +356,13 @@ var _ = Describe("HttpAdapter", func() {
 
 			w := httptest.NewRecorder()
 
-			fc.On("UnLock", mock.Anything, "abc123").Times(1).Return(connector.NewResponse(200), nil)
+			fc.On("UnLock", mock.Anything, "abc123").Times(1).Return(
+				connector.NewResponseWithVersion(&typesv1beta1.Timestamp{Seconds: uint64(1234), Nanos: uint32(567)}), nil)
 
 			httpAdapter.UnLock(w, req)
 			resp := w.Result()
 			Expect(resp.StatusCode).To(Equal(200))
+			Expect(resp.Header.Get(connector.HeaderWopiVersion)).To(Equal("v1234567"))
 		})
 	})
 
@@ -458,12 +486,14 @@ var _ = Describe("HttpAdapter", func() {
 
 			w := httptest.NewRecorder()
 
-			cc.On("PutFile", mock.Anything, mock.Anything, int64(len(contentBody)), "abc123").Times(1).Return(connector.NewResponseWithLock(409, "zzz111"), nil)
+			cc.On("PutFile", mock.Anything, mock.Anything, int64(len(contentBody)), "abc123").Times(1).Return(
+				connector.NewResponseLockConflict("zzz111", "Lock Conflict"), nil)
 
 			httpAdapter.PutFile(w, req)
 			resp := w.Result()
 			Expect(resp.StatusCode).To(Equal(409))
 			Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111"))
+			Expect(resp.Header.Get(connector.HeaderWopiLockFailureReason)).To(Equal("Lock Conflict"))
 		})
 
 		It("Success", func() {
@@ -473,11 +503,18 @@ var _ = Describe("HttpAdapter", func() {
 
 			w := httptest.NewRecorder()
 
-			cc.On("PutFile", mock.Anything, mock.Anything, int64(len(contentBody)), "abc123").Times(1).Return(connector.NewResponse(200), nil)
+			cc.On("PutFile", mock.Anything, mock.Anything, int64(len(contentBody)), "abc123").Times(1).Return(
+				connector.NewResponseWithVersionAndLock(
+					200,
+					&typesv1beta1.Timestamp{Seconds: uint64(1234), Nanos: uint32(567)},
+					"abc123",
+				), nil)
 
 			httpAdapter.PutFile(w, req)
 			resp := w.Result()
 			Expect(resp.StatusCode).To(Equal(200))
+			Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("abc123"))
+			Expect(resp.Header.Get(connector.HeaderWopiVersion)).To(Equal("v1234567"))
 		})
 	})
 })
diff --git a/services/collaboration/pkg/middleware/middleware_suite_test.go b/services/collaboration/pkg/middleware/middleware_suite_test.go
new file mode 100644
index 00000000000..09e4e113609
--- /dev/null
+++ b/services/collaboration/pkg/middleware/middleware_suite_test.go
@@ -0,0 +1,13 @@
+package middleware_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+func TestMiddleware(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Middleware Suite")
+}
diff --git a/services/collaboration/pkg/middleware/wopicontext_test.go b/services/collaboration/pkg/middleware/wopicontext_test.go
new file mode 100644
index 00000000000..900cf5c5679
--- /dev/null
+++ b/services/collaboration/pkg/middleware/wopicontext_test.go
@@ -0,0 +1,226 @@
+package middleware_test
+
+import (
+	"context"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"path"
+	"strconv"
+
+	appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
+	userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
+	providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
+	"github.com/cs3org/reva/v2/pkg/token"
+	rjwt "github.com/cs3org/reva/v2/pkg/token/manager/jwt"
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
+	"github.com/owncloud/ocis/v2/services/collaboration/pkg/helpers"
+	"github.com/owncloud/ocis/v2/services/collaboration/pkg/middleware"
+	"github.com/owncloud/ocis/v2/services/collaboration/pkg/wopisrc"
+)
+
+var _ = Describe("Wopi Context Middleware", func() {
+	var (
+		cfg     *config.Config
+		ctx     context.Context
+		mw      http.Handler
+		rid     *providerv1beta1.ResourceId
+		tknMngr token.Manager
+		user    *userv1beta1.User
+		src     *url.URL
+	)
+
+	BeforeEach(func() {
+		var err error
+		cfg = &config.Config{
+			TokenManager: &config.TokenManager{JWTSecret: "jwtSecret"},
+			Wopi: config.Wopi{
+				Secret:  "wopiSecret",
+				WopiSrc: "https://localhost:9300",
+			},
+		}
+
+		ctx = context.Background()
+
+		next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			w.WriteHeader(http.StatusOK)
+		})
+		mw = middleware.WopiContextAuthMiddleware(cfg, next)
+
+		tknMngr, err = rjwt.New(map[string]interface{}{
+			"secret":  cfg.TokenManager.JWTSecret,
+			"expires": int64(24 * 60 * 60),
+		})
+		Expect(err).ToNot(HaveOccurred())
+
+		user = &userv1beta1.User{
+			Id: &userv1beta1.UserId{
+				Idp:      "example.com",
+				OpaqueId: "12345",
+				Type:     userv1beta1.UserType_USER_TYPE_PRIMARY,
+			},
+			Username: "admin",
+			Mail:     "admin@example.com",
+		}
+
+		rid = &providerv1beta1.ResourceId{
+			StorageId: "storageID",
+			OpaqueId:  "opaqueID",
+			SpaceId:   "spaceID",
+		}
+
+		src, err = url.Parse(cfg.Wopi.WopiSrc)
+		src.Path = path.Join("wopi", "files", helpers.HashResourceId(rid))
+		Expect(err).ToNot(HaveOccurred())
+	})
+	It("Should not authorize with empty access token", func() {
+		req := httptest.NewRequest("GET", src.String(), nil).WithContext(ctx)
+		resp := httptest.NewRecorder()
+
+		mw.ServeHTTP(resp, req)
+		Expect(resp.Code).To(Equal(http.StatusUnauthorized))
+	})
+	It("Should not authorize with malformed access token", func() {
+		req := httptest.NewRequest("GET", src.String(), nil).WithContext(ctx)
+		q := req.URL.Query()
+		q.Add("access_token", "token")
+		req.URL.RawQuery = q.Encode()
+
+		resp := httptest.NewRecorder()
+
+		mw.ServeHTTP(resp, req)
+		Expect(resp.Code).To(Equal(http.StatusUnauthorized))
+	})
+	It("Should not authorize when fileID mismatches", func() {
+		req := httptest.NewRequest("GET", src.String(), nil).WithContext(ctx)
+		// create request with different fileID in the wopi context
+		token, err := tknMngr.MintToken(ctx, user, nil)
+		Expect(err).ToNot(HaveOccurred())
+		wopiContext := middleware.WopiContext{
+			AccessToken: token,
+			ViewMode:    appprovider.ViewMode_VIEW_MODE_READ_WRITE,
+			FileReference: &providerv1beta1.Reference{
+				ResourceId: &providerv1beta1.ResourceId{
+					StorageId: "storageID",
+					OpaqueId:  "opaqueID2",
+					SpaceId:   "spaceID",
+				},
+				Path: ".",
+			},
+		}
+		wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, cfg)
+		q := req.URL.Query()
+		q.Add("access_token", wopiToken)
+		q.Add("access_token_ttl", strconv.FormatInt(ttl, 10))
+		req.URL.RawQuery = q.Encode()
+		resp := httptest.NewRecorder()
+
+		mw.ServeHTTP(resp, req)
+		Expect(resp.Code).To(Equal(http.StatusUnauthorized))
+	})
+	It("Should not authorize with wrong wopi secret", func() {
+		src.Path = path.Join("wopi", "files", helpers.HashResourceId(rid))
+		req := httptest.NewRequest("GET", src.String(), nil).WithContext(ctx)
+		token, err := tknMngr.MintToken(ctx, user, nil)
+		Expect(err).ToNot(HaveOccurred())
+
+		wopiContext := middleware.WopiContext{
+			AccessToken: token,
+		}
+		// use wrong wopi secret when generating the wopi token
+		wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, &config.Config{Wopi: config.Wopi{
+			Secret: "wrongSecret",
+		}})
+		q := req.URL.Query()
+		q.Add("access_token", wopiToken)
+		q.Add("access_token_ttl", strconv.FormatInt(ttl, 10))
+		req.URL.RawQuery = q.Encode()
+		resp := httptest.NewRecorder()
+
+		mw.ServeHTTP(resp, req)
+		Expect(resp.Code).To(Equal(http.StatusUnauthorized))
+	})
+	It("Should authorize successful", func() {
+		req := httptest.NewRequest("GET", src.String(), nil).WithContext(ctx)
+		token, err := tknMngr.MintToken(ctx, user, nil)
+		Expect(err).ToNot(HaveOccurred())
+
+		wopiContext := middleware.WopiContext{
+			AccessToken: token,
+			ViewMode:    appprovider.ViewMode_VIEW_MODE_READ_WRITE,
+			FileReference: &providerv1beta1.Reference{
+				ResourceId: rid,
+				Path:       ".",
+			},
+		}
+		wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, cfg)
+		q := req.URL.Query()
+		q.Add("access_token", wopiToken)
+		q.Add("access_token_ttl", strconv.FormatInt(ttl, 10))
+		req.URL.RawQuery = q.Encode()
+		resp := httptest.NewRecorder()
+
+		mw.ServeHTTP(resp, req)
+		Expect(resp.Code).To(Equal(http.StatusOK))
+	})
+	It("Should not authorize with proxy when fileID mismatches", func() {
+		cfg.Wopi.ProxySecret = "proxySecret"
+		cfg.Wopi.ProxyURL = "https://proxy"
+		src, err := wopisrc.GenerateWopiSrc(helpers.HashResourceId(rid), cfg)
+		Expect(err).ToNot(HaveOccurred())
+
+		req := httptest.NewRequest("GET", src.String(), nil).WithContext(ctx)
+		token, err := tknMngr.MintToken(ctx, user, nil)
+		Expect(err).ToNot(HaveOccurred())
+		wopiContext := middleware.WopiContext{
+			AccessToken: token,
+			ViewMode:    appprovider.ViewMode_VIEW_MODE_READ_WRITE,
+			FileReference: &providerv1beta1.Reference{
+				ResourceId: &providerv1beta1.ResourceId{
+					StorageId: "storageID",
+					OpaqueId:  "opaqueID3",
+					SpaceId:   "spaceID",
+				},
+				Path: ".",
+			},
+		}
+		wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, cfg)
+		q := req.URL.Query()
+		q.Add("access_token", wopiToken)
+		q.Add("access_token_ttl", strconv.FormatInt(ttl, 10))
+		req.URL.RawQuery = q.Encode()
+
+		resp := httptest.NewRecorder()
+		mw.ServeHTTP(resp, req)
+		Expect(resp.Code).To(Equal(http.StatusUnauthorized))
+	})
+	It("Should authorize successful with proxy", func() {
+		cfg.Wopi.ProxySecret = "proxySecret"
+		cfg.Wopi.ProxyURL = "https://proxy"
+		src, err := wopisrc.GenerateWopiSrc(helpers.HashResourceId(rid), cfg)
+		Expect(err).ToNot(HaveOccurred())
+
+		req := httptest.NewRequest("GET", src.String(), nil).WithContext(ctx)
+		token, err := tknMngr.MintToken(ctx, user, nil)
+		Expect(err).ToNot(HaveOccurred())
+		wopiContext := middleware.WopiContext{
+			AccessToken: token,
+			ViewMode:    appprovider.ViewMode_VIEW_MODE_READ_WRITE,
+			FileReference: &providerv1beta1.Reference{
+				ResourceId: rid,
+				Path:       ".",
+			},
+		}
+		wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, cfg)
+		q := req.URL.Query()
+		q.Add("access_token", wopiToken)
+		q.Add("access_token_ttl", strconv.FormatInt(ttl, 10))
+		req.URL.RawQuery = q.Encode()
+
+		resp := httptest.NewRecorder()
+		mw.ServeHTTP(resp, req)
+		Expect(resp.Code).To(Equal(http.StatusOK))
+	})
+})
diff --git a/services/collaboration/pkg/service/grpc/v0/service_test.go b/services/collaboration/pkg/service/grpc/v0/service_test.go
index 6fd77b12504..40655ac3164 100644
--- a/services/collaboration/pkg/service/grpc/v0/service_test.go
+++ b/services/collaboration/pkg/service/grpc/v0/service_test.go
@@ -187,15 +187,60 @@ var _ = Describe("Discovery", func() {
 			Entry("Microsoft chat no lang", "Microsoft", "", false, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e"),
 			Entry("Collabora chat no lang", "Collabora", "", false, "https://test.server.prv/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e"),
 			Entry("OnlyOffice chat no lang", "OnlyOffice", "", false, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e"),
-			Entry("Microsoft chat lang", "Microsoft", "de", false, "https://test.server.prv/hosting/wopi/word/edit?UI_LLCC=de&WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e"),
-			Entry("Collabora chat lang", "Collabora", "de", false, "https://test.server.prv/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&lang=de"),
-			Entry("OnlyOffice chat lang", "OnlyOffice", "de", false, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&ui=de"),
+			Entry("Microsoft chat lang", "Microsoft", "de", false, "https://test.server.prv/hosting/wopi/word/edit?UI_LLCC=de-DE&WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e"),
+			Entry("Collabora chat lang", "Collabora", "de", false, "https://test.server.prv/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&lang=de-DE"),
+			Entry("OnlyOffice chat lang", "OnlyOffice", "de", false, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&ui=de-DE"),
 			Entry("Microsoft no chat no lang", "Microsoft", "", true, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"),
 			Entry("Collabora no chat no lang", "Collabora", "", true, "https://test.server.prv/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"),
 			Entry("OnlyOffice no chat no lang", "OnlyOffice", "", true, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"),
-			Entry("Microsoft no chat lang", "Microsoft", "de", true, "https://test.server.prv/hosting/wopi/word/edit?UI_LLCC=de&WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"),
-			Entry("Collabora no chat lang", "Collabora", "de", true, "https://test.server.prv/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1&lang=de"),
-			Entry("OnlyOffice no chat lang", "OnlyOffice", "de", true, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1&ui=de"),
+			Entry("Microsoft no chat lang", "Microsoft", "de", true, "https://test.server.prv/hosting/wopi/word/edit?UI_LLCC=de-DE&WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"),
+			Entry("Collabora no chat lang", "Collabora", "de", true, "https://test.server.prv/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1&lang=de-DE"),
+			Entry("OnlyOffice no chat lang", "OnlyOffice", "de", true, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1&ui=de-DE"),
 		)
+		It("Success with Wopi Proxy", func() {
+			ctx := context.Background()
+			nowTime := time.Now()
+
+			cfg.Wopi.WopiSrc = "https://wopiserver.test.prv"
+			cfg.Wopi.Secret = "my_supa_secret"
+			cfg.Wopi.ProxyURL = "https://office.proxy.test.prv"
+			cfg.Wopi.ProxySecret = "your_supa_secret"
+			cfg.App.Name = "Microsoft"
+
+			myself := &userv1beta1.User{
+				Id: &userv1beta1.UserId{
+					Idp:      "myIdp",
+					OpaqueId: "opaque001",
+					Type:     userv1beta1.UserType_USER_TYPE_PRIMARY,
+				},
+				Username: "username",
+			}
+
+			req := &appproviderv1beta1.OpenInAppRequest{
+				ResourceInfo: &providerv1beta1.ResourceInfo{
+					Id: &providerv1beta1.ResourceId{
+						StorageId: "myStorage",
+						OpaqueId:  "storageOpaque001",
+						SpaceId:   "SpaceA",
+					},
+					Path: "/path/to/file.docx",
+				},
+				ViewMode:    appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
+				AccessToken: MintToken(myself, cfg.Wopi.Secret, nowTime),
+			}
+			req.Opaque = utils.AppendPlainToOpaque(req.Opaque, "lang", "en")
+
+			gatewayClient.On("WhoAmI", mock.Anything, mock.Anything).Times(1).Return(&gatewayv1beta1.WhoAmIResponse{
+				Status: status.NewOK(ctx),
+				User:   myself,
+			}, nil)
+
+			resp, err := srv.OpenInApp(ctx, req)
+			Expect(err).To(Succeed())
+			Expect(resp.GetStatus().GetCode()).To(Equal(rpcv1beta1.Code_CODE_OK))
+			Expect(resp.GetAppUrl().GetMethod()).To(Equal("POST"))
+			Expect(resp.GetAppUrl().GetAppUrl()).To(Equal("https://test.server.prv/hosting/wopi/word/edit?UI_LLCC=en-US&WOPISrc=https%3A%2F%2Foffice.proxy.test.prv%2Fwopi%2Ffiles%2FeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1IjoiaHR0cHM6Ly93b3Bpc2VydmVyLnRlc3QucHJ2L3dvcGkvZmlsZXMvIiwiZiI6IjJmNmVjMTg2OTZkZDEwMDgxMDY3NDliZDk0MTA2ZTVjZmFkNWMwOWUxNWRlN2I3NzA4OGQwMzg0M2U3MWI0M2UifQ.yfyLHZ18Z1MFOa6u7AP0LqfIiQ9X5AMkYauEZGhbCNs"))
+			Expect(resp.GetAppUrl().GetFormParameters()["access_token_ttl"]).To(Equal(strconv.FormatInt(nowTime.Add(5*time.Hour).Unix()*1000, 10)))
+		})
 	})
 })
diff --git a/services/collaboration/pkg/wopisrc/wopisrc_suite_test.go b/services/collaboration/pkg/wopisrc/wopisrc_suite_test.go
new file mode 100644
index 00000000000..5740b990640
--- /dev/null
+++ b/services/collaboration/pkg/wopisrc/wopisrc_suite_test.go
@@ -0,0 +1,13 @@
+package wopisrc_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+func TestWopisrc(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Wopisrc Suite")
+}
diff --git a/services/collaboration/pkg/wopisrc/wopisrc_test.go b/services/collaboration/pkg/wopisrc/wopisrc_test.go
new file mode 100644
index 00000000000..1b4bab0d70d
--- /dev/null
+++ b/services/collaboration/pkg/wopisrc/wopisrc_test.go
@@ -0,0 +1,64 @@
+package wopisrc_test
+
+import (
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
+	"github.com/owncloud/ocis/v2/services/collaboration/pkg/wopisrc"
+)
+
+var _ = Describe("Wopisrc Test", func() {
+	var (
+		c *config.Config
+	)
+
+	Context("GenerateWopiSrc", func() {
+		BeforeEach(func() {
+			c = &config.Config{
+				Wopi: config.Wopi{
+					WopiSrc:     "https://ocis.team/wopi/files",
+					ProxyURL:    "https://cloud.proxy.com",
+					ProxySecret: "secret",
+				},
+			}
+		})
+		When("WopiSrc URL is incorrect", func() {
+			c = &config.Config{
+				Wopi: config.Wopi{
+					WopiSrc: "https:&//ocis.team/wopi/files",
+				},
+			}
+			url, err := wopisrc.GenerateWopiSrc("123456", c)
+			Expect(err).To(HaveOccurred())
+			Expect(url).To(BeNil())
+		})
+		When("proxy URL is incorrect", func() {
+			c = &config.Config{
+				Wopi: config.Wopi{
+					WopiSrc:     "https://ocis.team/wopi/files",
+					ProxyURL:    "cloud",
+					ProxySecret: "secret",
+				},
+			}
+			url, err := wopisrc.GenerateWopiSrc("123456", c)
+			Expect(err).To(HaveOccurred())
+			Expect(url).To(BeNil())
+		})
+		When("proxy URL and proxy secret are configured", func() {
+			It("should generate a WOPI src URL as a jwt token", func() {
+				url, err := wopisrc.GenerateWopiSrc("123456", c)
+				Expect(err).ToNot(HaveOccurred())
+				Expect(url.String()).To(Equal("https://cloud.proxy.com/wopi/files/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1IjoiaHR0cHM6Ly9vY2lzLnRlYW0vd29waS9maWxlcy8iLCJmIjoiMTIzNDU2In0.6ol9PQXGKktKfAri8tsJ4X_a9rIeosJ7id6KTQW6Ui0"))
+			})
+		})
+		When("proxy URL and proxy secret are not configured", func() {
+			It("should generate a WOPI src URL as a direct URL", func() {
+				c.Wopi.ProxyURL = ""
+				c.Wopi.ProxySecret = ""
+				url, err := wopisrc.GenerateWopiSrc("123456", c)
+				Expect(err).ToNot(HaveOccurred())
+				Expect(url.String()).To(Equal("https://ocis.team/wopi/files/123456"))
+			})
+		})
+	})
+})