mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
feat(sync): add custom cap http plugin for webdav methods
This commit is contained in:
parent
5c27dd2b5a
commit
84c24ebf79
8 changed files with 1299 additions and 19 deletions
|
|
@ -0,0 +1,138 @@
|
|||
package com.superproductivity.plugins.webdavhttp;
|
||||
|
||||
import com.getcapacitor.JSObject;
|
||||
import com.getcapacitor.Plugin;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import com.getcapacitor.PluginMethod;
|
||||
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.Iterator;
|
||||
|
||||
@CapacitorPlugin(name = "WebDavHttp")
|
||||
public class WebDavHttpPlugin extends Plugin {
|
||||
|
||||
private static final String[] WEBDAV_METHODS = {
|
||||
"PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK"
|
||||
};
|
||||
|
||||
private boolean isWebDavMethod(String method) {
|
||||
for (String webdavMethod : WEBDAV_METHODS) {
|
||||
if (webdavMethod.equalsIgnoreCase(method)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void request(PluginCall call) {
|
||||
String urlString = call.getString("url");
|
||||
String method = call.getString("method", "GET");
|
||||
JSObject headers = call.getObject("headers", new JSObject());
|
||||
String data = call.getString("data");
|
||||
|
||||
if (urlString == null) {
|
||||
call.reject("URL is required");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
URL url = new URL(urlString);
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
|
||||
// For WebDAV methods, we need to use reflection since HttpURLConnection
|
||||
// only supports standard HTTP methods
|
||||
if (isWebDavMethod(method)) {
|
||||
try {
|
||||
// First set it as POST to allow output
|
||||
connection.setRequestMethod("POST");
|
||||
// Then use reflection to set the actual method
|
||||
java.lang.reflect.Field methodField = HttpURLConnection.class.getDeclaredField("method");
|
||||
methodField.setAccessible(true);
|
||||
methodField.set(connection, method);
|
||||
|
||||
// Also update the delegate if using OkHttp
|
||||
Object delegate = connection;
|
||||
try {
|
||||
java.lang.reflect.Field delegateField = connection.getClass().getDeclaredField("delegate");
|
||||
delegateField.setAccessible(true);
|
||||
delegate = delegateField.get(connection);
|
||||
if (delegate != null) {
|
||||
java.lang.reflect.Field delegateMethodField = delegate.getClass().getSuperclass().getDeclaredField("method");
|
||||
delegateMethodField.setAccessible(true);
|
||||
delegateMethodField.set(delegate, method);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// OkHttp delegate not present, that's OK
|
||||
}
|
||||
} catch (Exception e) {
|
||||
call.reject("Failed to set WebDAV method: " + method, e);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Standard HTTP method
|
||||
connection.setRequestMethod(method);
|
||||
}
|
||||
|
||||
// Set headers
|
||||
Iterator<String> keys = headers.keys();
|
||||
while (keys.hasNext()) {
|
||||
String key = keys.next();
|
||||
String value = headers.getString(key);
|
||||
connection.setRequestProperty(key, value);
|
||||
}
|
||||
|
||||
// Handle request body
|
||||
if (data != null && !data.isEmpty()) {
|
||||
connection.setDoOutput(true);
|
||||
DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream());
|
||||
outputStream.writeBytes(data);
|
||||
outputStream.flush();
|
||||
outputStream.close();
|
||||
}
|
||||
|
||||
// Get response
|
||||
int responseCode = connection.getResponseCode();
|
||||
|
||||
// Read response body
|
||||
BufferedReader reader;
|
||||
if (responseCode >= 200 && responseCode < 300) {
|
||||
reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
|
||||
} else {
|
||||
reader = new BufferedReader(new InputStreamReader(connection.getErrorStream()));
|
||||
}
|
||||
|
||||
StringBuilder responseBody = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
responseBody.append(line).append("\n");
|
||||
}
|
||||
reader.close();
|
||||
|
||||
// Get response headers
|
||||
JSObject responseHeaders = new JSObject();
|
||||
for (String headerKey : connection.getHeaderFields().keySet()) {
|
||||
if (headerKey != null) {
|
||||
responseHeaders.put(headerKey, connection.getHeaderField(headerKey));
|
||||
}
|
||||
}
|
||||
|
||||
// Return response
|
||||
JSObject result = new JSObject();
|
||||
result.put("data", responseBody.toString());
|
||||
result.put("status", responseCode);
|
||||
result.put("headers", responseHeaders);
|
||||
result.put("url", connection.getURL().toString());
|
||||
|
||||
call.resolve(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
call.reject("Request failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import com.superproductivity.superproductivity.util.printWebViewVersion
|
|||
import com.superproductivity.superproductivity.webview.JavaScriptInterface
|
||||
import com.superproductivity.superproductivity.webview.WebHelper
|
||||
import com.superproductivity.superproductivity.plugins.SafBridgePlugin
|
||||
import com.superproductivity.plugins.webdavhttp.WebDavHttpPlugin
|
||||
|
||||
/**
|
||||
* All new Super-Productivity main activity, based on Capacitor to support offline use of the entire application
|
||||
|
|
@ -26,7 +27,8 @@ class CapacitorMainActivity : BridgeActivity() {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Register plugins before calling super.onCreate()
|
||||
registerPlugin(SafBridgePlugin::class.java)
|
||||
|
||||
registerPlugin(WebDavHttpPlugin::class.java)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
printWebViewVersion(bridge.webView)
|
||||
|
||||
|
|
|
|||
922
package-lock.json
generated
922
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,18 @@
|
|||
export interface WebDavHttpPlugin {
|
||||
request(options: WebDavHttpOptions): Promise<WebDavHttpResponse>;
|
||||
}
|
||||
|
||||
export interface WebDavHttpOptions {
|
||||
url: string;
|
||||
method: string;
|
||||
headers?: Record<string, string>;
|
||||
data?: string | null;
|
||||
responseType?: 'text' | 'json';
|
||||
}
|
||||
|
||||
export interface WebDavHttpResponse {
|
||||
data: string;
|
||||
status: number;
|
||||
headers: Record<string, string>;
|
||||
url: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { registerPlugin } from '@capacitor/core';
|
||||
import type { WebDavHttpPlugin } from './definitions';
|
||||
|
||||
const WebDavHttp = registerPlugin<WebDavHttpPlugin>('WebDavHttp', {
|
||||
web: () => import('./web').then((m) => new m.WebDavHttpWeb()),
|
||||
});
|
||||
|
||||
export * from './definitions';
|
||||
export { WebDavHttp };
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { WebPlugin } from '@capacitor/core';
|
||||
import type {
|
||||
WebDavHttpPlugin,
|
||||
WebDavHttpOptions,
|
||||
WebDavHttpResponse,
|
||||
} from './definitions';
|
||||
|
||||
export class WebDavHttpWeb extends WebPlugin implements WebDavHttpPlugin {
|
||||
async request(options: WebDavHttpOptions): Promise<WebDavHttpResponse> {
|
||||
const { url, method, headers, data } = options;
|
||||
|
||||
// For web platform, we can use fetch with any HTTP method including WebDAV
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body: data || undefined,
|
||||
});
|
||||
|
||||
// Get response headers
|
||||
const responseHeaders: Record<string, string> = {};
|
||||
response.headers.forEach((value, key) => {
|
||||
responseHeaders[key] = value;
|
||||
});
|
||||
|
||||
// Get response data
|
||||
const responseData = await response.text();
|
||||
|
||||
return {
|
||||
data: responseData,
|
||||
status: response.status,
|
||||
headers: responseHeaders,
|
||||
url: response.url,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import {
|
|||
RemoteFileNotFoundAPIError,
|
||||
TooManyRequestsAPIError,
|
||||
} from '../../../errors/errors';
|
||||
import { CapacitorHttp } from '@capacitor/core';
|
||||
|
||||
describe('WebDavHttpAdapter', () => {
|
||||
let adapter: WebDavHttpAdapter;
|
||||
|
|
@ -232,16 +233,135 @@ describe('WebDavHttpAdapter', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// Skip CapacitorHttp tests as they require a proper Capacitor environment
|
||||
// These would be better as integration tests
|
||||
xdescribe('CapacitorHttp mode (Android)', () => {
|
||||
// CapacitorHttp tests for Android WebView mode
|
||||
describe('CapacitorHttp mode (Android)', () => {
|
||||
let capacitorHttpSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
adapter = new TestableWebDavHttpAdapter(true);
|
||||
capacitorHttpSpy = spyOn(CapacitorHttp, 'request');
|
||||
});
|
||||
|
||||
it('should use CapacitorHttp when in Android WebView mode', () => {
|
||||
// This is a placeholder - real tests would require Capacitor environment
|
||||
expect(adapter).toBeDefined();
|
||||
it('should use CapacitorHttp for PROPFIND method', async () => {
|
||||
const mockResponse = {
|
||||
status: 207,
|
||||
headers: { content_type: 'application/xml' },
|
||||
data: '<xml>response</xml>',
|
||||
url: 'http://example.com/test',
|
||||
};
|
||||
capacitorHttpSpy.and.returnValue(Promise.resolve(mockResponse));
|
||||
|
||||
const result = await adapter.request({
|
||||
url: 'http://example.com/test',
|
||||
method: 'PROPFIND',
|
||||
headers: { CONTENT_TYPE: 'application/xml' },
|
||||
body: '<xml>propfind</xml>',
|
||||
});
|
||||
|
||||
expect(capacitorHttpSpy).toHaveBeenCalledWith({
|
||||
url: 'http://example.com/test',
|
||||
method: 'PROPFIND',
|
||||
headers: { CONTENT_TYPE: 'application/xml' },
|
||||
data: '<xml>propfind</xml>',
|
||||
});
|
||||
expect(result.status).toBe(207);
|
||||
});
|
||||
|
||||
it('should use WebDavHttp for MKCOL method', async () => {
|
||||
const mockResponse = {
|
||||
status: 201,
|
||||
headers: {},
|
||||
data: '',
|
||||
url: 'http://example.com/newfolder',
|
||||
};
|
||||
webDavHttpSpy.and.returnValue(Promise.resolve(mockResponse));
|
||||
|
||||
const result = await adapter.request({
|
||||
url: 'http://example.com/newfolder',
|
||||
method: 'MKCOL',
|
||||
});
|
||||
|
||||
expect(webDavHttpSpy).toHaveBeenCalledWith({
|
||||
url: 'http://example.com/newfolder',
|
||||
method: 'MKCOL',
|
||||
headers: undefined,
|
||||
data: null,
|
||||
});
|
||||
expect(capacitorHttpSpy).not.toHaveBeenCalled();
|
||||
expect(result.status).toBe(201);
|
||||
});
|
||||
|
||||
it('should use CapacitorHttp for standard HTTP methods', async () => {
|
||||
const mockResponse = {
|
||||
status: 200,
|
||||
headers: { content_type: 'text/plain' },
|
||||
data: 'file content',
|
||||
};
|
||||
capacitorHttpSpy.and.returnValue(Promise.resolve(mockResponse));
|
||||
|
||||
const result = await adapter.request({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
expect(capacitorHttpSpy).toHaveBeenCalledWith({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'GET',
|
||||
headers: undefined,
|
||||
data: null,
|
||||
});
|
||||
expect(webDavHttpSpy).not.toHaveBeenCalled();
|
||||
expect(result.status).toBe(200);
|
||||
});
|
||||
|
||||
it('should handle WebDAV methods case-insensitively', async () => {
|
||||
const mockResponse = {
|
||||
status: 207,
|
||||
headers: {},
|
||||
data: '<xml/>',
|
||||
url: 'http://example.com/test',
|
||||
};
|
||||
webDavHttpSpy.and.returnValue(Promise.resolve(mockResponse));
|
||||
|
||||
await adapter.request({
|
||||
url: 'http://example.com/test',
|
||||
method: 'propfind', // lowercase
|
||||
});
|
||||
|
||||
expect(webDavHttpSpy).toHaveBeenCalled();
|
||||
expect(capacitorHttpSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle all WebDAV methods', async () => {
|
||||
const webDavMethods = [
|
||||
'PROPFIND',
|
||||
'MKCOL',
|
||||
'MOVE',
|
||||
'COPY',
|
||||
'LOCK',
|
||||
'UNLOCK',
|
||||
'PROPPATCH',
|
||||
];
|
||||
const mockResponse = {
|
||||
status: 200,
|
||||
headers: {},
|
||||
data: '',
|
||||
url: 'http://example.com/test',
|
||||
};
|
||||
webDavHttpSpy.and.returnValue(Promise.resolve(mockResponse));
|
||||
|
||||
for (const method of webDavMethods) {
|
||||
webDavHttpSpy.calls.reset();
|
||||
capacitorHttpSpy.calls.reset();
|
||||
|
||||
await adapter.request({
|
||||
url: 'http://example.com/test',
|
||||
method: method,
|
||||
});
|
||||
|
||||
expect(webDavHttpSpy).toHaveBeenCalled();
|
||||
expect(capacitorHttpSpy).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { CapacitorHttp, HttpResponse } from '@capacitor/core';
|
||||
import { CapacitorHttp, HttpResponse, registerPlugin } from '@capacitor/core';
|
||||
import { IS_ANDROID_WEB_VIEW } from '../../../../../util/is-android-web-view';
|
||||
import { PFLog } from '../../../../../core/log';
|
||||
import {
|
||||
|
|
@ -9,6 +9,13 @@ import {
|
|||
} from '../../../errors/errors';
|
||||
import { WebDavHttpStatus } from './webdav.const';
|
||||
|
||||
// Define and register our WebDAV plugin
|
||||
interface WebDavHttpPlugin {
|
||||
request(options: any): Promise<any>;
|
||||
}
|
||||
|
||||
const WebDavHttp = registerPlugin<WebDavHttpPlugin>('WebDavHttp');
|
||||
|
||||
export interface WebDavHttpRequest {
|
||||
url: string;
|
||||
method: string;
|
||||
|
|
@ -24,6 +31,15 @@ export interface WebDavHttpResponse {
|
|||
|
||||
export class WebDavHttpAdapter {
|
||||
private static readonly L = 'WebDavHttpAdapter';
|
||||
private static readonly WEBDAV_METHODS = [
|
||||
'PROPFIND',
|
||||
'MKCOL',
|
||||
'MOVE',
|
||||
'COPY',
|
||||
'LOCK',
|
||||
'UNLOCK',
|
||||
'PROPPATCH',
|
||||
];
|
||||
|
||||
// Make IS_ANDROID_WEB_VIEW testable by making it a class property
|
||||
protected get isAndroidWebView(): boolean {
|
||||
|
|
@ -35,15 +51,33 @@ export class WebDavHttpAdapter {
|
|||
let response: WebDavHttpResponse;
|
||||
|
||||
if (this.isAndroidWebView) {
|
||||
// Use CapacitorHttp for Android WebView
|
||||
const capacitorResponse = await CapacitorHttp.request({
|
||||
url: options.url,
|
||||
method: options.method,
|
||||
headers: options.headers,
|
||||
data: options.body,
|
||||
});
|
||||
// Check if this is a WebDAV method
|
||||
const isWebDavMethod = WebDavHttpAdapter.WEBDAV_METHODS.includes(
|
||||
options.method.toUpperCase(),
|
||||
);
|
||||
|
||||
response = this._convertCapacitorResponse(capacitorResponse);
|
||||
if (isWebDavMethod) {
|
||||
// Use our custom WebDAV plugin for WebDAV methods
|
||||
PFLog.log(
|
||||
`${WebDavHttpAdapter.L}.request() using WebDavHttp for ${options.method}`,
|
||||
);
|
||||
const webdavResponse = await WebDavHttp.request({
|
||||
url: options.url,
|
||||
method: options.method,
|
||||
headers: options.headers,
|
||||
data: options.body,
|
||||
});
|
||||
response = this._convertWebDavResponse(webdavResponse);
|
||||
} else {
|
||||
// Use standard CapacitorHttp for regular HTTP methods
|
||||
const capacitorResponse = await CapacitorHttp.request({
|
||||
url: options.url,
|
||||
method: options.method,
|
||||
headers: options.headers,
|
||||
data: options.body,
|
||||
});
|
||||
response = this._convertCapacitorResponse(capacitorResponse);
|
||||
}
|
||||
} else {
|
||||
// Use fetch for other platforms
|
||||
const fetchResponse = await fetch(options.url, {
|
||||
|
|
@ -91,6 +125,14 @@ export class WebDavHttpAdapter {
|
|||
};
|
||||
}
|
||||
|
||||
private _convertWebDavResponse(response: any): WebDavHttpResponse {
|
||||
return {
|
||||
status: response.status,
|
||||
headers: response.headers || {},
|
||||
data: response.data || '',
|
||||
};
|
||||
}
|
||||
|
||||
private async _convertFetchResponse(response: Response): Promise<WebDavHttpResponse> {
|
||||
const headers: Record<string, string> = {};
|
||||
response.headers.forEach((value, key) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue