Merge remote-tracking branch 'origin/dev' into 45001-component-to-show-the-list-of-non-working-days-of-year
After Width: | Height: | Size: 154 KiB |
Before Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 69 KiB |
After Width: | Height: | Size: 262 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 154 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 55 KiB |
After Width: | Height: | Size: 146 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 81 KiB |
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 131 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 90 KiB |
After Width: | Height: | Size: 137 KiB |
Before Width: | Height: | Size: 389 KiB After Width: | Height: | Size: 241 KiB |
@ -1,130 +0,0 @@ |
||||
// -- copyright
|
||||
// OpenProject is an open source project management software.
|
||||
// Copyright (C) 2012-2022 the OpenProject GmbH
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See COPYRIGHT and LICENSE files for more details.
|
||||
//++
|
||||
|
||||
import { Injectable } from '@angular/core'; |
||||
import { HttpClient } from '@angular/common/http'; |
||||
import { Observable } from 'rxjs'; |
||||
import { map, switchMap } from 'rxjs/operators'; |
||||
|
||||
import { IUploadLink } from 'core-app/core/state/storage-files/upload-link.model'; |
||||
import { IStorageFile } from 'core-app/core/state/storage-files/storage-file.model'; |
||||
import { TimezoneService } from 'core-app/core/datetime/timezone.service'; |
||||
|
||||
@Injectable() |
||||
export class UploadStorageFilesService { |
||||
constructor( |
||||
private readonly httpClient:HttpClient, |
||||
private readonly timezoneService:TimezoneService, |
||||
) {} |
||||
|
||||
public uploadFile(uploadLink:IUploadLink, file:File):Observable<IStorageFile> { |
||||
const url = new URL(uploadLink._links.destination.href); |
||||
const token = url.username; |
||||
const password = url.password; |
||||
url.username = ''; |
||||
url.password = ''; |
||||
|
||||
const headers = { |
||||
Authorization: `Basic ${btoa(`${token}:${password}`)}`, |
||||
'X-External-Request': 'true', |
||||
}; |
||||
|
||||
const method = uploadLink._links.destination.method; |
||||
return this.httpClient |
||||
.request(method, url.toString(), { body: file, headers }) |
||||
.pipe( |
||||
switchMap(() => this.httpClient.request( |
||||
'propfind', |
||||
url.toString(), |
||||
{ |
||||
body: this.propfindBody, |
||||
headers, |
||||
responseType: 'text', |
||||
}, |
||||
)), |
||||
map((xml) => this.parseXmlResponse(xml)), |
||||
); |
||||
} |
||||
|
||||
private parseXmlResponse(xml:string):IStorageFile { |
||||
const response = new DOMParser().parseFromString(xml, 'application/xml'); |
||||
const error = new Error(`Invalid response for uploaded file: ${xml}`); |
||||
|
||||
const id = response.getElementsByTagName('oc:fileid')[0].textContent; |
||||
if (!id) { throw error; } |
||||
|
||||
const mimeType = response.getElementsByTagName('d:getcontenttype')[0].textContent; |
||||
if (!mimeType) { throw error; } |
||||
|
||||
const size = response.getElementsByTagName('oc:size')[0].textContent; |
||||
if (!size) { throw error; } |
||||
|
||||
const href = response.getElementsByTagName('d:href')[0].textContent; |
||||
const parts = href?.split('/'); |
||||
if (!parts || parts.length < 1) { throw error; } |
||||
|
||||
const name = parts.pop(); |
||||
if (!name) { throw error; } |
||||
|
||||
const location = `/${parts.slice(parts.indexOf('webdav') + 1).join('/')}`; |
||||
|
||||
const date = response.getElementsByTagName('d:getlastmodified')[0].textContent; |
||||
if (!date) { throw error; } |
||||
const createdAt = this.timezoneService.parseDatetime(date).toISOString(); |
||||
const lastModifiedAt = createdAt; |
||||
|
||||
const creator = response.getElementsByTagName('oc:owner-display-name')[0].textContent; |
||||
if (!creator) { throw error; } |
||||
|
||||
return { |
||||
id, |
||||
name: decodeURIComponent(name), |
||||
location, |
||||
mimeType, |
||||
size: parseInt(size, 10), |
||||
createdAt, |
||||
createdByName: creator, |
||||
lastModifiedAt, |
||||
lastModifiedByName: creator, |
||||
permissions: [], |
||||
}; |
||||
} |
||||
|
||||
private get propfindBody() { |
||||
return `<?xml version="1.0"?>
|
||||
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns"> |
||||
<d:prop> |
||||
<oc:fileid /> |
||||
<d:getlastmodified /> |
||||
<d:getcontenttype /> |
||||
<oc:size /> |
||||
<oc:owner-display-name /> |
||||
</d:prop> |
||||
</d:propfind>`; |
||||
} |
||||
} |
@ -0,0 +1,150 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2023 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
module Storages::Peripherals::StorageInteraction::Nextcloud |
||||
class LegacyUploadLinkQuery < Storages::Peripherals::StorageInteraction::StorageQuery |
||||
using Storages::Peripherals::ServiceResultRefinements # use '>>' (bind) operator for ServiceResult |
||||
|
||||
URI_BASE_PATH = '/ocs/v2.php/apps/files_sharing/api/v1/shares'.freeze |
||||
UPLOAD_LINK_BASE = '/public.php/webdav'.freeze |
||||
|
||||
def initialize(base_uri:, token:, retry_proc:) |
||||
super() |
||||
|
||||
@base_uri = base_uri |
||||
@token = token |
||||
@retry_proc = retry_proc |
||||
end |
||||
|
||||
def query(data) |
||||
validated(data) >> |
||||
method(:create_file_share) >> |
||||
method(:apply_drop_permission) >> |
||||
method(:build_upload_link) |
||||
end |
||||
|
||||
private |
||||
|
||||
def validated(data) |
||||
if data.nil? || data['fileName'].nil? || data['parent'].nil? |
||||
error(:error, 'Data is invalid', data) |
||||
else |
||||
ServiceResult.success( |
||||
result: Struct.new(:file_name, :parent) |
||||
.new(data['fileName'], data['parent']) |
||||
) |
||||
end |
||||
end |
||||
|
||||
def create_file_share(data) |
||||
password = SecureRandom.uuid |
||||
|
||||
outbound_response( |
||||
method: :post, |
||||
relative_path: URI_BASE_PATH, |
||||
payload: { |
||||
shareType: 3, |
||||
password:, |
||||
path: data.parent, |
||||
expireDate: Date.tomorrow |
||||
} |
||||
).map do |response| |
||||
Struct.new(:id, :token, :password, :file_name) |
||||
.new(response.ocs.data.id, response.ocs.data.token, password, data.file_name) |
||||
end |
||||
end |
||||
|
||||
def apply_drop_permission(share) |
||||
outbound_response( |
||||
method: :put, |
||||
relative_path: "#{URI_BASE_PATH}/#{share.id}", |
||||
payload: { |
||||
permissions: 5 |
||||
} |
||||
).map { share } |
||||
end |
||||
|
||||
def build_upload_link(share) |
||||
destination = @base_uri.merge("#{UPLOAD_LINK_BASE}/#{ERB::Util.url_encode(share.file_name)}") |
||||
destination.user = share.token |
||||
destination.password = share.password |
||||
|
||||
ServiceResult.success(result: Storages::UploadLink.new(destination, :put)) |
||||
end |
||||
|
||||
def outbound_response(method:, relative_path:, payload:) # rubocop:disable Metrics/AbcSize |
||||
@retry_proc.call(@token) do |token| |
||||
begin |
||||
response = ServiceResult.success( |
||||
result: RestClient::Request.execute( |
||||
method:, |
||||
url: @base_uri.merge(relative_path).to_s, |
||||
payload: payload.to_json, |
||||
headers: { |
||||
'Authorization' => "Bearer #{token.access_token}", |
||||
'OCS-APIRequest' => 'true', |
||||
'Accept' => 'application/json', |
||||
'Content-Type' => 'application/json' |
||||
} |
||||
) |
||||
) |
||||
rescue RestClient::Unauthorized => e |
||||
response = error(:not_authorized, 'Outbound request not authorized!', e.response) |
||||
rescue RestClient::NotFound => e |
||||
response = error(:not_found, 'Outbound request destination not found!', e.response) |
||||
rescue RestClient::ExceptionWithResponse => e |
||||
response = error(:error, 'Outbound request failed!', e.response) |
||||
rescue StandardError |
||||
response = error(:error, 'Outbound request failed!') |
||||
end |
||||
|
||||
# rubocop:disable Style/OpenStructUse |
||||
# rubocop:disable Style/MultilineBlockChain |
||||
response |
||||
.bind do |r| |
||||
# The nextcloud API returns a successful response with empty body if the authorization is missing or expired |
||||
if r.body.blank? |
||||
error(:not_authorized, 'Outbound request not authorized!') |
||||
else |
||||
ServiceResult.success(result: r) |
||||
end |
||||
end |
||||
.map { |r| JSON.parse(r.body, object_class: OpenStruct) } |
||||
# rubocop:enable Style/MultilineBlockChain |
||||
# rubocop:enable Style/OpenStructUse Style/MultilineBlockChain |
||||
end |
||||
end |
||||
|
||||
def error(code, log_message = nil, data = nil) |
||||
ServiceResult.failure( |
||||
result: code, # This is needed to work with the ConnectionManager token refresh mechanism. |
||||
errors: Storages::StorageError.new(code:, log_message:, data:) |
||||
) |
||||
end |
||||
end |
||||
end |