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 + } +}