diff --git a/android/capacitor/src/main/java/com/getcapacitor/Bridge.java b/android/capacitor/src/main/java/com/getcapacitor/Bridge.java
index 72ecfcbc6c..4b219ae032 100644
--- a/android/capacitor/src/main/java/com/getcapacitor/Bridge.java
+++ b/android/capacitor/src/main/java/com/getcapacitor/Bridge.java
@@ -24,9 +24,10 @@
import com.getcapacitor.plugin.Camera;
import com.getcapacitor.plugin.Clipboard;
import com.getcapacitor.plugin.Device;
-import com.getcapacitor.plugin.Filesystem;
+import com.getcapacitor.plugin.filesystem.Filesystem;
import com.getcapacitor.plugin.Geolocation;
import com.getcapacitor.plugin.Haptics;
+import com.getcapacitor.plugin.http.Http;
import com.getcapacitor.plugin.Keyboard;
import com.getcapacitor.plugin.LocalNotifications;
import com.getcapacitor.plugin.Modals;
@@ -401,6 +402,7 @@ private void registerAllPlugins() {
this.registerPlugin(Filesystem.class);
this.registerPlugin(Geolocation.class);
this.registerPlugin(Haptics.class);
+ this.registerPlugin(Http.class);
this.registerPlugin(Keyboard.class);
this.registerPlugin(Modals.class);
this.registerPlugin(Network.class);
diff --git a/android/capacitor/src/main/java/com/getcapacitor/PluginCall.java b/android/capacitor/src/main/java/com/getcapacitor/PluginCall.java
index d3bbac9393..d46a35e0cc 100644
--- a/android/capacitor/src/main/java/com/getcapacitor/PluginCall.java
+++ b/android/capacitor/src/main/java/com/getcapacitor/PluginCall.java
@@ -83,7 +83,7 @@ public void errorCallback(String msg) {
this.msgHandler.sendResponseMessage(this, null, errorResult);
}
- public void error(String msg, Exception ex) {
+ public void error(String msg, Exception ex, JSObject data) {
PluginResult errorResult = new PluginResult();
if(ex != null) {
@@ -92,6 +92,9 @@ public void error(String msg, Exception ex) {
try {
errorResult.put("message", msg);
+ if (ex != null) {
+ errorResult.put("platformMessage", ex.getMessage());
+ }
} catch (Exception jsonEx) {
Log.e(LogUtils.getPluginTag(), jsonEx.getMessage());
}
@@ -99,12 +102,20 @@ public void error(String msg, Exception ex) {
this.msgHandler.sendResponseMessage(this, null, errorResult);
}
+ public void error(String msg, Exception ex) {
+ error(msg, ex, null);
+ }
+
public void error(String msg) {
- error(msg, null);
+ error(msg, null, null);
+ }
+
+ public void reject(String msg, Exception ex, JSObject data) {
+ error(msg, ex, data);
}
public void reject(String msg, Exception ex) {
- error(msg, ex);
+ error(msg, ex, null);
}
public void reject(String msg) {
diff --git a/android/capacitor/src/main/java/com/getcapacitor/PluginRequestCodes.java b/android/capacitor/src/main/java/com/getcapacitor/PluginRequestCodes.java
index f3ca5ebae3..b0999e99c7 100644
--- a/android/capacitor/src/main/java/com/getcapacitor/PluginRequestCodes.java
+++ b/android/capacitor/src/main/java/com/getcapacitor/PluginRequestCodes.java
@@ -23,4 +23,6 @@ public class PluginRequestCodes {
public static final int FILESYSTEM_REQUEST_STAT_PERMISSIONS = 9019;
public static final int FILESYSTEM_REQUEST_RENAME_PERMISSIONS = 9020;
public static final int FILESYSTEM_REQUEST_COPY_PERMISSIONS = 9021;
+ public static final int HTTP_REQUEST_DOWNLOAD_WRITE_PERMISSIONS = 9022;
+ public static final int HTTP_REQUEST_UPLOAD_READ_PERMISSIONS = 9023;
}
diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/Filesystem.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/filesystem/Filesystem.java
similarity index 89%
rename from android/capacitor/src/main/java/com/getcapacitor/plugin/Filesystem.java
rename to android/capacitor/src/main/java/com/getcapacitor/plugin/filesystem/Filesystem.java
index 5b99f5cb2f..fda96bf488 100644
--- a/android/capacitor/src/main/java/com/getcapacitor/plugin/Filesystem.java
+++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/filesystem/Filesystem.java
@@ -1,4 +1,4 @@
-package com.getcapacitor.plugin;
+package com.getcapacitor.plugin.filesystem;
import android.Manifest;
import android.content.Context;
@@ -62,46 +62,6 @@ private Charset getEncoding(String encoding) {
return null;
}
- private File getDirectory(String directory) {
- Context c = bridge.getContext();
- switch(directory) {
- case "APPLICATION":
- return c.getFilesDir();
- case "DOCUMENTS":
- return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
- case "DATA":
- return c.getFilesDir();
- case "CACHE":
- return c.getCacheDir();
- case "EXTERNAL":
- return c.getExternalFilesDir(null);
- case "EXTERNAL_STORAGE":
- return Environment.getExternalStorageDirectory();
- }
- return null;
- }
-
- private File getFileObject(String path, String directory) {
- if (directory == null) {
- Uri u = Uri.parse(path);
- if (u.getScheme() == null || u.getScheme().equals("file")) {
- return new File(u.getPath());
- }
- }
-
- File androidDirectory = this.getDirectory(directory);
-
- if (androidDirectory == null) {
- return null;
- } else {
- if(!androidDirectory.exists()) {
- androidDirectory.mkdir();
- }
- }
-
- return new File(androidDirectory, path);
- }
-
private InputStream getInputStream(String path, String directory) throws IOException {
if (directory == null) {
Uri u = Uri.parse(path);
@@ -112,7 +72,7 @@ private InputStream getInputStream(String path, String directory) throws IOExcep
}
}
- File androidDirectory = this.getDirectory(directory);
+ File androidDirectory = FilesystemUtils.getDirectory(getContext(), directory);
if (androidDirectory == null) {
throw new IOException("Directory not found");
@@ -163,7 +123,7 @@ public void readFile(PluginCall call) {
return;
}
- if (!isPublicDirectory(directory)
+ if (!FilesystemUtils.isPublicDirectory(directory)
|| isStoragePermissionGranted(PluginRequestCodes.FILESYSTEM_REQUEST_READ_FILE_PERMISSIONS, Manifest.permission.READ_EXTERNAL_STORAGE)) {
try {
InputStream is = getInputStream(file, directory);
@@ -208,10 +168,10 @@ public void writeFile(PluginCall call) {
String directory = getDirectoryParameter(call);
if (directory != null) {
- if (!isPublicDirectory(directory)
+ if (!FilesystemUtils.isPublicDirectory(directory)
|| isStoragePermissionGranted(PluginRequestCodes.FILESYSTEM_REQUEST_WRITE_FILE_PERMISSIONS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
// create directory because it might not exist
- File androidDir = getDirectory(directory);
+ File androidDir = FilesystemUtils.getDirectory(getContext(), directory);
if (androidDir != null) {
if (androidDir.exists() || androidDir.mkdirs()) {
// path might include directories as well
@@ -283,7 +243,7 @@ private void saveFile(PluginCall call, File file, String data) {
if (success) {
// update mediaStore index only if file was written to external storage
- if (isPublicDirectory(getDirectoryParameter(call))) {
+ if (FilesystemUtils.isPublicDirectory(getDirectoryParameter(call))) {
MediaScannerConnection.scanFile(getContext(), new String[] {file.getAbsolutePath()}, null, null);
}
Log.d(getLogTag(), "File '" + file.getAbsolutePath() + "' saved!");
@@ -310,9 +270,9 @@ public void deleteFile(PluginCall call) {
String file = call.getString("path");
String directory = getDirectoryParameter(call);
- File fileObject = getFileObject(file, directory);
+ File fileObject = FilesystemUtils.getFileObject(getContext(), file, directory);
- if (!isPublicDirectory(directory)
+ if (!FilesystemUtils.isPublicDirectory(directory)
|| isStoragePermissionGranted(PluginRequestCodes.FILESYSTEM_REQUEST_DELETE_FILE_PERMISSIONS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
if (!fileObject.exists()) {
call.error("File does not exist");
@@ -335,14 +295,14 @@ public void mkdir(PluginCall call) {
String directory = getDirectoryParameter(call);
boolean recursive = call.getBoolean("recursive", false).booleanValue();
- File fileObject = getFileObject(path, directory);
+ File fileObject = FilesystemUtils.getFileObject(getContext(), path, directory);
if (fileObject.exists()) {
call.error("Directory exists");
return;
}
- if (!isPublicDirectory(directory)
+ if (!FilesystemUtils.isPublicDirectory(directory)
|| isStoragePermissionGranted(PluginRequestCodes.FILESYSTEM_REQUEST_WRITE_FOLDER_PERMISSIONS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
boolean created = false;
if (recursive) {
@@ -365,9 +325,9 @@ public void rmdir(PluginCall call) {
String directory = getDirectoryParameter(call);
Boolean recursive = call.getBoolean("recursive", false);
- File fileObject = getFileObject(path, directory);
+ File fileObject = FilesystemUtils.getFileObject(getContext(), path, directory);
- if (!isPublicDirectory(directory)
+ if (!FilesystemUtils.isPublicDirectory(directory)
|| isStoragePermissionGranted(PluginRequestCodes.FILESYSTEM_REQUEST_DELETE_FOLDER_PERMISSIONS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
if (!fileObject.exists()) {
call.error("Directory does not exist");
@@ -401,9 +361,9 @@ public void readdir(PluginCall call) {
String path = call.getString("path");
String directory = getDirectoryParameter(call);
- File fileObject = getFileObject(path, directory);
+ File fileObject = FilesystemUtils.getFileObject(getContext(), path, directory);
- if (!isPublicDirectory(directory)
+ if (!FilesystemUtils.isPublicDirectory(directory)
|| isStoragePermissionGranted(PluginRequestCodes.FILESYSTEM_REQUEST_READ_FOLDER_PERMISSIONS, Manifest.permission.READ_EXTERNAL_STORAGE)) {
if (fileObject != null && fileObject.exists()) {
String[] files = fileObject.list();
@@ -423,9 +383,9 @@ public void getUri(PluginCall call) {
String path = call.getString("path");
String directory = getDirectoryParameter(call);
- File fileObject = getFileObject(path, directory);
+ File fileObject = FilesystemUtils.getFileObject(getContext(), path, directory);
- if (!isPublicDirectory(directory)
+ if (!FilesystemUtils.isPublicDirectory(directory)
|| isStoragePermissionGranted(PluginRequestCodes.FILESYSTEM_REQUEST_URI_PERMISSIONS, Manifest.permission.READ_EXTERNAL_STORAGE)) {
JSObject data = new JSObject();
data.put("uri", Uri.fromFile(fileObject).toString());
@@ -439,9 +399,9 @@ public void stat(PluginCall call) {
String path = call.getString("path");
String directory = getDirectoryParameter(call);
- File fileObject = getFileObject(path, directory);
+ File fileObject = FilesystemUtils.getFileObject(getContext(), path, directory);
- if (!isPublicDirectory(directory)
+ if (!FilesystemUtils.isPublicDirectory(directory)
|| isStoragePermissionGranted(PluginRequestCodes.FILESYSTEM_REQUEST_STAT_PERMISSIONS, Manifest.permission.READ_EXTERNAL_STORAGE)) {
if (!fileObject.exists()) {
call.error("File does not exist");
@@ -525,8 +485,8 @@ private void _copy(PluginCall call, boolean doRename) {
return;
}
- File fromObject = getFileObject(from, directory);
- File toObject = getFileObject(to, toDirectory);
+ File fromObject = FilesystemUtils.getFileObject(getContext(), from, directory);
+ File toObject = FilesystemUtils.getFileObject(getContext(), to, toDirectory);
assert fromObject != null;
assert toObject != null;
@@ -551,7 +511,7 @@ private void _copy(PluginCall call, boolean doRename) {
return;
}
- if (isPublicDirectory(directory) || isPublicDirectory(toDirectory)) {
+ if (FilesystemUtils.isPublicDirectory(directory) || FilesystemUtils.isPublicDirectory(toDirectory)) {
if (doRename) {
if (!isStoragePermissionGranted(PluginRequestCodes.FILESYSTEM_REQUEST_RENAME_PERMISSIONS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
return;
@@ -626,13 +586,7 @@ private String getDirectoryParameter(PluginCall call) {
return call.getString("directory");
}
- /**
- * True if the given directory string is a public storage directory, which is accessible by the user or other apps.
- * @param directory the directory string.
- */
- private boolean isPublicDirectory(String directory) {
- return "DOCUMENTS".equals(directory) || "EXTERNAL_STORAGE".equals(directory);
- }
+
@Override
protected void handleRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/filesystem/FilesystemUtils.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/filesystem/FilesystemUtils.java
new file mode 100644
index 0000000000..0978b605c5
--- /dev/null
+++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/filesystem/FilesystemUtils.java
@@ -0,0 +1,68 @@
+package com.getcapacitor.plugin.filesystem;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Environment;
+
+import java.io.File;
+
+public class FilesystemUtils {
+ public static final String DIRECTORY_DOCUMENTS = "DOCUMENTS";
+ public static final String DIRECTORY_APPLICATION = "APPLICATION";
+ public static final String DIRECTORY_DOWNLOADS = "DOWNLOADS";
+ public static final String DIRECTORY_DATA = "DATA";
+ public static final String DIRECTORY_CACHE = "CACHE";
+ public static final String DIRECTORY_EXTERNAL = "EXTERNAL";
+ public static final String DIRECTORY_EXTERNAL_STORAGE = "EXTERNAL_STORAGE";
+
+ public static File getFileObject(Context c, String path, String directory) {
+ if (directory == null) {
+ Uri u = Uri.parse(path);
+ if (u.getScheme() == null || u.getScheme().equals("file")) {
+ return new File(u.getPath());
+ }
+ }
+
+ File androidDirectory = FilesystemUtils.getDirectory(c, directory);
+
+ if (androidDirectory == null) {
+ return null;
+ } else {
+ if(!androidDirectory.exists()) {
+ androidDirectory.mkdir();
+ }
+ }
+
+ return new File(androidDirectory, path);
+ }
+
+ public static File getDirectory(Context c, String directory) {
+ switch(directory) {
+ case DIRECTORY_APPLICATION:
+ return c.getFilesDir();
+ case DIRECTORY_DOCUMENTS:
+ return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
+ case DIRECTORY_DOWNLOADS:
+ return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+ case DIRECTORY_DATA:
+ return c.getFilesDir();
+ case DIRECTORY_CACHE:
+ return c.getCacheDir();
+ case DIRECTORY_EXTERNAL:
+ return c.getExternalFilesDir(null);
+ case DIRECTORY_EXTERNAL_STORAGE:
+ return Environment.getExternalStorageDirectory();
+ }
+ return null;
+ }
+
+ /**
+ * True if the given directory string is a public storage directory, which is accessible by the user or other apps.
+ * @param directory the directory string.
+ */
+ public static boolean isPublicDirectory(String directory) {
+ return DIRECTORY_DOCUMENTS.equals(directory) ||
+ DIRECTORY_DOWNLOADS.equals(directory) ||
+ "EXTERNAL_STORAGE".equals(directory);
+ }
+}
diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/http/FormUploader.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/http/FormUploader.java
new file mode 100644
index 0000000000..3fce58f0db
--- /dev/null
+++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/http/FormUploader.java
@@ -0,0 +1,116 @@
+package com.getcapacitor.plugin.http;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.net.HttpURLConnection;
+import java.net.URLConnection;
+import java.util.UUID;
+
+public class FormUploader {
+ private final String boundary;
+ private static final String LINE_FEED = "\r\n";
+ private HttpURLConnection httpConn;
+ private String charset = "UTF-8";
+ private OutputStream outputStream;
+ private PrintWriter writer;
+
+ /**
+ * This constructor initializes a new HTTP POST request with content type
+ * is set to multipart/form-data
+ *
+ * @param conn
+ * @throws java.io.IOException
+ */
+ public FormUploader(HttpURLConnection conn) throws IOException {
+ UUID uuid = UUID.randomUUID();
+ boundary = uuid.toString();
+ httpConn = conn;
+
+ httpConn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
+
+ outputStream = httpConn.getOutputStream();
+ writer = new PrintWriter(new OutputStreamWriter(outputStream, charset), true);
+ }
+
+ /**
+ * Adds a form field to the request
+ *
+ * @param name field name
+ * @param value field value
+ */
+ public void addFormField(String name, String value) {
+ writer.append(LINE_FEED);
+ writer.append("--" + boundary).append(LINE_FEED);
+ writer.append("Content-Disposition: form-data; name=\"" + name + "\"")
+ .append(LINE_FEED);
+ writer.append("Content-Type: text/plain; charset=" + charset).append(
+ LINE_FEED);
+ writer.append(LINE_FEED);
+ writer.append(value);
+ writer.append(LINE_FEED).append("--" + boundary + "--").append(LINE_FEED);
+ writer.flush();
+ }
+
+ /**
+ * Adds a upload file section to the request
+ *
+ * @param fieldName name attribute in
+ * @param uploadFile a File to be uploaded
+ * @throws IOException
+ */
+ public void addFilePart(String fieldName, File uploadFile)
+ throws IOException {
+ String fileName = uploadFile.getName();
+ writer.append(LINE_FEED);
+ writer.append("--" + boundary).append(LINE_FEED);
+ writer.append(
+ "Content-Disposition: form-data; name=\"" + fieldName
+ + "\"; filename=\"" + fileName + "\"")
+ .append(LINE_FEED);
+ writer.append(
+ "Content-Type: "
+ + URLConnection.guessContentTypeFromName(fileName))
+ .append(LINE_FEED)
+ .append(LINE_FEED);
+ writer.flush();
+
+ FileInputStream inputStream = new FileInputStream(uploadFile);
+ byte[] buffer = new byte[4096];
+ int bytesRead;
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, bytesRead);
+ }
+ outputStream.flush();
+ inputStream.close();
+ writer.append(LINE_FEED).append("--" + boundary + "--").append(LINE_FEED);
+ writer.flush();
+ }
+
+ /**
+ * Adds a header field to the request.
+ *
+ * @param name - name of the header field
+ * @param value - value of the header field
+ */
+ public void addHeaderField(String name, String value) {
+ writer.append(name + ": " + value).append(LINE_FEED);
+ writer.flush();
+ }
+
+ /**
+ * Completes the request and receives response from the server.
+ *
+ * @return a list of Strings as response in case the server returned
+ * status OK, otherwise an exception is thrown.
+ * @throws IOException
+ */
+ public void finish() throws IOException {
+ writer.append(LINE_FEED).flush();
+ writer.append("--" + boundary + "--").append(LINE_FEED);
+ writer.close();
+ }
+}
diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java
new file mode 100644
index 0000000000..fb97d2e427
--- /dev/null
+++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/http/Http.java
@@ -0,0 +1,464 @@
+package com.getcapacitor.plugin.http;
+
+import android.Manifest;
+import android.content.pm.PackageManager;
+import android.util.Log;
+
+import com.getcapacitor.JSArray;
+import com.getcapacitor.JSObject;
+import com.getcapacitor.NativePlugin;
+import com.getcapacitor.Plugin;
+import com.getcapacitor.PluginCall;
+import com.getcapacitor.PluginMethod;
+import com.getcapacitor.PluginRequestCodes;
+import com.getcapacitor.plugin.filesystem.FilesystemUtils;
+
+import org.json.JSONException;
+
+import java.io.BufferedReader;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.HttpCookie;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Haptic engine plugin, also handles vibration.
+ *
+ * Requires the android.permission.VIBRATE permission.
+ */
+@NativePlugin(requestCodes = {
+ PluginRequestCodes.HTTP_REQUEST_DOWNLOAD_WRITE_PERMISSIONS,
+ PluginRequestCodes.HTTP_REQUEST_UPLOAD_READ_PERMISSIONS,
+})
+public class Http extends Plugin {
+ CookieManager cookieManager = new CookieManager();
+
+ @Override
+ public void load() {
+ CookieHandler.setDefault(cookieManager);
+ }
+
+ @PluginMethod()
+ public void request(PluginCall call) {
+ String url = call.getString("url");
+ String method = call.getString("method");
+ JSObject headers = call.getObject("headers");
+ JSObject params = call.getObject("params");
+
+ switch (method) {
+ case "GET":
+ case "HEAD":
+ get(call, url, method, headers, params);
+ return;
+ case "DELETE":
+ case "PATCH":
+ case "POST":
+ case "PUT":
+ mutate(call, url, method, headers);
+ return;
+ }
+ }
+
+ private void get(PluginCall call, String urlString, String method, JSObject headers, JSObject params) {
+ try {
+ Integer connectTimeout = call.getInt("connectTimeout");
+ Integer readTimeout = call.getInt("readTimeout");
+
+ URL url = new URL(urlString);
+
+ HttpURLConnection conn = makeUrlConnection(url, method, connectTimeout, readTimeout, headers);
+
+ buildResponse(call, conn);
+ } catch (MalformedURLException ex) {
+ call.reject("Invalid URL", ex);
+ } catch (IOException ex) {
+ call.reject("Error", ex);
+ } catch (Exception ex) {
+ call.reject("Error", ex);
+ }
+ }
+
+
+ private void mutate(PluginCall call, String urlString, String method, JSObject headers) {
+ try {
+ Integer connectTimeout = call.getInt("connectTimeout");
+ Integer readTimeout = call.getInt("readTimeout");
+ JSObject data = call.getObject("data");
+
+ URL url = new URL(urlString);
+
+ HttpURLConnection conn = makeUrlConnection(url, method, connectTimeout, readTimeout, headers);
+
+ conn.setDoOutput(true);
+
+ setRequestBody(conn, data, headers);
+
+ conn.connect();
+
+ buildResponse(call, conn);
+ } catch (MalformedURLException ex) {
+ call.reject("Invalid URL", ex);
+ } catch (IOException ex) {
+ call.reject("Error", ex);
+ } catch (Exception ex) {
+ call.reject("Error", ex);
+ }
+ }
+
+ private HttpURLConnection makeUrlConnection(URL url, String method, Integer connectTimeout, Integer readTimeout, JSObject headers) throws Exception {
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+
+ conn.setAllowUserInteraction(false);
+ conn.setRequestMethod(method);
+
+ if (connectTimeout != null) {
+ conn.setConnectTimeout(connectTimeout);
+ }
+
+ if (readTimeout != null) {
+ conn.setReadTimeout(readTimeout);
+ }
+
+ setRequestHeaders(conn, headers);
+
+ return conn;
+ }
+
+ @SuppressWarnings("unused")
+ @PluginMethod()
+ public void downloadFile(PluginCall call) {
+ try {
+ saveCall(call);
+ String urlString = call.getString("url");
+ String filePath = call.getString("filePath");
+ String fileDirectory = call.getString("fileDirectory", FilesystemUtils.DIRECTORY_DOCUMENTS);
+ JSObject headers = call.getObject("headers");
+
+ Integer connectTimeout = call.getInt("connectTimeout");
+ Integer readTimeout = call.getInt("readTimeout");
+
+ URL url = new URL(urlString);
+
+ if (!FilesystemUtils.isPublicDirectory(fileDirectory)
+ || isStoragePermissionGranted(PluginRequestCodes.HTTP_REQUEST_DOWNLOAD_WRITE_PERMISSIONS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+ this.freeSavedCall();
+
+ File file = FilesystemUtils.getFileObject(getContext(), filePath, fileDirectory);
+
+ HttpURLConnection conn = makeUrlConnection(url, "GET", connectTimeout, readTimeout, headers);
+
+ InputStream is = conn.getInputStream();
+
+ FileOutputStream fos = new FileOutputStream(file, false);
+
+ byte[] buffer = new byte[1024];
+ int len;
+
+ while ((len = is.read(buffer)) > 0) {
+ fos.write(buffer, 0, len);
+ }
+
+ is.close();
+ fos.close();
+
+ call.resolve(new JSObject() {{
+ put("path", file.getAbsolutePath());
+ }});
+ }
+ } catch (MalformedURLException ex) {
+ call.reject("Invalid URL", ex);
+ } catch (IOException ex) {
+ call.reject("Error", ex);
+ } catch (Exception ex) {
+ call.reject("Error", ex);
+ }
+ }
+
+ private boolean isStoragePermissionGranted(int permissionRequestCode, String permission) {
+ if (hasPermission(permission)) {
+ Log.v(getLogTag(),"Permission '" + permission + "' is granted");
+ return true;
+ } else {
+ Log.v(getLogTag(),"Permission '" + permission + "' denied. Asking user for it.");
+ pluginRequestPermissions(new String[] {permission}, permissionRequestCode);
+ return false;
+ }
+ }
+
+ @Override
+ protected void handleRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ super.handleRequestPermissionsResult(requestCode, permissions, grantResults);
+
+ if (getSavedCall() == null) {
+ Log.d(getLogTag(),"No stored plugin call for permissions request result");
+ return;
+ }
+
+ PluginCall savedCall = getSavedCall();
+
+ for (int i = 0; i < grantResults.length; i++) {
+ int result = grantResults[i];
+ String perm = permissions[i];
+ if(result == PackageManager.PERMISSION_DENIED) {
+ Log.d(getLogTag(), "User denied storage permission: " + perm);
+ savedCall.error("User denied write permission needed to save files");
+ this.freeSavedCall();
+ return;
+ }
+ }
+
+ this.freeSavedCall();
+
+ // Run on background thread to avoid main-thread network requests
+ final Http httpPlugin = this;
+ bridge.execute(new Runnable() {
+ @Override
+ public void run() {
+ if (requestCode == PluginRequestCodes.HTTP_REQUEST_DOWNLOAD_WRITE_PERMISSIONS) {
+ httpPlugin.downloadFile(savedCall);
+ } else if (requestCode == PluginRequestCodes.HTTP_REQUEST_UPLOAD_READ_PERMISSIONS) {
+ httpPlugin.uploadFile(savedCall);
+ }
+ }
+ });
+ }
+
+
+ @SuppressWarnings("unused")
+ @PluginMethod()
+ public void uploadFile(PluginCall call) {
+ String urlString = call.getString("url");
+ String filePath = call.getString("filePath");
+ String fileDirectory = call.getString("fileDirectory", FilesystemUtils.DIRECTORY_DOCUMENTS);
+ String name = call.getString("name", "file");
+ Integer connectTimeout = call.getInt("connectTimeout");
+ Integer readTimeout = call.getInt("readTimeout");
+ JSObject headers = call.getObject("headers");
+ JSObject params = call.getObject("params");
+ JSObject data = call.getObject("data");
+
+ try {
+ saveCall(call);
+ URL url = new URL(urlString);
+
+ if (!FilesystemUtils.isPublicDirectory(fileDirectory)
+ || isStoragePermissionGranted(PluginRequestCodes.HTTP_REQUEST_UPLOAD_READ_PERMISSIONS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+ this.freeSavedCall();
+ File file = FilesystemUtils.getFileObject(getContext(), filePath, fileDirectory);
+
+ HttpURLConnection conn = makeUrlConnection(url, "POST", connectTimeout, readTimeout, headers);
+ conn.setDoOutput(true);
+
+ FormUploader builder = new FormUploader(conn);
+ builder.addFilePart(name, file);
+ builder.finish();
+
+ buildResponse(call, conn);
+ }
+ } catch (Exception ex) {
+ call.reject("Error", ex);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @PluginMethod()
+ public void setCookie(PluginCall call) {
+ String url = call.getString("url");
+ String key = call.getString("key");
+ String value = call.getString("value");
+
+ URI uri = getUri(url);
+ if (uri == null) {
+ call.reject("Invalid URL");
+ return;
+ }
+
+ cookieManager.getCookieStore().add(uri, new HttpCookie(key, value));
+
+ call.resolve();
+ }
+
+ @SuppressWarnings("unused")
+ @PluginMethod()
+ public void getCookies(PluginCall call) {
+ String url = call.getString("url");
+
+ URI uri = getUri(url);
+ if (uri == null) {
+ call.reject("Invalid URL");
+ return;
+ }
+
+ List cookies = cookieManager.getCookieStore().get(uri);
+
+ JSArray cookiesArray = new JSArray();
+
+ for (HttpCookie cookie : cookies) {
+ JSObject ret = new JSObject();
+ ret.put("key", cookie.getName());
+ ret.put("value", cookie.getValue());
+ cookiesArray.put(ret);
+ }
+
+ JSObject ret = new JSObject();
+ ret.put("value", cookiesArray);
+ call.resolve(ret);
+ }
+
+ @SuppressWarnings("unused")
+ @PluginMethod()
+ public void deleteCookie(PluginCall call) {
+ String url = call.getString("url");
+ String key = call.getString("key");
+
+ URI uri = getUri(url);
+ if (uri == null) {
+ call.reject("Invalid URL");
+ return;
+ }
+
+
+ List cookies = cookieManager.getCookieStore().get(uri);
+
+ for (HttpCookie cookie : cookies) {
+ if (cookie.getName().equals(key)) {
+ cookieManager.getCookieStore().remove(uri, cookie);
+ }
+ }
+
+ call.resolve();
+ }
+
+ @SuppressWarnings("unused")
+ @PluginMethod()
+ public void clearCookies(PluginCall call) {
+ cookieManager.getCookieStore().removeAll();
+ call.resolve();
+ }
+
+ private void buildResponse(PluginCall call, HttpURLConnection conn) throws Exception {
+ int statusCode = conn.getResponseCode();
+
+ JSObject ret = new JSObject();
+ ret.put("status", statusCode);
+ ret.put("headers", makeResponseHeaders(conn));
+
+ InputStream stream = conn.getInputStream();
+
+ BufferedReader in = new BufferedReader(new InputStreamReader(stream));
+ StringBuilder builder = new StringBuilder();
+ String line;
+ while ((line = in.readLine()) != null) {
+ builder.append(line);
+ }
+ in.close();
+
+ Log.d(getLogTag(), "GET request completed, got data");
+
+ String contentType = conn.getHeaderField("Content-Type");
+
+ if (contentType != null) {
+ if (contentType.contains("application/json")) {
+ JSObject jsonValue = new JSObject(builder.toString());
+ ret.put("data", jsonValue);
+ } else {
+ ret.put("data", builder.toString());
+ }
+ } else {
+ ret.put("data", builder.toString());
+ }
+
+ call.resolve(ret);
+ }
+
+ private JSArray makeResponseHeaders(HttpURLConnection conn) {
+ JSArray ret = new JSArray();
+
+ for (Map.Entry> entries : conn.getHeaderFields().entrySet()) {
+ JSObject header = new JSObject();
+
+ String val = "";
+ for (String headerVal : entries.getValue()) {
+ val += headerVal + ", ";
+ }
+
+ header.put(entries.getKey(), val);
+ ret.put(header);
+ }
+
+ return ret;
+ }
+
+ private void setRequestHeaders(HttpURLConnection conn, JSObject headers) {
+ Iterator keys = headers.keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ String value = headers.getString(key);
+ conn.setRequestProperty(key, value);
+ }
+ }
+
+ private void setRequestBody(HttpURLConnection conn, JSObject data, JSObject headers) throws IOException, JSONException {
+ String contentType = conn.getRequestProperty("Content-Type");
+
+ if (contentType != null) {
+ if (contentType.contains("application/json")) {
+ DataOutputStream os = new DataOutputStream(conn.getOutputStream());
+ os.writeBytes(data.toString());
+ os.flush();
+ os.close();
+ } else if (contentType.contains("application/x-www-form-urlencoded")) {
+
+ StringBuilder builder = new StringBuilder();
+
+ Iterator keys = data.keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ Object d = data.get(key);
+ if (d != null) {
+ builder.append(key + "=" + URLEncoder.encode(d.toString(), "UTF-8"));
+ if (keys.hasNext()) {
+ builder.append("&");
+ }
+ }
+ }
+
+ DataOutputStream os = new DataOutputStream(conn.getOutputStream());
+ os.writeBytes(builder.toString());
+ os.flush();
+ os.close();
+ } else if (contentType.contains("multipart/form-data")) {
+ FormUploader uploader = new FormUploader(conn);
+
+ Iterator keys = data.keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+
+ String d = data.get(key).toString();
+ uploader.addFormField(key, d);
+ }
+ uploader.finish();
+ }
+ }
+ }
+ private URI getUri(String url) {
+ try {
+ return new URI(url);
+ } catch (Exception ex) {
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/src/core-plugin-definitions.ts b/core/src/core-plugin-definitions.ts
index 6ae95be5e9..c79f627b10 100644
--- a/core/src/core-plugin-definitions.ts
+++ b/core/src/core-plugin-definitions.ts
@@ -1,5 +1,11 @@
import { Plugin, PluginListenerHandle } from './definitions';
+import { HttpPlugin } from './plugins/http';
+import { FilesystemPlugin } from './plugins/fs';
+
+export * from './plugins/http';
+export * from './plugins/fs';
+
export interface PluginRegistry {
Accessibility: AccessibilityPlugin;
App: AppPlugin;
@@ -11,6 +17,7 @@ export interface PluginRegistry {
Filesystem: FilesystemPlugin;
Geolocation: GeolocationPlugin;
Haptics: HapticsPlugin;
+ Http: HttpPlugin;
Keyboard: KeyboardPlugin;
LocalNotifications: LocalNotificationsPlugin;
Modals: ModalsPlugin;
@@ -475,317 +482,6 @@ export interface DeviceBatteryInfo {
export interface DeviceLanguageCodeResult {
value: string;
}
-//
-
-export interface FilesystemPlugin extends Plugin {
- /**
- * Read a file from disk
- * @param options options for the file read
- * @return a promise that resolves with the read file data result
- */
- readFile(options: FileReadOptions): Promise;
-
- /**
- * Write a file to disk in the specified location on device
- * @param options options for the file write
- * @return a promise that resolves with the file write result
- */
- writeFile(options: FileWriteOptions): Promise;
-
- /**
- * Append to a file on disk in the specified location on device
- * @param options options for the file append
- * @return a promise that resolves with the file write result
- */
- appendFile(options: FileAppendOptions): Promise;
-
- /**
- * Delete a file from disk
- * @param options options for the file delete
- * @return a promise that resolves with the deleted file data result
- */
- deleteFile(options: FileDeleteOptions): Promise;
-
- /**
- * Create a directory.
- * @param options options for the mkdir
- * @return a promise that resolves with the mkdir result
- */
- mkdir(options: MkdirOptions): Promise;
-
- /**
- * Remove a directory
- * @param options the options for the directory remove
- */
- rmdir(options: RmdirOptions): Promise;
-
- /**
- * Return a list of files from the directory (not recursive)
- * @param options the options for the readdir operation
- * @return a promise that resolves with the readdir directory listing result
- */
- readdir(options: ReaddirOptions): Promise;
-
- /**
- * Return full File URI for a path and directory
- * @param options the options for the stat operation
- * @return a promise that resolves with the file stat result
- */
- getUri(options: GetUriOptions): Promise;
-
- /**
- * Return data about a file
- * @param options the options for the stat operation
- * @return a promise that resolves with the file stat result
- */
- stat(options: StatOptions): Promise;
-
- /**
- * Rename a file or directory
- * @param options the options for the rename operation
- * @return a promise that resolves with the rename result
- */
- rename(options: RenameOptions): Promise;
-
- /**
- * Copy a file or directory
- * @param options the options for the copy operation
- * @return a promise that resolves with the copy result
- */
- copy(options: CopyOptions): Promise;
-}
-
-export enum FilesystemDirectory {
- /**
- * The Application directory
- */
- Application = 'APPLICATION',
- /**
- * The Documents directory
- */
- Documents = 'DOCUMENTS',
- /**
- * The Data directory
- */
- Data = 'DATA',
- /**
- * The Cache directory
- */
- Cache = 'CACHE',
- /**
- * The external directory (Android only)
- */
- External = 'EXTERNAL',
- /**
- * The external storage directory (Android only)
- */
- ExternalStorage = 'EXTERNAL_STORAGE'
-}
-
-export enum FilesystemEncoding {
- UTF8 = 'utf8',
- ASCII = 'ascii',
- UTF16 = 'utf16'
-}
-
-export interface FileWriteOptions {
- /**
- * The filename to write
- */
- path: string;
- /**
- * The data to write
- */
- data: string;
- /**
- * The FilesystemDirectory to store the file in
- */
- directory?: FilesystemDirectory;
- /**
- * The encoding to write the file in. If not provided, data
- * is written as base64 encoded data.
- *
- * Pass FilesystemEncoding.UTF8 to write data as string
- */
- encoding?: FilesystemEncoding;
- /**
- * Whether to create any missing parent directories.
- * Defaults to false
- */
- recursive?: boolean;
-}
-
-export interface FileAppendOptions {
- /**
- * The filename to write
- */
- path: string;
- /**
- * The data to write
- */
- data: string;
- /**
- * The FilesystemDirectory to store the file in
- */
- directory?: FilesystemDirectory;
- /**
- * The encoding to write the file in. If not provided, data
- * is written as base64 encoded data.
- *
- * Pass FilesystemEncoding.UTF8 to write data as string
- */
- encoding?: FilesystemEncoding;
-}
-
-export interface FileReadOptions {
- /**
- * The filename to read
- */
- path: string;
- /**
- * The FilesystemDirectory to read the file from
- */
- directory?: FilesystemDirectory;
- /**
- * The encoding to read the file in, if not provided, data
- * is read as binary and returned as base64 encoded data.
- *
- * Pass FilesystemEncoding.UTF8 to read data as string
- */
- encoding?: FilesystemEncoding;
-}
-
-export interface FileDeleteOptions {
- /**
- * The filename to delete
- */
- path: string;
- /**
- * The FilesystemDirectory to delete the file from
- */
- directory?: FilesystemDirectory;
-}
-
-export interface MkdirOptions {
- /**
- * The path of the new directory
- */
- path: string;
- /**
- * The FilesystemDirectory to make the new directory in
- */
- directory?: FilesystemDirectory;
- /**
- * Whether to create any missing parent directories as well.
- * Defaults to false
- */
- recursive?: boolean;
-}
-
-export interface RmdirOptions {
- /**
- * The path of the directory to remove
- */
- path: string;
- /**
- * The FilesystemDirectory to remove the directory from
- */
- directory?: FilesystemDirectory;
- /**
- * Whether to recursively remove the contents of the directory
- * Defaults to false
- */
- recursive?: boolean;
-}
-
-export interface ReaddirOptions {
- /**
- * The path of the directory to read
- */
- path: string;
- /**
- * The FilesystemDirectory to list files from
- */
- directory?: FilesystemDirectory;
-}
-
-export interface GetUriOptions {
- /**
- * The path of the file to get the URI for
- */
- path: string;
- /**
- * The FilesystemDirectory to get the file under
- */
- directory: FilesystemDirectory;
-}
-
-export interface StatOptions {
- /**
- * The path of the file to get data about
- */
- path: string;
- /**
- * The FilesystemDirectory to get the file under
- */
- directory?: FilesystemDirectory;
-}
-
-export interface CopyOptions {
- /**
- * The existing file or directory
- */
- from: string;
- /**
- * The destination file or directory
- */
- to: string;
- /**
- * The FilesystemDirectory containing the existing file or directory
- */
- directory?: FilesystemDirectory;
- /**
- * The FilesystemDirectory containing the destination file or directory. If not supplied will use the 'directory'
- * parameter as the destination
- */
- toDirectory?: FilesystemDirectory;
-}
-
-export interface RenameOptions extends CopyOptions {}
-
-export interface FileReadResult {
- data: string;
-}
-export interface FileDeleteResult {
-}
-export interface FileWriteResult {
- uri: string;
-}
-export interface FileAppendResult {
-}
-export interface MkdirResult {
-}
-export interface RmdirResult {
-}
-export interface RenameResult {
-}
-export interface CopyResult {
-}
-export interface ReaddirResult {
- files: string[];
-}
-export interface GetUriResult {
- uri: string;
-}
-export interface StatResult {
- type: string;
- size: number;
- ctime: number;
- mtime: number;
- uri: string;
-}
-
-//
export interface GeolocationPlugin extends Plugin {
/**
@@ -904,6 +600,8 @@ export enum HapticsNotificationType {
ERROR = 'ERROR'
}
+// Vibrate
+
export interface VibrateOptions {
duration?: number;
}
diff --git a/core/src/plugins/fs.ts b/core/src/plugins/fs.ts
new file mode 100644
index 0000000000..1d2e486eb8
--- /dev/null
+++ b/core/src/plugins/fs.ts
@@ -0,0 +1,313 @@
+import { Plugin } from '../definitions';
+
+export interface FilesystemPlugin extends Plugin {
+ /**
+ * Read a file from disk
+ * @param options options for the file read
+ * @return a promise that resolves with the read file data result
+ */
+ readFile(options: FileReadOptions): Promise;
+
+ /**
+ * Write a file to disk in the specified location on device
+ * @param options options for the file write
+ * @return a promise that resolves with the file write result
+ */
+ writeFile(options: FileWriteOptions): Promise;
+
+ /**
+ * Append to a file on disk in the specified location on device
+ * @param options options for the file append
+ * @return a promise that resolves with the file write result
+ */
+ appendFile(options: FileAppendOptions): Promise;
+
+ /**
+ * Delete a file from disk
+ * @param options options for the file delete
+ * @return a promise that resolves with the deleted file data result
+ */
+ deleteFile(options: FileDeleteOptions): Promise;
+
+ /**
+ * Create a directory.
+ * @param options options for the mkdir
+ * @return a promise that resolves with the mkdir result
+ */
+ mkdir(options: MkdirOptions): Promise;
+
+ /**
+ * Remove a directory
+ * @param options the options for the directory remove
+ */
+ rmdir(options: RmdirOptions): Promise;
+
+ /**
+ * Return a list of files from the directory (not recursive)
+ * @param options the options for the readdir operation
+ * @return a promise that resolves with the readdir directory listing result
+ */
+ readdir(options: ReaddirOptions): Promise;
+
+ /**
+ * Return full File URI for a path and directory
+ * @param options the options for the stat operation
+ * @return a promise that resolves with the file stat result
+ */
+ getUri(options: GetUriOptions): Promise;
+
+ /**
+ * Return data about a file
+ * @param options the options for the stat operation
+ * @return a promise that resolves with the file stat result
+ */
+ stat(options: StatOptions): Promise;
+
+ /**
+ * Rename a file or directory
+ * @param options the options for the rename operation
+ * @return a promise that resolves with the rename result
+ */
+ rename(options: RenameOptions): Promise;
+
+ /**
+ * Copy a file or directory
+ * @param options the options for the copy operation
+ * @return a promise that resolves with the copy result
+ */
+ copy(options: CopyOptions): Promise;
+}
+
+export enum FilesystemDirectory {
+ /**
+ * The Application directory
+ */
+ Application = 'APPLICATION',
+ /**
+ * The Documents directory
+ */
+ Documents = 'DOCUMENTS',
+ /**
+ * The Downloads directory
+ */
+ Downloads = 'DOWNLOADS',
+ /**
+ * The Data directory
+ */
+ Data = 'DATA',
+ /**
+ * The Cache directory
+ */
+ Cache = 'CACHE',
+ /**
+ * The external directory (Android only)
+ */
+ External = 'EXTERNAL',
+ /**
+ * The external storage directory (Android only)
+ */
+ ExternalStorage = 'EXTERNAL_STORAGE'
+}
+
+export enum FilesystemEncoding {
+ UTF8 = 'utf8',
+ ASCII = 'ascii',
+ UTF16 = 'utf16'
+}
+
+export interface FileWriteOptions {
+ /**
+ * The filename to write
+ */
+ path: string;
+ /**
+ * The data to write
+ */
+ data: string;
+ /**
+ * The FilesystemDirectory to store the file in
+ */
+ directory?: FilesystemDirectory;
+ /**
+ * The encoding to write the file in. If not provided, data
+ * is written as base64 encoded data.
+ *
+ * Pass FilesystemEncoding.UTF8 to write data as string
+ */
+ encoding?: FilesystemEncoding;
+ /**
+ * Whether to create any missing parent directories.
+ * Defaults to false
+ */
+ recursive?: boolean;
+}
+
+export interface FileAppendOptions {
+ /**
+ * The filename to write
+ */
+ path: string;
+ /**
+ * The data to write
+ */
+ data: string;
+ /**
+ * The FilesystemDirectory to store the file in
+ */
+ directory?: FilesystemDirectory;
+ /**
+ * The encoding to write the file in. If not provided, data
+ * is written as base64 encoded data.
+ *
+ * Pass FilesystemEncoding.UTF8 to write data as string
+ */
+ encoding?: FilesystemEncoding;
+}
+
+export interface FileReadOptions {
+ /**
+ * The filename to read
+ */
+ path: string;
+ /**
+ * The FilesystemDirectory to read the file from
+ */
+ directory?: FilesystemDirectory;
+ /**
+ * The encoding to read the file in, if not provided, data
+ * is read as binary and returned as base64 encoded data.
+ *
+ * Pass FilesystemEncoding.UTF8 to read data as string
+ */
+ encoding?: FilesystemEncoding;
+}
+
+export interface FileDeleteOptions {
+ /**
+ * The filename to delete
+ */
+ path: string;
+ /**
+ * The FilesystemDirectory to delete the file from
+ */
+ directory?: FilesystemDirectory;
+}
+
+export interface MkdirOptions {
+ /**
+ * The path of the new directory
+ */
+ path: string;
+ /**
+ * The FilesystemDirectory to make the new directory in
+ */
+ directory?: FilesystemDirectory;
+ /**
+ * Whether to create any missing parent directories as well.
+ * Defaults to false
+ */
+ recursive?: boolean;
+}
+
+export interface RmdirOptions {
+ /**
+ * The path of the directory to remove
+ */
+ path: string;
+ /**
+ * The FilesystemDirectory to remove the directory from
+ */
+ directory?: FilesystemDirectory;
+ /**
+ * Whether to recursively remove the contents of the directory
+ * Defaults to false
+ */
+ recursive?: boolean;
+}
+
+export interface ReaddirOptions {
+ /**
+ * The path of the directory to read
+ */
+ path: string;
+ /**
+ * The FilesystemDirectory to list files from
+ */
+ directory?: FilesystemDirectory;
+}
+
+export interface GetUriOptions {
+ /**
+ * The path of the file to get the URI for
+ */
+ path: string;
+ /**
+ * The FilesystemDirectory to get the file under
+ */
+ directory: FilesystemDirectory;
+}
+
+export interface StatOptions {
+ /**
+ * The path of the file to get data about
+ */
+ path: string;
+ /**
+ * The FilesystemDirectory to get the file under
+ */
+ directory?: FilesystemDirectory;
+}
+
+export interface CopyOptions {
+ /**
+ * The existing file or directory
+ */
+ from: string;
+ /**
+ * The destination file or directory
+ */
+ to: string;
+ /**
+ * The FilesystemDirectory containing the existing file or directory
+ */
+ directory?: FilesystemDirectory;
+ /**
+ * The FilesystemDirectory containing the destination file or directory. If not supplied will use the 'directory'
+ * parameter as the destination
+ */
+ toDirectory?: FilesystemDirectory;
+}
+
+export interface RenameOptions extends CopyOptions {}
+
+export interface FileReadResult {
+ data: string;
+}
+export interface FileDeleteResult {
+}
+export interface FileWriteResult {
+ uri: string;
+}
+export interface FileAppendResult {
+}
+export interface MkdirResult {
+}
+export interface RmdirResult {
+}
+export interface RenameResult {
+}
+export interface CopyResult {
+}
+export interface ReaddirResult {
+ files: string[];
+}
+export interface GetUriResult {
+ uri: string;
+}
+export interface StatResult {
+ type: string;
+ size: number;
+ ctime: number;
+ mtime: number;
+ uri: string;
+}
diff --git a/core/src/plugins/http.ts b/core/src/plugins/http.ts
new file mode 100644
index 0000000000..d179b40771
--- /dev/null
+++ b/core/src/plugins/http.ts
@@ -0,0 +1,122 @@
+import { FilesystemDirectory } from './fs';
+import { Plugin } from '../definitions';
+
+export interface HttpPlugin extends Plugin {
+ request(options: HttpOptions): Promise;
+ setCookie(options: HttpSetCookieOptions): Promise;
+ getCookies(options: HttpGetCookiesOptions): Promise;
+ deleteCookie(options: HttpDeleteCookieOptions): Promise;
+ clearCookies(options: HttpClearCookiesOptions): Promise;
+ uploadFile(options: HttpUploadFileOptions): Promise;
+ downloadFile(options: HttpDownloadFileOptions): Promise;
+}
+
+export interface HttpOptions {
+ url: string;
+ method?: string;
+ params?: HttpParams;
+ data?: any;
+ headers?: HttpHeaders;
+ /**
+ * How long to wait to read additional data. Resets each time new
+ * data is received
+ */
+ readTimeout?: number;
+ /**
+ * How long to wait for the initial connection.
+ */
+ connectTimeout?: number;
+ /**
+ * Extra arguments for fetch when running on the web
+ */
+ webFetchExtra?: RequestInit;
+}
+
+export interface HttpParams {
+ [key: string]: string;
+}
+
+export interface HttpHeaders {
+ [key: string]: string;
+}
+
+export interface HttpResponse {
+ data: any;
+ status: number;
+ headers: HttpHeaders;
+}
+
+export interface HttpDownloadFileOptions extends HttpOptions {
+ /**
+ * The path the downloaded file should be moved to
+ */
+ filePath: string;
+ /**
+ * Optionally, the directory to put the file in
+ *
+ * If this option is used, filePath can be a relative path rather than absolute
+ */
+ fileDirectory?: FilesystemDirectory;
+}
+
+export interface HttpUploadFileOptions extends HttpOptions {
+ /**
+ * The URL to upload the file to
+ */
+ url: string;
+ /**
+ * The field name to upload the file with
+ */
+ name: string;
+ /**
+ * For uploading a file on the web, a JavaScript Blob to upload
+ */
+ blob?: Blob;
+ /**
+ * For uploading a file natively, the path to the file on disk to upload
+ */
+ filePath?: string;
+ /**
+ * Optionally, the directory to look for the file in.
+ *
+ * If this option is used, filePath can be a relative path rather than absolute
+ */
+ fileDirectory?: FilesystemDirectory;
+}
+
+export interface HttpCookie {
+ key: string;
+ value: string;
+}
+
+export interface HttpSetCookieOptions {
+ url: string;
+ key: string;
+ value: string;
+ ageDays?: number;
+}
+
+export interface HttpGetCookiesOptions {
+ url: string;
+}
+
+export interface HttpDeleteCookieOptions {
+ url: string;
+ key: string;
+}
+
+export interface HttpClearCookiesOptions {
+ url: string;
+}
+
+export interface HttpGetCookiesResult {
+ value: HttpCookie[];
+}
+
+export interface HttpDownloadFileResult {
+ path?: string;
+ blob?: Blob;
+}
+
+export interface HttpUploadFileResult {
+}
\ No newline at end of file
diff --git a/core/src/web-plugins.ts b/core/src/web-plugins.ts
index 34d7e81d31..314b2b790f 100644
--- a/core/src/web-plugins.ts
+++ b/core/src/web-plugins.ts
@@ -9,6 +9,7 @@ export * from './web/clipboard';
export * from './web/filesystem';
export * from './web/geolocation';
export * from './web/device';
+export * from './web/http';
export * from './web/local-notifications';
export * from './web/share';
export * from './web/modals';
diff --git a/core/src/web/http.ts b/core/src/web/http.ts
new file mode 100644
index 0000000000..273c92e98e
--- /dev/null
+++ b/core/src/web/http.ts
@@ -0,0 +1,163 @@
+import { WebPlugin } from './index';
+
+import {
+ HttpPlugin,
+ HttpOptions,
+ //HttpCookie,
+ HttpDeleteCookieOptions,
+ HttpHeaders,
+ HttpResponse,
+ HttpSetCookieOptions,
+ HttpClearCookiesOptions,
+ HttpGetCookiesOptions,
+ HttpGetCookiesResult,
+ //HttpParams,
+ HttpDownloadFileOptions,
+ HttpDownloadFileResult,
+ HttpUploadFileOptions,
+ HttpUploadFileResult
+} from '../core-plugin-definitions';
+
+export class HttpPluginWeb extends WebPlugin implements HttpPlugin {
+ constructor() {
+ super({
+ name: 'Http',
+ platforms: ['web', 'electron']
+ });
+ }
+
+ private getRequestHeader(headers: HttpHeaders, key: string): string {
+ const originalKeys = Object.keys(headers);
+ const keys = Object.keys(headers).map(k => k.toLocaleLowerCase());
+ const lowered = keys.reduce((newHeaders, key, index) => {
+ newHeaders[key] = headers[originalKeys[index]];
+ return newHeaders;
+ }, {} as HttpHeaders);
+
+ return lowered[key.toLocaleLowerCase()];
+ }
+
+ private nativeHeadersToObject(headers: Headers): HttpHeaders {
+ const h = {} as HttpHeaders;
+
+ headers.forEach((value: string, key: string) => {
+ h[key] = value;
+ });
+
+ return h;
+ }
+
+ private makeFetchOptions(options: HttpOptions, fetchExtra: RequestInit): RequestInit {
+ const req = {
+ method: options.method || 'GET',
+ headers: options.headers,
+ ...(fetchExtra || {})
+ } as RequestInit;
+
+ const contentType = this.getRequestHeader(options.headers || {}, 'content-type') || '';
+
+ if (contentType.indexOf('application/json') === 0) {
+ req['body'] = JSON.stringify(options.data);
+ } else if (contentType.indexOf('application/x-www-form-urlencoded') === 0) {
+ } else if (contentType.indexOf('multipart/form-data') === 0) {
+ }
+
+ return req;
+ }
+
+ async request(options: HttpOptions): Promise {
+ const fetchOptions = this.makeFetchOptions(options, options.webFetchExtra);
+
+ const ret = await fetch(options.url, fetchOptions);
+
+ const contentType = ret.headers.get('content-type');
+
+ let data;
+ if (contentType && contentType.indexOf('application/json') === 0) {
+ data = await ret.json();
+ } else {
+ data = await ret.text();
+ }
+
+ return {
+ status: ret.status,
+ data,
+ headers: this.nativeHeadersToObject(ret.headers)
+ }
+ }
+
+ async setCookie(options: HttpSetCookieOptions) {
+ var expires = "";
+ if (options.ageDays) {
+ const date = new Date();
+ date.setTime(date.getTime() + (options.ageDays * 24 * 60 * 60 * 1000));
+ expires = "; expires=" + date.toUTCString();
+ }
+ document.cookie = options.key + "=" + (options.value || "") + expires + "; path=/";
+ }
+
+ async getCookies(_options: HttpGetCookiesOptions): Promise {
+ if (!document.cookie) {
+ return { value: [] }
+ }
+
+ var cookies = document.cookie.split(';');
+ return {
+ value: cookies.map(c => {
+ const cParts = c.split(';').map(cv => cv.trim());
+ const cNameValue = cParts[0];
+ const cValueParts = cNameValue.split('=');
+ const key = cValueParts[0];
+ const value = cValueParts[1];
+
+ return {
+ key,
+ value
+ }
+ })
+ }
+ }
+
+ async deleteCookie(options: HttpDeleteCookieOptions) {
+ document.cookie = options.key + '=; Max-Age=0'
+ }
+
+ async clearCookies(_options: HttpClearCookiesOptions) {
+ document.cookie
+ .split(";")
+ .forEach(c =>
+ document.cookie = c.replace(/^ +/, '')
+ .replace(/=.*/, `=;expires=${new Date().toUTCString()};path=/`));
+ }
+
+ async uploadFile(options: HttpUploadFileOptions): Promise {
+ const fetchOptions = this.makeFetchOptions(options, options.webFetchExtra);
+
+ const formData = new FormData();
+ formData.append(options.name, options.blob);
+
+ await fetch(options.url, {
+ ...fetchOptions,
+ body: formData,
+ method: 'POST'
+ });
+
+ return {};
+ }
+
+ async downloadFile(options: HttpDownloadFileOptions): Promise {
+ const fetchOptions = this.makeFetchOptions(options, options.webFetchExtra);
+
+ const ret = await fetch(options.url, fetchOptions);
+
+ const blob = await ret.blob();
+
+ return {
+ blob
+ }
+ }
+}
+
+const Http = new HttpPluginWeb();
+
+export { Http };
diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
index 75c05281f7..ef723c5d50 100644
--- a/example/android/app/src/main/AndroidManifest.xml
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -8,6 +8,7 @@ package="com.getcapacitor.myapp">
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
+ android:usesCleartextTraffic="true"
android:theme="@style/AppTheme">
= 2.1.2 < 3"
}
@@ -3088,10 +3176,9 @@
"integrity": "sha1-QLja9P16MRUL0AIWD2ZJbiKpjDw="
},
"ipaddr.js": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
- "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==",
- "dev": true
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
},
"is-accessor-descriptor": {
"version": "0.1.6",
@@ -3563,8 +3650,7 @@
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
- "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
- "dev": true
+ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"mem": {
"version": "1.1.0",
@@ -3606,14 +3692,12 @@
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
- "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
- "dev": true
+ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
- "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
- "dev": true
+ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
},
"micromatch": {
"version": "2.3.11",
@@ -3648,20 +3732,17 @@
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
- "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
- "dev": true
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": {
"version": "1.42.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz",
- "integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==",
- "dev": true
+ "integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ=="
},
"mime-types": {
"version": "2.1.25",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.25.tgz",
"integrity": "sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg==",
- "dev": true,
"requires": {
"mime-db": "1.42.0"
}
@@ -3737,6 +3818,21 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
+ "multer": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz",
+ "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==",
+ "requires": {
+ "append-field": "^1.0.0",
+ "busboy": "^0.2.11",
+ "concat-stream": "^1.5.2",
+ "mkdirp": "^0.5.1",
+ "object-assign": "^4.1.1",
+ "on-finished": "^2.3.0",
+ "type-is": "^1.6.4",
+ "xtend": "^4.0.0"
+ }
+ },
"nan": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
@@ -3780,8 +3876,7 @@
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
- "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
- "dev": true
+ "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"neo-async": {
"version": "2.6.1",
@@ -4001,8 +4096,7 @@
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
- "dev": true
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"object-copy": {
"version": "0.1.0",
@@ -4089,7 +4183,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
- "dev": true,
"requires": {
"ee-first": "1.1.1"
}
@@ -4223,8 +4316,7 @@
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
- "dev": true
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"pascalcase": {
"version": "0.1.1",
@@ -4382,13 +4474,12 @@
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"proxy-addr": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz",
- "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==",
- "dev": true,
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
+ "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
"requires": {
"forwarded": "~0.1.2",
- "ipaddr.js": "1.9.0"
+ "ipaddr.js": "1.9.1"
}
},
"proxy-middleware": {
@@ -4438,8 +4529,7 @@
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
- "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
- "dev": true
+ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"querystring": {
"version": "0.2.0",
@@ -4497,14 +4587,12 @@
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
- "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
- "dev": true
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
- "dev": true,
"requires": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
@@ -5044,8 +5132,7 @@
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "dev": true
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sass-graph": {
"version": "2.2.4",
@@ -5096,7 +5183,6 @@
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
- "dev": true,
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
@@ -5116,8 +5202,7 @@
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
- "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
- "dev": true
+ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
@@ -5125,7 +5210,6 @@
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
- "dev": true,
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
@@ -5174,8 +5258,7 @@
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
- "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==",
- "dev": true
+ "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"sha.js": {
"version": "2.4.11",
@@ -5426,8 +5509,7 @@
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
- "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
- "dev": true
+ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"stdout-stream": {
"version": "1.4.1",
@@ -5461,6 +5543,11 @@
"xtend": "^4.0.0"
}
},
+ "streamsearch": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
+ "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo="
+ },
"string-template": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz",
@@ -5666,8 +5753,7 @@
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
- "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
- "dev": true
+ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"tough-cookie": {
"version": "2.4.3",
@@ -5798,12 +5884,16 @@
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
- "dev": true,
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
}
},
+ "typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
+ },
"typescript": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.2.tgz",
@@ -5920,8 +6010,7 @@
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
- "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
- "dev": true
+ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"unset-value": {
"version": "1.0.0",
@@ -6042,8 +6131,7 @@
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
- "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
- "dev": true
+ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"uuid": {
"version": "3.3.3",
@@ -6064,8 +6152,7 @@
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
- "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
- "dev": true
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"verror": {
"version": "1.10.0",
@@ -6787,8 +6874,7 @@
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
- "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
- "dev": true
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
},
"y18n": {
"version": "3.2.1",
diff --git a/example/package.json b/example/package.json
index f2df3b3fb9..55446a0c41 100644
--- a/example/package.json
+++ b/example/package.json
@@ -25,8 +25,13 @@
"@angular/http": "5.0.1",
"@angular/platform-browser": "5.0.1",
"@angular/platform-browser-dynamic": "5.0.1",
+ "body-parser": "^1.19.0",
+ "cookie-parser": "^1.4.4",
+ "cors": "^2.8.5",
+ "express": "^4.17.1",
"ionic-angular": "^3.9.6",
"ionicons": "3.0.0",
+ "multer": "^1.4.2",
"rxjs": "5.5.2",
"sw-toolbox": "3.6.0",
"zone.js": "0.8.18"
diff --git a/example/server/document.pdf b/example/server/document.pdf
new file mode 100644
index 0000000000..f0d0e16d47
Binary files /dev/null and b/example/server/document.pdf differ
diff --git a/example/server/server.js b/example/server/server.js
new file mode 100644
index 0000000000..fef0fd56e2
--- /dev/null
+++ b/example/server/server.js
@@ -0,0 +1,127 @@
+var path = require('path'),
+ express = require('express'),
+ bodyParser = require('body-parser'),
+ cors = require('cors'),
+ cookieParser = require('cookie-parser'),
+ multer = require('multer'),
+ upload = multer({ dest: 'uploads/' })
+
+
+var fs = require('fs');
+
+var app = express();
+
+var staticPath = path.join(__dirname, '/public');
+app.use(express.static(staticPath));
+
+app.use(cors({ origin: true }));
+app.use(bodyParser.json());
+app.use(bodyParser.urlencoded({ extended: true }));
+app.use(cookieParser());
+
+app.listen(3455, function() {
+ console.log('listening');
+
+});
+
+app.get('/get', (req, res) => {
+ const headers = req.headers;
+ const params = req.query;
+ console.log('Got headers', headers);
+ console.log('Got params', params);
+ console.log(req.url);
+ res.status(200);
+ res.send();
+});
+
+app.get('/get-json', (req, res) => {
+ res.status(200);
+ res.json({
+ name: 'Max',
+ superpower: 'Being Awesome'
+ })
+});
+
+app.get('/get-html', (req, res) => {
+ res.status(200);
+ res.header('Content-Type', 'text/html');
+ res.send('Hi
');
+});
+
+app.get('/head', (req, res) => {
+ const headers = req.headers;
+ console.log('HEAD');
+ console.log('Got headers', headers);
+ res.status(200);
+ res.send();
+});
+
+app.delete('/delete', (req, res) => {
+ const headers = req.headers;
+ console.log('DELETE');
+ console.log('Got headers', headers);
+ res.status(200);
+ res.send();
+});
+app.patch('/patch', (req, res) => {
+ const headers = req.headers;
+ console.log('PATCH');
+ console.log('Got headers', headers);
+ res.status(200);
+ res.send();
+});
+app.post('/post', (req, res) => {
+ const headers = req.headers;
+ console.log('POST');
+ console.log('Got headers', headers);
+ res.status(200);
+ res.send();
+});
+app.put('/put', (req, res) => {
+ const headers = req.headers;
+ console.log('PUT');
+ console.log('Got headers', headers);
+ res.status(200);
+ res.send();
+});
+
+app.get('/cookie', (req, res) => {
+ console.log('COOKIE', req.cookies);
+ res.status(200);
+ res.send();
+});
+
+app.get('/download-pdf', (req, res) => {
+ console.log('Sending PDF to request', +new Date);
+ res.download('document.pdf');
+});
+
+app.get('/set-cookies', (req, res) => {
+ res.cookie('style', 'very cool');
+ res.send();
+});
+
+app.post('/upload-pdf', upload.single('myFile'), (req, res) => {
+ console.log('Handling upload');
+ const file = req.file;
+ console.log('Got file', file);
+
+ res.status(200);
+ res.send();
+});
+
+app.post('/form-data', (req, res) => {
+ console.log('Got form data post', req.body);
+
+ res.status(200);
+ res.send();
+})
+
+app.post('/form-data-multi', upload.any(), (req, res) => {
+ console.log('Got form data multipart post', req.body);
+
+ console.log(req.files);
+
+ res.status(200);
+ res.send();
+})
\ No newline at end of file
diff --git a/example/src/app/app.component.ts b/example/src/app/app.component.ts
index 0195524525..a40f823bef 100644
--- a/example/src/app/app.component.ts
+++ b/example/src/app/app.component.ts
@@ -9,7 +9,7 @@ import { Plugins } from '@capacitor/core';
export class MyApp {
@ViewChild(Nav) nav: Nav;
- rootPage = 'AppPage';
+ rootPage = 'HttpPage';
PLUGINS = [
{ name: 'App', page: 'AppPage' },
@@ -22,6 +22,7 @@ export class MyApp {
{ name: 'Filesystem', page: 'FilesystemPage' },
{ name: 'Geolocation', page: 'GeolocationPage' },
{ name: 'Haptics', page: 'HapticsPage' },
+ { name: 'Http', page: 'HttpPage' },
{ name: 'Keyboard', page: 'KeyboardPage' },
{ name: 'LocalNotifications', page: 'LocalNotificationsPage' },
{ name: 'Modals', page: 'ModalsPage' },
diff --git a/example/src/pages/filesystem/filesystem.ts b/example/src/pages/filesystem/filesystem.ts
index b7e256f108..44b252e55d 100644
--- a/example/src/pages/filesystem/filesystem.ts
+++ b/example/src/pages/filesystem/filesystem.ts
@@ -109,7 +109,7 @@ export class FilesystemPage {
try {
let ret = await Plugins.Filesystem.getUri({
path: 'text.txt',
- directory: FilesystemDirectory.Application
+ directory: FilesystemDirectory.Data
});
alert(ret.uri);
} catch(e) {
diff --git a/example/src/pages/http/http.html b/example/src/pages/http/http.html
new file mode 100644
index 0000000000..4497573887
--- /dev/null
+++ b/example/src/pages/http/http.html
@@ -0,0 +1,42 @@
+
+
+
+
+ Http
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Output
+ {{output}}
+
\ No newline at end of file
diff --git a/example/src/pages/http/http.module.ts b/example/src/pages/http/http.module.ts
new file mode 100644
index 0000000000..24d45cfbb9
--- /dev/null
+++ b/example/src/pages/http/http.module.ts
@@ -0,0 +1,13 @@
+import { NgModule } from '@angular/core';
+import { IonicPageModule } from 'ionic-angular';
+import { HttpPage } from './http';
+
+@NgModule({
+ declarations: [
+ HttpPage,
+ ],
+ imports: [
+ IonicPageModule.forChild(HttpPage),
+ ],
+})
+export class HttpPageModule { }
diff --git a/example/src/pages/http/http.scss b/example/src/pages/http/http.scss
new file mode 100644
index 0000000000..377b84b2e6
--- /dev/null
+++ b/example/src/pages/http/http.scss
@@ -0,0 +1,19 @@
+ion-textarea {
+ border: 1px solid #eee;
+ textarea {
+ height: 500px;
+ }
+}
+
+#output {
+ height: 400px;
+ overflow: auto;
+
+ unicode-bidi: embed;
+ font-family: monospace;
+ white-space: pre;
+ word-wrap: break-word;
+ max-width: 100%;
+ word-break: break-word;
+ white-space: pre;
+}
\ No newline at end of file
diff --git a/example/src/pages/http/http.ts b/example/src/pages/http/http.ts
new file mode 100644
index 0000000000..46e4ee902b
--- /dev/null
+++ b/example/src/pages/http/http.ts
@@ -0,0 +1,252 @@
+import { Component } from '@angular/core';
+import { IonicPage, NavController, NavParams, LoadingController, Loading } from 'ionic-angular';
+
+import { FilesystemDirectory, Plugins } from '@capacitor/core';
+import { SERVER_TRANSITION_PROVIDERS } from '@angular/platform-browser/src/browser/server-transition';
+
+const { Filesystem, Http } = Plugins;
+
+/**
+ * Generated class for the KeyboardPage page.
+ *
+ * See https://ionicframework.com/docs/components/#navigation for more info on
+ * Ionic pages and navigation.
+ */
+
+@IonicPage()
+@Component({
+ selector: 'page-http',
+ templateUrl: 'http.html',
+})
+export class HttpPage {
+ serverUrl = 'http://localhost:3455';
+
+ output: string = '';
+
+ loading: Loading;
+
+ constructor(public navCtrl: NavController, public navParams: NavParams, public loadingCtrl: LoadingController) {
+ }
+
+ ionViewDidLoad() {
+ console.log('ionViewDidLoad KeyboardPage');
+ }
+
+ async get(path = '/get', method = 'GET') {
+ this.output = '';
+
+ this.loading = this.loadingCtrl.create({
+ content: 'Requesting...'
+ });
+ this.loading.present();
+
+ try {
+ const ret = await Http.request({
+ method: method,
+ url: this.apiUrl(path),
+ headers: {
+ 'X-Fake-Header': 'Max was here'
+ },
+ params: {
+ 'size': 'XL'
+ }
+ });
+ console.log('Got ret', ret);
+ this.output = JSON.stringify(ret, null, 2);
+ } catch (e) {
+ this.output = `Error: ${e.message}, ${e.platformMessage}`;
+ console.error(e);
+ } finally {
+ this.loading.dismiss();
+ }
+ }
+
+ getJson = () => this.get('/get-json');
+ getHtml = () => this.get('/get-html');
+
+ head = () => this.get('/head', 'HEAD');
+ delete = () => this.mutate('/delete', 'DELETE', { title: 'foo', body: 'bar', userId: 1 });
+ patch = () => this.mutate('/patch', 'PATCH', { title: 'foo', body: 'bar', userId: 1 });
+ post = () => this.mutate('/post', 'POST', { title: 'foo', body: 'bar', userId: 1 });
+ put = () => this.mutate('/put', 'PUT', { title: 'foo', body: 'bar', userId: 1 });
+
+ async mutate(path, method, data = {}) {
+ this.output = '';
+ this.loading = this.loadingCtrl.create({
+ content: 'Requesting...'
+ });
+ this.loading.present();
+ try {
+ const ret = await Http.request({
+ url: this.apiUrl(path),
+ method: method,
+ headers: {
+ 'content-type': 'application/json',
+ },
+ data
+ });
+ console.log('Got ret', ret);
+ this.loading.dismiss();
+ this.output = JSON.stringify(ret, null, 2);
+ } catch (e) {
+ this.output = `Error: ${e.message}, ${e.platformMessage}`;
+ console.error(e);
+ } finally {
+ this.loading.dismiss();
+ }
+ }
+
+ apiUrl = (path: string) => `${this.serverUrl}${path}`;
+
+ testSetCookies = () => this.get('/set-cookies');
+
+ formPost = async () => {
+ this.output = '';
+ this.loading = this.loadingCtrl.create({
+ content: 'Requesting...'
+ });
+ this.loading.present();
+ try {
+ const ret = await Http.request({
+ url: this.apiUrl('/form-data'),
+ method: 'POST',
+ headers: {
+ 'content-type': 'application/x-www-form-urlencoded'
+ },
+ data: {
+ name: 'Max',
+ age: 5
+ }
+ });
+ console.log('Got ret', ret);
+ this.loading.dismiss();
+ this.output = JSON.stringify(ret, null, 2);
+ } catch (e) {
+ this.output = `Error: ${e.message}, ${e.platformMessage}`;
+ console.error(e);
+ } finally {
+ this.loading.dismiss();
+ }
+ }
+
+ formPostMultipart = async () => {
+ this.output = '';
+ this.loading = this.loadingCtrl.create({
+ content: 'Requesting...'
+ });
+ this.loading.present();
+ try {
+ const ret = await Http.request({
+ url: this.apiUrl('/form-data-multi'),
+ method: 'POST',
+ headers: {
+ 'content-type': 'multipart/form-data'
+ },
+ data: {
+ name: 'Max',
+ age: 5
+ }
+ });
+ console.log('Got ret', ret);
+ this.loading.dismiss();
+ this.output = JSON.stringify(ret, null, 2);
+ } catch (e) {
+ this.output = `Error: ${e.message}, ${e.platformMessage}`;
+ console.error(e);
+ } finally {
+ this.loading.dismiss();
+ }
+ }
+
+ setCookie = async () => {
+ const ret = await Http.setCookie({
+ url: this.apiUrl('/cookie'),
+ key: 'language',
+ value: 'en'
+ });
+ }
+
+ deleteCookie = async () => {
+ const ret = await Http.deleteCookie({
+ url: this.apiUrl('/cookie'),
+ key: 'language',
+ });
+ }
+
+ clearCookies = async () => {
+ const ret = await Http.clearCookies({
+ url: this.apiUrl('/cookie'),
+ });
+ }
+
+ getCookies = async () => {
+ const ret = await Http.getCookies({
+ url: this.apiUrl('/cookie')
+ });
+ console.log('Got cookies', ret);
+ this.output = JSON.stringify(ret.value);
+ }
+
+ testCookies = async () => {
+ this.loading = this.loadingCtrl.create({
+ content: 'Requesting...'
+ });
+ this.loading.present();
+ try {
+ const ret = await Http.request({
+ method: 'GET',
+ url: this.apiUrl('/cookie')
+ });
+ console.log('Got ret', ret);
+ this.loading.dismiss();
+ } catch (e) {
+ this.output = `Error: ${e.message}`;
+ console.error(e);
+ } finally {
+ this.loading.dismiss();
+ }
+ }
+
+ downloadFile = async () => {
+ console.log('Doing download', FilesystemDirectory.Downloads);
+
+ const ret = await Http.downloadFile({
+ url: this.apiUrl('/download-pdf'),
+ filePath: 'document.pdf',
+ fileDirectory: FilesystemDirectory.Downloads
+ });
+
+ console.log('Got download ret', ret);
+
+
+ /*
+ const renameRet = await Filesystem.rename({
+ from: ret.path,
+ to: 'document.pdf',
+ toDirectory: FilesystemDirectory.Downloads
+ });
+
+ console.log('Did rename', renameRet);
+ */
+
+ if (ret.path) {
+ const read = await Filesystem.readFile({
+ path: 'document.pdf',
+ directory: FilesystemDirectory.Downloads
+ });
+
+ console.log('Read', read);
+ }
+ }
+
+ uploadFile = async () => {
+ const ret = await Http.uploadFile({
+ url: this.apiUrl('/upload-pdf'),
+ name: 'myFile',
+ filePath: 'document.pdf',
+ fileDirectory: FilesystemDirectory.Downloads
+ });
+
+ console.log('Got upload ret', ret);
+ }
+}
\ No newline at end of file
diff --git a/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m b/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m
index 8d8b4e895a..75cfe6db03 100644
--- a/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m
+++ b/ios/Capacitor/Capacitor/Plugins/DefaultPlugins.m
@@ -73,6 +73,16 @@
CAP_PLUGIN_METHOD(vibrate, CAPPluginReturnNone);
)
+CAP_PLUGIN(CAPHttpPlugin, "Http",
+ CAP_PLUGIN_METHOD(request, CAPPluginReturnPromise);
+ CAP_PLUGIN_METHOD(setCookie, CAPPluginReturnPromise);
+ CAP_PLUGIN_METHOD(getCookies, CAPPluginReturnPromise);
+ CAP_PLUGIN_METHOD(deleteCookie, CAPPluginReturnPromise);
+ CAP_PLUGIN_METHOD(clearCookies, CAPPluginReturnPromise);
+ CAP_PLUGIN_METHOD(downloadFile, CAPPluginReturnPromise);
+ CAP_PLUGIN_METHOD(uploadFile, CAPPluginReturnPromise);
+)
+
CAP_PLUGIN(CAPKeyboard, "Keyboard",
CAP_PLUGIN_METHOD(show, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(hide, CAPPluginReturnPromise);
diff --git a/ios/Capacitor/Capacitor/Plugins/Filesystem.swift b/ios/Capacitor/Capacitor/Plugins/Filesystem/Filesystem.swift
similarity index 74%
rename from ios/Capacitor/Capacitor/Plugins/Filesystem.swift
rename to ios/Capacitor/Capacitor/Plugins/Filesystem/Filesystem.swift
index 43105055d0..9b6a575168 100644
--- a/ios/Capacitor/Capacitor/Plugins/Filesystem.swift
+++ b/ios/Capacitor/Capacitor/Plugins/Filesystem/Filesystem.swift
@@ -5,39 +5,7 @@ import Foundation
public class CAPFilesystemPlugin : CAPPlugin {
let DEFAULT_DIRECTORY = "DOCUMENTS"
- /**
- * Get the SearchPathDirectory corresponding to the JS string
- */
- func getDirectory(directory: String) -> FileManager.SearchPathDirectory {
- switch directory {
- case "DOCUMENTS":
- return .documentDirectory
- case "APPLICATION":
- return .applicationDirectory
- case "CACHE":
- return .cachesDirectory
- default:
- return .documentDirectory
- }
- }
-
- /**
- * Get the URL for this file, supporting file:// paths and
- * files with directory mappings.
- */
- func getFileUrl(_ path: String, _ directoryOption: String) -> URL? {
- if path.starts(with: "file://") {
- return URL(string: path)
- }
-
- let directory = getDirectory(directory: directoryOption)
-
- guard let dir = FileManager.default.urls(for: directory, in: .userDomainMask).first else {
- return nil
- }
-
- return dir.appendingPathComponent(path)
- }
+
/**
* Helper for handling errors
@@ -58,23 +26,11 @@ public class CAPFilesystemPlugin : CAPPlugin {
}
let directoryOption = call.get("directory", String.self, DEFAULT_DIRECTORY)!
- guard let fileUrl = getFileUrl(file, directoryOption) else {
- handleError(call, "Invalid path")
- return
- }
-
do {
- if encoding != nil {
- let data = try String(contentsOf: fileUrl, encoding: .utf8)
- call.success([
- "data": data
- ])
- } else {
- let data = try Data(contentsOf: fileUrl)
- call.success([
- "data": data.base64EncodedString()
- ])
- }
+ let data = try FilesystemUtils.readFileString(file, directoryOption, encoding)
+ call.resolve([
+ "data": data
+ ])
} catch let error as NSError {
handleError(call, error.localizedDescription, error)
}
@@ -87,7 +43,7 @@ public class CAPFilesystemPlugin : CAPPlugin {
let encoding = call.getString("encoding")
let recursive = call.get("recursive", Bool.self, false)!
// TODO: Allow them to switch encoding
- guard let file = call.get("path", String.self) else {
+ guard let path = call.get("path", String.self) else {
handleError(call, "path must be provided and must be a string.")
return
}
@@ -99,31 +55,9 @@ public class CAPFilesystemPlugin : CAPPlugin {
let directoryOption = call.get("directory", String.self) ?? DEFAULT_DIRECTORY
- guard let fileUrl = getFileUrl(file, directoryOption) else {
- handleError(call, "Invalid path")
- return
- }
-
do {
- if !FileManager.default.fileExists(atPath: fileUrl.deletingLastPathComponent().absoluteString) {
- if recursive {
- try FileManager.default.createDirectory(at: fileUrl.deletingLastPathComponent(), withIntermediateDirectories: recursive, attributes: nil)
- } else {
- handleError(call, "Parent folder doesn't exist");
- return
- }
- }
- if encoding != nil {
- try data.write(to: fileUrl, atomically: false, encoding: .utf8)
- } else {
- let cleanData = getCleanData(data)
- if let base64Data = Data(base64Encoded: cleanData) {
- try base64Data.write(to: fileUrl)
- } else {
- handleError(call, "Unable to save file")
- return
- }
- }
+ let fileUrl = try FilesystemUtils.writeFileString(path, directoryOption, encoding, data, recursive)
+
call.success([
"uri": fileUrl.absoluteString
])
@@ -149,7 +83,7 @@ public class CAPFilesystemPlugin : CAPPlugin {
}
let directoryOption = call.get("directory", String.self) ?? DEFAULT_DIRECTORY
- guard let fileUrl = getFileUrl(file, directoryOption) else {
+ guard let fileUrl = FilesystemUtils.getFileUrl(file, directoryOption) else {
handleError(call, "Invalid path")
return
}
@@ -165,7 +99,7 @@ public class CAPFilesystemPlugin : CAPPlugin {
}
writeData = userData
} else {
- let cleanData = getCleanData(data)
+ let cleanData = FilesystemUtils.getCleanBase64Data(data)
if let base64Data = Data(base64Encoded: cleanData) {
writeData = base64Data
} else {
@@ -187,14 +121,6 @@ public class CAPFilesystemPlugin : CAPPlugin {
}
}
- func getCleanData(_ data: String) -> String {
- let dataParts = data.split(separator: ",")
- var cleanData = data
- if dataParts.count > 0 {
- cleanData = String(dataParts.last!)
- }
- return cleanData
- }
/**
* Delete a file.
@@ -208,7 +134,7 @@ public class CAPFilesystemPlugin : CAPPlugin {
}
let directoryOption = call.get("directory", String.self) ?? DEFAULT_DIRECTORY
- guard let fileUrl = getFileUrl(file, directoryOption) else {
+ guard let fileUrl = FilesystemUtils.getFileUrl(file, directoryOption) else {
handleError(call, "Invalid path")
return
}
@@ -235,7 +161,7 @@ public class CAPFilesystemPlugin : CAPPlugin {
let recursive = call.get("recursive", Bool.self, false)!
let directoryOption = call.get("directory", String.self, DEFAULT_DIRECTORY)!
- guard let fileUrl = getFileUrl(path, directoryOption) else {
+ guard let fileUrl = FilesystemUtils.getFileUrl(path, directoryOption) else {
handleError(call, "Invalid path")
return
}
@@ -259,7 +185,7 @@ public class CAPFilesystemPlugin : CAPPlugin {
}
let directoryOption = call.get("directory", String.self, DEFAULT_DIRECTORY)!
- guard let fileUrl = getFileUrl(path, directoryOption) else {
+ guard let fileUrl = FilesystemUtils.getFileUrl(path, directoryOption) else {
handleError(call, "Invalid path")
return
}
@@ -295,7 +221,7 @@ public class CAPFilesystemPlugin : CAPPlugin {
}
let directoryOption = call.get("directory", String.self, DEFAULT_DIRECTORY)!
- guard let fileUrl = getFileUrl(path, directoryOption) else {
+ guard let fileUrl = FilesystemUtils.getFileUrl(path, directoryOption) else {
handleError(call, "Invalid path")
return
}
@@ -323,7 +249,7 @@ public class CAPFilesystemPlugin : CAPPlugin {
}
let directoryOption = call.get("directory", String.self, DEFAULT_DIRECTORY)!
- guard let fileUrl = getFileUrl(path, directoryOption) else {
+ guard let fileUrl = FilesystemUtils.getFileUrl(path, directoryOption) else {
handleError(call, "Invalid path")
return
}
@@ -349,7 +275,7 @@ public class CAPFilesystemPlugin : CAPPlugin {
}
let directoryOption = call.get("directory", String.self, DEFAULT_DIRECTORY)!
- guard let fileUrl = getFileUrl(path, directoryOption) else {
+ guard let fileUrl = FilesystemUtils.getFileUrl(path, directoryOption) else {
handleError(call, "Invalid path")
return
}
@@ -390,12 +316,12 @@ public class CAPFilesystemPlugin : CAPPlugin {
toDirectoryOption = directoryOption;
}
- guard let fromUrl = getFileUrl(from, directoryOption) else {
+ guard let fromUrl = FilesystemUtils.getFileUrl(from, directoryOption) else {
handleError(call, "Invalid from path")
return
}
- guard let toUrl = getFileUrl(to, toDirectoryOption) else {
+ guard let toUrl = FilesystemUtils.getFileUrl(to, toDirectoryOption) else {
handleError(call, "Invalid to path")
return
}
diff --git a/ios/Capacitor/Capacitor/Plugins/Filesystem/FilesystemUtils.swift b/ios/Capacitor/Capacitor/Plugins/Filesystem/FilesystemUtils.swift
new file mode 100644
index 0000000000..33fffee71f
--- /dev/null
+++ b/ios/Capacitor/Capacitor/Plugins/Filesystem/FilesystemUtils.swift
@@ -0,0 +1,122 @@
+import Foundation
+import MobileCoreServices
+
+enum FilesystemError: Error {
+ case fileNotFound(String)
+ case invalidPath(String)
+ case parentFolderNotExists(String)
+ case saveError(String)
+}
+
+class FilesystemUtils {
+ /**
+ * Get the SearchPathDirectory corresponding to the JS string
+ */
+ static func getDirectory(directory: String) -> FileManager.SearchPathDirectory {
+ switch directory {
+ case "DOCUMENTS":
+ return .documentDirectory
+ case "APPLICATION":
+ return .applicationDirectory
+ case "CACHE":
+ return .cachesDirectory
+ case "DOWNLOADS":
+ return .downloadsDirectory
+ default:
+ return .documentDirectory
+ }
+ }
+
+ /**
+ * Get the URL for this file, supporting file:// paths and
+ * files with directory mappings.
+ */
+ static func getFileUrl(_ path: String, _ directoryOption: String) -> URL? {
+ if path.starts(with: "file://") {
+ return URL(string: path)
+ }
+
+ let directory = FilesystemUtils.getDirectory(directory: directoryOption)
+
+ guard let dir = FileManager.default.urls(for: directory, in: .userDomainMask).first else {
+ return nil
+ }
+
+ return dir.appendingPathComponent(path)
+ }
+
+ static func createDirectoryForFile(_ fileUrl: URL, _ recursive: Bool) throws {
+ if !FileManager.default.fileExists(atPath: fileUrl.deletingLastPathComponent().absoluteString) {
+ if recursive {
+ try FileManager.default.createDirectory(at: fileUrl.deletingLastPathComponent(), withIntermediateDirectories: recursive, attributes: nil)
+ } else {
+ throw FilesystemError.parentFolderNotExists("Parent folder doesn't exist")
+ }
+ }
+ }
+
+ /**
+ * Read a file as a string at the given directory and with the given encoding
+ */
+ static func readFileString(_ path: String, _ directory: String, _ encoding: String?) throws -> String {
+ guard let fileUrl = FilesystemUtils.getFileUrl(path, directory) else {
+ throw FilesystemError.fileNotFound("No such file exists")
+ }
+ if encoding != nil {
+ let data = try String(contentsOf: fileUrl, encoding: .utf8)
+ return data
+ } else {
+ let data = try Data(contentsOf: fileUrl)
+ return data.base64EncodedString()
+ }
+ }
+
+ static func writeFileString(_ path: String, _ directory: String, _ encoding: String?, _ data: String, _ recursive: Bool = false) throws -> URL {
+
+ guard let fileUrl = FilesystemUtils.getFileUrl(path, directory) else {
+ throw FilesystemError.invalidPath("Invlid path")
+ }
+
+ if !FileManager.default.fileExists(atPath: fileUrl.deletingLastPathComponent().absoluteString) {
+ if recursive {
+ try FileManager.default.createDirectory(at: fileUrl.deletingLastPathComponent(), withIntermediateDirectories: recursive, attributes: nil)
+ } else {
+ throw FilesystemError.parentFolderNotExists("Parent folder doesn't exist")
+ }
+ }
+
+ if encoding != nil {
+ try data.write(to: fileUrl, atomically: false, encoding: .utf8)
+ } else {
+ let cleanData = getCleanBase64Data(data)
+ if let base64Data = Data(base64Encoded: cleanData) {
+ try base64Data.write(to: fileUrl)
+ } else {
+ throw FilesystemError.saveError("Unable to save file")
+ }
+ }
+
+ return fileUrl
+ }
+
+
+ static func getCleanBase64Data(_ data: String) -> String {
+ let dataParts = data.split(separator: ",")
+ var cleanData = data
+ if dataParts.count > 0 {
+ cleanData = String(dataParts.last!)
+ }
+ return cleanData
+ }
+
+ static func mimeTypeForPath(path: String) -> String {
+ let url = NSURL(fileURLWithPath: path)
+ let pathExtension = url.pathExtension
+ if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension! as NSString, nil)?.takeRetainedValue() {
+ if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() {
+ return mimetype as String
+ }
+ }
+ return "application/octet-stream"
+ }
+}
diff --git a/ios/Capacitor/Capacitor/Plugins/Http/Http.swift b/ios/Capacitor/Capacitor/Plugins/Http/Http.swift
new file mode 100644
index 0000000000..e9c0891065
--- /dev/null
+++ b/ios/Capacitor/Capacitor/Plugins/Http/Http.swift
@@ -0,0 +1,395 @@
+import Foundation
+import AudioToolbox
+
+@objc(CAPHttpPlugin)
+public class CAPHttpPlugin: CAPPlugin {
+
+ @objc public func request(_ call: CAPPluginCall) {
+ guard let urlValue = call.getString("url") else {
+ return call.reject("Must provide a URL")
+ }
+ guard let method = call.getString("method") else {
+ return call.reject("Must provide a method. One of GET, DELETE, HEAD PATCH, POST, or PUT")
+ }
+
+ let headers = (call.getObject("headers") ?? [:]) as [String:String]
+
+ let params = (call.getObject("params") ?? [:]) as [String:String]
+
+ guard var url = URL(string: urlValue) else {
+ return call.reject("Invalid URL")
+ }
+
+
+ switch method {
+ case "GET", "HEAD":
+ get(call, &url, method, headers, params)
+ case "DELETE", "PATCH", "POST", "PUT":
+ mutate(call, url, method, headers)
+ default:
+ call.reject("Unknown method")
+ }
+ }
+
+
+ @objc public func downloadFile(_ call: CAPPluginCall) {
+ guard let urlValue = call.getString("url") else {
+ return call.reject("Must provide a URL")
+ }
+ guard let filePath = call.getString("filePath") else {
+ return call.reject("Must provide a file path to download the file to")
+ }
+
+ let fileDirectory = call.getString("fileDirectory") ?? "DOCUMENTS"
+
+ guard let url = URL(string: urlValue) else {
+ return call.reject("Invalid URL")
+ }
+
+ let task = URLSession.shared.downloadTask(with: url) { (downloadLocation, response, error) in
+ if error != nil {
+ CAPLog.print("Error on download file", downloadLocation, response, error)
+ call.reject("Error", error, [:])
+ return
+ }
+
+ guard let location = downloadLocation else {
+ call.reject("Unable to get file after downloading")
+ return
+ }
+
+ // TODO: Move to abstracted FS operations
+ let fileManager = FileManager.default
+
+ let foundDir = FilesystemUtils.getDirectory(directory: fileDirectory)
+ let dir = fileManager.urls(for: foundDir, in: .userDomainMask).first
+
+ do {
+ let dest = dir!.appendingPathComponent(filePath)
+ print("File Dest", dest.absoluteString)
+
+ try FilesystemUtils.createDirectoryForFile(dest, true)
+
+ try fileManager.moveItem(at: location, to: dest)
+ call.resolve([
+ "path": dest.absoluteString
+ ])
+ } catch let e {
+ call.reject("Unable to download file", e)
+ return
+ }
+
+
+ CAPLog.print("Downloaded file", location)
+ call.resolve()
+ }
+
+ task.resume()
+ }
+
+ @objc public func uploadFile(_ call: CAPPluginCall) {
+ guard let urlValue = call.getString("url") else {
+ return call.reject("Must provide a URL")
+ }
+ guard let filePath = call.getString("filePath") else {
+ return call.reject("Must provide a file path to download the file to")
+ }
+ let name = call.getString("name") ?? "file"
+
+ let fileDirectory = call.getString("fileDirectory") ?? "DOCUMENTS"
+
+ guard let url = URL(string: urlValue) else {
+ return call.reject("Invalid URL")
+ }
+
+ guard let fileUrl = FilesystemUtils.getFileUrl(filePath, fileDirectory) else {
+ return call.reject("Unable to get file URL")
+ }
+
+ var request = URLRequest.init(url: url)
+ request.httpMethod = "POST"
+
+ let boundary = UUID().uuidString
+
+ var fullFormData: Data?
+ do {
+ fullFormData = try generateFullMultipartRequestBody(fileUrl, name, boundary)
+ } catch let e {
+ return call.reject("Unable to read file to upload", e)
+ }
+
+
+ request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
+
+ let task = URLSession.shared.uploadTask(with: request, from: fullFormData) { (data, response, error) in
+ if error != nil {
+ CAPLog.print("Error on upload file", data, response, error)
+ call.reject("Error", error, [:])
+ return
+ }
+
+ let res = response as! HTTPURLResponse
+
+ //CAPLog.print("Uploaded file", location)
+ call.resolve()
+ }
+
+ task.resume()
+ }
+
+ @objc public func setCookie(_ call: CAPPluginCall) {
+
+ guard let key = call.getString("key") else {
+ return call.reject("Must provide key")
+ }
+ guard let value = call.getString("value") else {
+ return call.reject("Must provide value")
+ }
+ guard let urlString = call.getString("url") else {
+ return call.reject("Must provide URL")
+ }
+
+ guard let url = URL(string: urlString) else {
+ return call.reject("Invalid URL")
+ }
+
+ let jar = HTTPCookieStorage.shared
+ let field = ["Set-Cookie": "\(key)=\(value)"]
+ let cookies = HTTPCookie.cookies(withResponseHeaderFields: field, for: url)
+ jar.setCookies(cookies, for: url, mainDocumentURL: url)
+
+ call.resolve()
+ }
+
+ @objc public func getCookies(_ call: CAPPluginCall) {
+ guard let urlString = call.getString("url") else {
+ return call.reject("Must provide URL")
+ }
+
+ guard let url = URL(string: urlString) else {
+ return call.reject("Invalid URL")
+ }
+
+ let jar = HTTPCookieStorage.shared
+ guard let cookies = jar.cookies(for: url) else {
+ return call.resolve([
+ "value": []
+ ])
+ }
+
+ let c = cookies.map { (cookie: HTTPCookie) -> [String:String] in
+ return [
+ "key": cookie.name,
+ "value": cookie.value
+ ]
+ }
+
+ call.resolve([
+ "value": c
+ ])
+ }
+
+ @objc public func deleteCookie(_ call: CAPPluginCall) {
+ guard let urlString = call.getString("url") else {
+ return call.reject("Must provide URL")
+ }
+ guard let key = call.getString("key") else {
+ return call.reject("Must provide key")
+ }
+ guard let url = URL(string: urlString) else {
+ return call.reject("Invalid URL")
+ }
+
+ let jar = HTTPCookieStorage.shared
+
+ let cookie = jar.cookies(for: url)?.first(where: { (cookie) -> Bool in
+ return cookie.name == key
+ })
+ if cookie != nil {
+ jar.deleteCookie(cookie!)
+ }
+
+ call.resolve()
+ }
+
+ @objc public func clearCookies(_ call: CAPPluginCall) {
+ guard let urlString = call.getString("url") else {
+ return call.reject("Must provide URL")
+ }
+ guard let url = URL(string: urlString) else {
+ return call.reject("Invalid URL")
+ }
+ let jar = HTTPCookieStorage.shared
+ jar.cookies(for: url)?.forEach({ (cookie) in
+ jar.deleteCookie(cookie)
+ })
+ call.resolve()
+ }
+
+
+ /* PRIVATE */
+
+ // Handle GET operations
+ func get(_ call: CAPPluginCall, _ url: inout URL, _ method: String, _ headers: [String:String], _ params: [String:String]) {
+ setUrlQuery(&url, params)
+
+ var request = URLRequest(url: url)
+
+ request.httpMethod = method
+
+ setRequestHeaders(&request, headers)
+
+ let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
+ if error != nil {
+ call.reject("Error", error, [:])
+ return
+ }
+
+ let res = response as! HTTPURLResponse
+
+ call.resolve(self.buildResponse(data, res))
+ }
+
+ task.resume()
+ }
+
+ func setUrlQuery(_ url: inout URL, _ params: [String:String]) {
+ var cmps = URLComponents(url: url, resolvingAgainstBaseURL: true)
+ cmps!.queryItems = params.map({ (key, value) -> URLQueryItem in
+ return URLQueryItem(name: key, value: value)
+ })
+ url = cmps!.url!
+ }
+
+ func setRequestHeaders(_ request: inout URLRequest, _ headers: [String:String]) {
+ headers.keys.forEach { (key) in
+ guard let value = headers[key] else {
+ return
+ }
+ request.addValue(value, forHTTPHeaderField: key)
+ }
+ }
+
+ // Handle mutation operations: DELETE, PATCH, POST, and PUT
+ func mutate(_ call: CAPPluginCall, _ url: URL, _ method: String, _ headers: [String:String]) {
+ let data = call.getObject("data")
+
+ var request = URLRequest(url: url)
+ request.httpMethod = method
+
+ setRequestHeaders(&request, headers)
+
+ let contentType = getRequestHeader(headers, "Content-Type") as? String
+
+ if data != nil && contentType != nil {
+ do {
+ request.httpBody = try getRequestData(request, data!, contentType!)
+ } catch let e {
+ call.reject("Unable to set request data", e)
+ return
+ }
+ }
+
+ let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
+ if error != nil {
+ call.reject("Error", error, [:])
+ return
+ }
+
+ let res = response as! HTTPURLResponse
+
+ call.resolve(self.buildResponse(data, res))
+ }
+
+ task.resume()
+ }
+
+ func buildResponse(_ data: Data?, _ response: HTTPURLResponse) -> [String:Any] {
+
+ var ret = [:] as [String:Any]
+
+ ret["status"] = response.statusCode
+ ret["headers"] = response.allHeaderFields
+
+ let contentType = response.allHeaderFields["Content-Type"] as? String
+
+ if data != nil && contentType != nil && contentType!.contains("application/json") {
+ if let json = try? JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String: Any] {
+ print("Got json")
+ print(json)
+ // handle json...
+ ret["data"] = json
+ }
+ } else {
+ if (data != nil) {
+ ret["data"] = String(data: data!, encoding: .utf8);
+ } else {
+ ret["data"] = ""
+ }
+ }
+
+ return ret
+ }
+
+ func getRequestHeader(_ headers: [String:Any], _ header: String) -> Any? {
+ var normalizedHeaders = [:] as [String:Any]
+ headers.keys.forEach { (key) in
+ normalizedHeaders[key.lowercased()] = headers[key]
+ }
+ return normalizedHeaders[header.lowercased()]
+ }
+
+ func getRequestData(_ request: URLRequest, _ data: [String:Any], _ contentType: String) throws -> Data? {
+ if contentType.contains("application/json") {
+ return try setRequestDataJson(request, data)
+ } else if contentType.contains("application/x-www-form-urlencoded") {
+ return setRequestDataFormUrlEncoded(request, data)
+ } else if contentType.contains("multipart/form-data") {
+ return setRequestDataMultipartFormData(request, data)
+ }
+ return nil
+ }
+
+ func setRequestDataJson(_ request: URLRequest, _ data: [String:Any]) throws -> Data? {
+ let jsonData = try JSONSerialization.data(withJSONObject: data)
+ return jsonData
+ }
+
+ func setRequestDataFormUrlEncoded(_ request: URLRequest, _ data: [String:Any]) -> Data? {
+ guard var components = URLComponents(url: request.url!, resolvingAgainstBaseURL: false) else {
+ return nil
+ }
+ components.queryItems = []
+ data.keys.forEach { (key) in
+ components.queryItems?.append(URLQueryItem(name: key, value: "\(data[key] ?? "")"))
+ }
+
+ if components.query != nil {
+ return Data(components.query!.utf8)
+ }
+
+ return nil
+ }
+
+ func setRequestDataMultipartFormData(_ request: URLRequest, _ data: [String:Any]) -> Data? {
+ return nil
+ }
+
+
+ func generateFullMultipartRequestBody(_ url: URL, _ name: String, _ boundary: String) throws -> Data {
+ var data = Data()
+
+ let fileData = try Data(contentsOf: url)
+
+
+ let fname = url.lastPathComponent
+ let mimeType = FilesystemUtils.mimeTypeForPath(path: fname)
+ data.append("\r\n--\(boundary)\r\n".data(using: .utf8)!)
+ data.append("Content-Disposition: form-data; name=\"\(name)\"; filename=\"\(fname)\"\r\n".data(using: .utf8)!)
+ data.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!)
+ data.append(fileData)
+ data.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
+
+ return data
+ }
+}