mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(android upload): restore provider uploads
This commit is contained in:
@@ -0,0 +1,76 @@
|
|||||||
|
package fivechan.android;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.net.Uri;
|
||||||
|
import androidx.core.content.FileProvider;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** Live provider integration test (emulator/device) for Android's native catbox uploader. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class CatboxLiveUploadTest {
|
||||||
|
private static final long TEST_TIMEOUT_SEC = 30;
|
||||||
|
private static final String GENERATED_FILE_NAME = "catbox-gradient-320x320.png";
|
||||||
|
|
||||||
|
private Context appContext;
|
||||||
|
private Uri uploadUri;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||||
|
uploadUri = createGradientPngUri(appContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void catbox_liveUpload_fromGeneratedPng_succeeds() {
|
||||||
|
MediaUploadResult result = CatboxUploader.upload(appContext, uploadUri, TEST_TIMEOUT_SEC);
|
||||||
|
|
||||||
|
assertNotNull("Expected catbox result", result);
|
||||||
|
assertTrue("Expected catbox upload success, got: " + result.error, result.success);
|
||||||
|
assertNotNull("Expected uploaded URL", result.url);
|
||||||
|
String normalizedUrl = result.url.toLowerCase();
|
||||||
|
assertTrue(
|
||||||
|
"Expected catbox file URL, got: " + result.url,
|
||||||
|
normalizedUrl.matches("https?://files\\.catbox\\.moe/.+\\.(png|jpg|jpeg|webp|gif)(?:[?#].*)?"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Uri createGradientPngUri(Context context) throws IOException {
|
||||||
|
File dir = new File(context.getCacheDir(), "live-upload-test");
|
||||||
|
if (!dir.exists() && !dir.mkdirs()) {
|
||||||
|
throw new IOException("Unable to create cache directory: " + dir.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
File imageFile = new File(dir, GENERATED_FILE_NAME);
|
||||||
|
Bitmap bitmap = Bitmap.createBitmap(320, 320, Bitmap.Config.ARGB_8888);
|
||||||
|
for (int y = 0; y < 320; y++) {
|
||||||
|
for (int x = 0; x < 320; x++) {
|
||||||
|
bitmap.setPixel(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
Color.rgb((x * 7) % 256, (y * 5) % 256, ((x + y) * 3) % 256));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try (FileOutputStream fos = new FileOutputStream(imageFile)) {
|
||||||
|
if (!bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)) {
|
||||||
|
throw new IOException("Failed to encode generated PNG");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
bitmap.recycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
return FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
context.getPackageName() + ".fileprovider",
|
||||||
|
imageFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package fivechan.android;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.net.Uri;
|
||||||
|
import androidx.core.content.FileProvider;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** Live provider integration test (emulator/device) for Android's ImgBB WebView uploader. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class ImgbbLiveUploadTest {
|
||||||
|
private static final long TEST_TIMEOUT_SEC = 120;
|
||||||
|
private static final String GENERATED_FILE_NAME = "imgbb-gradient-320x320.png";
|
||||||
|
|
||||||
|
private Context appContext;
|
||||||
|
private Uri uploadUri;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||||
|
uploadUri = createGradientPngUri(appContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void imgbb_liveUpload_fromGeneratedPng_succeeds() throws Exception {
|
||||||
|
Intent launchIntent = new Intent(appContext, MainActivity.class);
|
||||||
|
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
appContext.startActivity(launchIntent);
|
||||||
|
Thread.sleep(1_000);
|
||||||
|
|
||||||
|
AtomicReference<MediaUploadResult> resultRef = new AtomicReference<>();
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
MediaUploadCallback callback =
|
||||||
|
result -> {
|
||||||
|
resultRef.set(result);
|
||||||
|
latch.countDown();
|
||||||
|
};
|
||||||
|
|
||||||
|
MediaUploadAutomationRunner runner =
|
||||||
|
new MediaUploadAutomationRunner(
|
||||||
|
appContext,
|
||||||
|
uploadUri,
|
||||||
|
GENERATED_FILE_NAME,
|
||||||
|
MediaUploadRecipes.PROVIDER_IMGBB,
|
||||||
|
callback);
|
||||||
|
|
||||||
|
runner.run();
|
||||||
|
|
||||||
|
assertTrue(
|
||||||
|
"Runner did not complete within " + TEST_TIMEOUT_SEC + "s",
|
||||||
|
latch.await(TEST_TIMEOUT_SEC, TimeUnit.SECONDS));
|
||||||
|
|
||||||
|
MediaUploadResult result = resultRef.get();
|
||||||
|
assertNotNull("Callback did not receive result", result);
|
||||||
|
assertTrue(
|
||||||
|
"Expected live imgbb upload success, got error="
|
||||||
|
+ result.error
|
||||||
|
+ " stage="
|
||||||
|
+ result.stage
|
||||||
|
+ " elapsedMs="
|
||||||
|
+ result.elapsedMs
|
||||||
|
+ " selector="
|
||||||
|
+ result.matchedSelectors
|
||||||
|
+ " retries="
|
||||||
|
+ result.triggerRetryCount,
|
||||||
|
result.success);
|
||||||
|
assertNotNull("Expected uploaded URL", result.url);
|
||||||
|
String normalizedUrl = result.url.toLowerCase();
|
||||||
|
assertTrue(
|
||||||
|
"Expected direct i.ibb.co URL, got: " + result.url,
|
||||||
|
normalizedUrl.contains("://i.ibb.co/"));
|
||||||
|
assertTrue(
|
||||||
|
"Expected direct image URL with extension, got: " + result.url,
|
||||||
|
normalizedUrl.matches(
|
||||||
|
"https?://i\\.ibb\\.co/.+\\.(jpg|jpeg|png|gif|webp|bmp|avif)(?:[?#].*)?"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Uri createGradientPngUri(Context context) throws IOException {
|
||||||
|
File dir = new File(context.getCacheDir(), "live-upload-test");
|
||||||
|
if (!dir.exists() && !dir.mkdirs()) {
|
||||||
|
throw new IOException("Unable to create cache directory: " + dir.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
File imageFile = new File(dir, GENERATED_FILE_NAME);
|
||||||
|
Bitmap bitmap = Bitmap.createBitmap(320, 320, Bitmap.Config.ARGB_8888);
|
||||||
|
Canvas canvas = new Canvas(bitmap);
|
||||||
|
Paint paint = new Paint();
|
||||||
|
for (int y = 0; y < 320; y++) {
|
||||||
|
int red = Math.round((255f * y) / 319f);
|
||||||
|
int blue = 255 - red;
|
||||||
|
paint.setColor(Color.rgb(red, 64, blue));
|
||||||
|
canvas.drawLine(0, y, 319, y, paint);
|
||||||
|
}
|
||||||
|
try (FileOutputStream fos = new FileOutputStream(imageFile)) {
|
||||||
|
if (!bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)) {
|
||||||
|
throw new IOException("Failed to encode generated PNG");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
bitmap.recycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
return FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
context.getPackageName() + ".fileprovider",
|
||||||
|
imageFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package fivechan.android;
|
|||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
@@ -16,6 +17,7 @@ import java.util.concurrent.CountDownLatch;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
|
import org.junit.Ignore;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
@@ -38,7 +40,13 @@ public class ImgurLiveUploadTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Ignore("Imgur upload is no longer supported on Android WebView; keep for manual diagnostics.")
|
||||||
public void imgur_liveUpload_fromGeneratedPng_succeeds() throws Exception {
|
public void imgur_liveUpload_fromGeneratedPng_succeeds() throws Exception {
|
||||||
|
Intent launchIntent = new Intent(appContext, MainActivity.class);
|
||||||
|
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
appContext.startActivity(launchIntent);
|
||||||
|
Thread.sleep(1_000);
|
||||||
|
|
||||||
AtomicReference<MediaUploadResult> resultRef = new AtomicReference<>();
|
AtomicReference<MediaUploadResult> resultRef = new AtomicReference<>();
|
||||||
CountDownLatch latch = new CountDownLatch(1);
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
MediaUploadCallback callback =
|
MediaUploadCallback callback =
|
||||||
|
|||||||
+5
-2
@@ -10,6 +10,7 @@ import java.util.concurrent.TimeUnit;
|
|||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
|
import org.junit.Ignore;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
@@ -17,13 +18,15 @@ import org.junit.runner.RunWith;
|
|||||||
* Instrumentation tests for MediaUploadAutomationRunner against controlled HTML fixtures.
|
* Instrumentation tests for MediaUploadAutomationRunner against controlled HTML fixtures.
|
||||||
* Uses DataTransfer injection (no chooser). Simulates: delayed DOM, missing selectors,
|
* Uses DataTransfer injection (no chooser). Simulates: delayed DOM, missing selectors,
|
||||||
* success URL extraction, blocked. Validates timeout/error classification (input_not_found,
|
* success URL extraction, blocked. Validates timeout/error classification (input_not_found,
|
||||||
* blocked/captcha, upload_timed_out). Runs on emulator or device; fixtures are deterministic.
|
* blocked/captcha, upload_timed_out). Kept for opt-in diagnostics because Android app uploads no
|
||||||
|
* longer use this WebView path.
|
||||||
*/
|
*/
|
||||||
|
@Ignore("Imgur WebView upload is no longer supported on Android; keep for manual diagnostics.")
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class MediaUploadAutomationRunnerTest {
|
public class MediaUploadAutomationRunnerTest {
|
||||||
|
|
||||||
private static final String FIXTURE_BASE = "file:///android_asset/fixtures/";
|
private static final String FIXTURE_BASE = "file:///android_asset/fixtures/";
|
||||||
private static final long TEST_TIMEOUT_SEC = 15;
|
private static final long TEST_TIMEOUT_SEC = 25;
|
||||||
|
|
||||||
/** Minimal 1x1 PNG for fixture tests (DataTransfer injection). */
|
/** Minimal 1x1 PNG for fixture tests (DataTransfer injection). */
|
||||||
private static final byte[] SAMPLE_FILE_BYTES =
|
private static final byte[] SAMPLE_FILE_BYTES =
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package fivechan.android;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.util.Log;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import okhttp3.Call;
|
||||||
|
import okhttp3.Callback;
|
||||||
|
import okhttp3.MediaType;
|
||||||
|
import okhttp3.MultipartBody;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
final class CatboxUploader {
|
||||||
|
private static final String TAG = "CatboxUploader";
|
||||||
|
private static final String CATBOX_API_URL = "https://catbox.moe/user/api.php";
|
||||||
|
private static final String CATBOX_FILE_URL_PREFIX = "https://files.catbox.moe/";
|
||||||
|
private static final String CATBOX_FILE_URL_PREFIX_HTTP = "http://files.catbox.moe/";
|
||||||
|
private static final int MAX_UPLOAD_ATTEMPTS = 2;
|
||||||
|
|
||||||
|
private CatboxUploader() {}
|
||||||
|
|
||||||
|
static MediaUploadResult upload(Context context, Uri fileUri, long timeoutSec) {
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "Resolving file for catbox upload");
|
||||||
|
File file = FileUtils.getFileFromUri(context, fileUri);
|
||||||
|
if (file == null) {
|
||||||
|
return new MediaUploadResult(false, null, "Could not resolve file");
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Resolved catbox upload file: " + file.getName() + " (" + file.length() + " bytes)");
|
||||||
|
|
||||||
|
OkHttpClient client =
|
||||||
|
new OkHttpClient.Builder()
|
||||||
|
.callTimeout(timeoutSec, TimeUnit.SECONDS)
|
||||||
|
.connectTimeout(timeoutSec, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(timeoutSec, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(timeoutSec, TimeUnit.SECONDS)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
MediaUploadResult lastResult = null;
|
||||||
|
for (int attempt = 1; attempt <= MAX_UPLOAD_ATTEMPTS; attempt++) {
|
||||||
|
Request request = buildRequest(file);
|
||||||
|
Log.d(TAG, "Starting catbox upload request attempt " + attempt);
|
||||||
|
lastResult = executeRequest(client, request, timeoutSec);
|
||||||
|
if (lastResult.success || !isRetryable(lastResult) || attempt == MAX_UPLOAD_ATTEMPTS) {
|
||||||
|
return lastResult;
|
||||||
|
}
|
||||||
|
Log.w(TAG, "Retrying catbox upload after: " + lastResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastResult != null ? lastResult : new MediaUploadResult(false, null, "No result");
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Catbox upload failed", e);
|
||||||
|
return new MediaUploadResult(false, null, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Request buildRequest(File file) {
|
||||||
|
RequestBody requestBody =
|
||||||
|
new MultipartBody.Builder()
|
||||||
|
.setType(MultipartBody.FORM)
|
||||||
|
.addFormDataPart("reqtype", "fileupload")
|
||||||
|
.addFormDataPart(
|
||||||
|
"fileToUpload",
|
||||||
|
file.getName(),
|
||||||
|
RequestBody.create(
|
||||||
|
file, MediaType.parse("application/octet-stream")))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return new Request.Builder().url(CATBOX_API_URL).post(requestBody).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MediaUploadResult executeRequest(
|
||||||
|
OkHttpClient client, Request request, long timeoutSec) throws InterruptedException {
|
||||||
|
Call call = client.newCall(request);
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
AtomicReference<MediaUploadResult> resultRef = new AtomicReference<>();
|
||||||
|
call.enqueue(
|
||||||
|
new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call call, IOException e) {
|
||||||
|
resultRef.set(new MediaUploadResult(false, null, e.getMessage()));
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call call, Response response) {
|
||||||
|
try {
|
||||||
|
resultRef.set(parseResponse(response));
|
||||||
|
} catch (Exception e) {
|
||||||
|
resultRef.set(new MediaUploadResult(false, null, e.getMessage()));
|
||||||
|
} finally {
|
||||||
|
response.close();
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
boolean completed = latch.await(timeoutSec, TimeUnit.SECONDS);
|
||||||
|
if (!completed) {
|
||||||
|
call.cancel();
|
||||||
|
return new MediaUploadResult(false, null, "Upload timeout");
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaUploadResult result = resultRef.get();
|
||||||
|
return result != null ? result : new MediaUploadResult(false, null, "No result");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isRetryable(MediaUploadResult result) {
|
||||||
|
if (result == null || result.success || result.error == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return result.error.equals("Empty response body")
|
||||||
|
|| result.error.startsWith("Unexpected response body:")
|
||||||
|
|| result.error.equals("Upload timeout");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MediaUploadResult parseResponse(Response response) throws IOException {
|
||||||
|
if (!response.isSuccessful()) {
|
||||||
|
return new MediaUploadResult(false, null, "Unexpected response " + response.code());
|
||||||
|
}
|
||||||
|
if (response.body() == null) {
|
||||||
|
return new MediaUploadResult(false, null, "Empty response body");
|
||||||
|
}
|
||||||
|
String trimmedUrl = response.body().string().trim();
|
||||||
|
if (trimmedUrl.isEmpty()) {
|
||||||
|
return new MediaUploadResult(false, null, "Empty response body");
|
||||||
|
}
|
||||||
|
if (!trimmedUrl.startsWith(CATBOX_FILE_URL_PREFIX)
|
||||||
|
&& !trimmedUrl.startsWith(CATBOX_FILE_URL_PREFIX_HTTP)) {
|
||||||
|
return new MediaUploadResult(false, null, "Unexpected response body: " + trimmedUrl);
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Catbox upload successful. URL: " + trimmedUrl);
|
||||||
|
return new MediaUploadResult(true, trimmedUrl, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
package fivechan.android;
|
package fivechan.android;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
import androidx.activity.result.ActivityResult;
|
import androidx.activity.result.ActivityResult;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
@@ -29,13 +25,6 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
|
|
||||||
import okhttp3.MediaType;
|
|
||||||
import okhttp3.MultipartBody;
|
|
||||||
import okhttp3.OkHttpClient;
|
|
||||||
import okhttp3.Request;
|
|
||||||
import okhttp3.RequestBody;
|
|
||||||
import okhttp3.Response;
|
|
||||||
|
|
||||||
@CapacitorPlugin(name = "FileUploader")
|
@CapacitorPlugin(name = "FileUploader")
|
||||||
public class FileUploaderPlugin extends Plugin {
|
public class FileUploaderPlugin extends Plugin {
|
||||||
private static final String TAG = "FileUploaderPlugin";
|
private static final String TAG = "FileUploaderPlugin";
|
||||||
@@ -46,9 +35,10 @@ public class FileUploaderPlugin extends Plugin {
|
|||||||
@PluginMethod
|
@PluginMethod
|
||||||
public void pickAndUploadMedia(PluginCall call) {
|
public void pickAndUploadMedia(PluginCall call) {
|
||||||
Log.d(TAG, "pickAndUploadMedia called");
|
Log.d(TAG, "pickAndUploadMedia called");
|
||||||
List<String> providerOrder = parseProviderOrder(call);
|
List<String> providerOrder = getProviderOrder(call);
|
||||||
if (providerOrder.isEmpty()) {
|
if (providerOrder.isEmpty()) {
|
||||||
providerOrder.add(PROVIDER_CATBOX);
|
call.reject("No supported upload providers selected");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||||
intent.setType("*/*");
|
intent.setType("*/*");
|
||||||
@@ -66,9 +56,12 @@ public class FileUploaderPlugin extends Plugin {
|
|||||||
Object o = arr.get(i);
|
Object o = arr.get(i);
|
||||||
if (o instanceof String) {
|
if (o instanceof String) {
|
||||||
String p = (String) o;
|
String p = (String) o;
|
||||||
if (PROVIDER_CATBOX.equals(p)
|
if (PROVIDER_CATBOX.equals(p)) {
|
||||||
|| MediaUploadRecipes.PROVIDER_IMGUR.equals(p)) {
|
|
||||||
order.add(p);
|
order.add(p);
|
||||||
|
} else if (MediaUploadRecipes.isAndroidUploadProvider(p)) {
|
||||||
|
order.add(p);
|
||||||
|
} else if (MediaUploadRecipes.PROVIDER_IMGUR.equals(p)) {
|
||||||
|
Log.d(TAG, "Skipping unsupported Android upload provider: " + p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -79,6 +72,14 @@ public class FileUploaderPlugin extends Plugin {
|
|||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<String> getProviderOrder(PluginCall call) {
|
||||||
|
List<String> providerOrder = parseProviderOrder(call);
|
||||||
|
if (providerOrder.isEmpty() && call.getArray("providerOrder") == null) {
|
||||||
|
providerOrder.add(PROVIDER_CATBOX);
|
||||||
|
}
|
||||||
|
return providerOrder;
|
||||||
|
}
|
||||||
|
|
||||||
@ActivityCallback
|
@ActivityCallback
|
||||||
private void pickFileResult(PluginCall call, ActivityResult result) {
|
private void pickFileResult(PluginCall call, ActivityResult result) {
|
||||||
Log.d(TAG, "pickFileResult callback received");
|
Log.d(TAG, "pickFileResult callback received");
|
||||||
@@ -103,9 +104,10 @@ public class FileUploaderPlugin extends Plugin {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> providerOrder = parseProviderOrder(call);
|
List<String> providerOrder = getProviderOrder(call);
|
||||||
if (providerOrder.isEmpty()) {
|
if (providerOrder.isEmpty()) {
|
||||||
providerOrder.add(PROVIDER_CATBOX);
|
call.reject("No supported upload providers selected");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
new Thread(
|
new Thread(
|
||||||
@@ -143,7 +145,7 @@ public class FileUploaderPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
attempt.put("error", res.error);
|
attempt.put("error", res.error);
|
||||||
errorSummary.append(provider).append(": ").append(res.error).append("; ");
|
errorSummary.append(provider).append(": ").append(res.error).append("; ");
|
||||||
} else if (MediaUploadRecipes.PROVIDER_IMGUR.equals(provider)) {
|
} else if (MediaUploadRecipes.isWebViewProvider(provider)) {
|
||||||
MediaUploadResult res = uploadViaWebViewSync(fileUri, provider);
|
MediaUploadResult res = uploadViaWebViewSync(fileUri, provider);
|
||||||
attempt.put("success", res.success);
|
attempt.put("success", res.success);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
@@ -195,82 +197,18 @@ public class FileUploaderPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private MediaUploadResult uploadToCatboxSync(Uri fileUri) {
|
private MediaUploadResult uploadToCatboxSync(Uri fileUri) {
|
||||||
try {
|
JSObject statusUpdate = new JSObject();
|
||||||
File file = FileUtils.getFileFromUri(getContext(), fileUri);
|
statusUpdate.put("status", "Uploading to catbox.moe...");
|
||||||
if (file == null) {
|
notifyListeners("uploadStatus", statusUpdate);
|
||||||
return new MediaUploadResult(false, null, "Could not resolve file");
|
|
||||||
}
|
|
||||||
|
|
||||||
JSObject statusUpdate = new JSObject();
|
return CatboxUploader.upload(getContext(), fileUri, CATBOX_TIMEOUT_SEC);
|
||||||
statusUpdate.put("status", "Uploading to catbox.moe...");
|
|
||||||
notifyListeners("uploadStatus", statusUpdate);
|
|
||||||
|
|
||||||
OkHttpClient client =
|
|
||||||
new OkHttpClient.Builder()
|
|
||||||
.connectTimeout(CATBOX_TIMEOUT_SEC, TimeUnit.SECONDS)
|
|
||||||
.writeTimeout(CATBOX_TIMEOUT_SEC, TimeUnit.SECONDS)
|
|
||||||
.readTimeout(CATBOX_TIMEOUT_SEC, TimeUnit.SECONDS)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
RequestBody requestBody =
|
|
||||||
new MultipartBody.Builder()
|
|
||||||
.setType(MultipartBody.FORM)
|
|
||||||
.addFormDataPart("reqtype", "fileupload")
|
|
||||||
.addFormDataPart(
|
|
||||||
"fileToUpload",
|
|
||||||
file.getName(),
|
|
||||||
RequestBody.create(
|
|
||||||
MediaType.parse("application/octet-stream"), file))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
Request request =
|
|
||||||
new Request.Builder().url("https://catbox.moe/user/api.php").post(requestBody).build();
|
|
||||||
|
|
||||||
try (Response response = client.newCall(request).execute()) {
|
|
||||||
if (!response.isSuccessful()) {
|
|
||||||
return new MediaUploadResult(
|
|
||||||
false, null, "Unexpected response " + response.code());
|
|
||||||
}
|
|
||||||
if (response.body() == null) {
|
|
||||||
return new MediaUploadResult(false, null, "Empty response body");
|
|
||||||
}
|
|
||||||
String url = response.body().string();
|
|
||||||
Log.d(TAG, "Catbox upload successful. URL: " + url);
|
|
||||||
return new MediaUploadResult(true, url.trim(), null);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "Catbox upload failed", e);
|
|
||||||
return new MediaUploadResult(false, null, e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private MediaUploadResult uploadViaWebViewSync(Uri fileUri, String provider) {
|
private MediaUploadResult uploadViaWebViewSync(Uri fileUri, String provider) {
|
||||||
ContentResolver resolver = getContext().getContentResolver();
|
JSObject statusUpdate = new JSObject();
|
||||||
byte[] fileBytes;
|
statusUpdate.put("status", "Uploading to " + provider + "...");
|
||||||
String mimeType;
|
notifyListeners("uploadStatus", statusUpdate);
|
||||||
try {
|
|
||||||
try (InputStream is = resolver.openInputStream(fileUri)) {
|
|
||||||
if (is == null) {
|
|
||||||
return new MediaUploadResult(false, null, "Could not open file stream");
|
|
||||||
}
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
byte[] buf = new byte[8192];
|
|
||||||
int n;
|
|
||||||
while ((n = is.read(buf)) != -1) {
|
|
||||||
baos.write(buf, 0, n);
|
|
||||||
}
|
|
||||||
fileBytes = baos.toByteArray();
|
|
||||||
}
|
|
||||||
String resolvedMime = resolver.getType(fileUri);
|
|
||||||
mimeType = (resolvedMime == null || resolvedMime.isEmpty())
|
|
||||||
? "application/octet-stream" : resolvedMime;
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "Failed to read file", e);
|
|
||||||
return new MediaUploadResult(false, null, "Could not read file: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
final byte[] finalFileBytes = fileBytes;
|
|
||||||
final String finalMimeType = mimeType;
|
|
||||||
CountDownLatch latch = new CountDownLatch(1);
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
AtomicReference<MediaUploadResult> resultRef = new AtomicReference<>();
|
AtomicReference<MediaUploadResult> resultRef = new AtomicReference<>();
|
||||||
|
|
||||||
@@ -291,12 +229,10 @@ public class FileUploaderPlugin extends Plugin {
|
|||||||
MediaUploadAutomationRunner runner =
|
MediaUploadAutomationRunner runner =
|
||||||
new MediaUploadAutomationRunner(
|
new MediaUploadAutomationRunner(
|
||||||
getContext(),
|
getContext(),
|
||||||
finalFileBytes,
|
fileUri,
|
||||||
fileName,
|
fileName,
|
||||||
finalMimeType,
|
|
||||||
provider,
|
provider,
|
||||||
callback,
|
callback);
|
||||||
null);
|
|
||||||
runner.run();
|
runner.run();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import android.annotation.SuppressLint;
|
|||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.webkit.CookieManager;
|
||||||
import android.webkit.ValueCallback;
|
import android.webkit.ValueCallback;
|
||||||
import android.webkit.WebChromeClient;
|
import android.webkit.WebChromeClient;
|
||||||
import android.webkit.WebSettings;
|
import android.webkit.WebSettings;
|
||||||
@@ -63,9 +65,9 @@ interface MediaUploadCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Non-interactive WebView automation for imgur uploads.
|
* Non-interactive WebView automation for provider uploads.
|
||||||
* Primary path injects file bytes with DataTransfer, then polls for success/blocked.
|
* Android uploads use the WebView file chooser when providers support it. ImgBB and fixture
|
||||||
* Legacy Uri chooser interception is retained only as passive fallback.
|
* tests use DataTransfer byte injection, then poll for success/blocked.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
public class MediaUploadAutomationRunner {
|
public class MediaUploadAutomationRunner {
|
||||||
@@ -80,28 +82,33 @@ public class MediaUploadAutomationRunner {
|
|||||||
static final String STAGE_SUCCESS_SELECTOR_MATCHED = "success_selector_matched";
|
static final String STAGE_SUCCESS_SELECTOR_MATCHED = "success_selector_matched";
|
||||||
static final String STAGE_BLOCKED_DETECTED = "blocked_detected";
|
static final String STAGE_BLOCKED_DETECTED = "blocked_detected";
|
||||||
static final String STAGE_INPUT_NOT_FOUND = "input_not_found";
|
static final String STAGE_INPUT_NOT_FOUND = "input_not_found";
|
||||||
|
static final String STAGE_CHOOSER_NOT_TRIGGERED = "chooser_not_triggered";
|
||||||
static final String STAGE_FILE_PAYLOAD_UNAVAILABLE = "file_payload_unavailable";
|
static final String STAGE_FILE_PAYLOAD_UNAVAILABLE = "file_payload_unavailable";
|
||||||
static final String STAGE_UPLOAD_TIMED_OUT = "upload_timed_out";
|
static final String STAGE_UPLOAD_TIMED_OUT = "upload_timed_out";
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
private final Uri fileUri;
|
||||||
private final String fileName;
|
private final String fileName;
|
||||||
private final byte[] fileBytes;
|
private byte[] fileBytes;
|
||||||
private final String mimeType;
|
private String mimeType;
|
||||||
private final String provider;
|
private final String provider;
|
||||||
private final MediaUploadCallback callback;
|
private final MediaUploadCallback callback;
|
||||||
/** When non-null (test fixtures), use instead of getUploadUrl(provider). */
|
/** When non-null (test fixtures), use instead of getUploadUrl(provider). */
|
||||||
private final String overrideUploadUrl;
|
private final String overrideUploadUrl;
|
||||||
|
|
||||||
private WebView webView;
|
private WebView webView;
|
||||||
|
private ValueCallback<Uri[]> filePathCallback;
|
||||||
private final Handler mainHandler;
|
private final Handler mainHandler;
|
||||||
private final Runnable pollRunnable;
|
private final Runnable pollRunnable;
|
||||||
|
private final Runnable overallTimeoutRunnable;
|
||||||
private boolean finished;
|
private boolean finished;
|
||||||
private boolean fileInjected;
|
private boolean fileInjected;
|
||||||
|
private boolean dataTransferFallbackAttempted;
|
||||||
private boolean submitClicked;
|
private boolean submitClicked;
|
||||||
private long startTime;
|
private long startTime;
|
||||||
/** Last matched selector (for diagnostics). */
|
/** Last matched selector (for diagnostics). */
|
||||||
private String lastMatchedSelector;
|
private String lastMatchedSelector;
|
||||||
/** Number of injection attempts so far (for diagnostics). */
|
/** Number of trigger or injection attempts so far (for diagnostics). */
|
||||||
private int triggerAttemptCount;
|
private int triggerAttemptCount;
|
||||||
|
|
||||||
private static final class ResolvedFilePayload {
|
private static final class ResolvedFilePayload {
|
||||||
@@ -155,6 +162,7 @@ public class MediaUploadAutomationRunner {
|
|||||||
MediaUploadCallback callback,
|
MediaUploadCallback callback,
|
||||||
String overrideUploadUrl) {
|
String overrideUploadUrl) {
|
||||||
this.context = context.getApplicationContext();
|
this.context = context.getApplicationContext();
|
||||||
|
this.fileUri = null;
|
||||||
this.fileName = fileName != null ? fileName : "file";
|
this.fileName = fileName != null ? fileName : "file";
|
||||||
this.fileBytes = fileBytes;
|
this.fileBytes = fileBytes;
|
||||||
this.mimeType = mimeType != null ? mimeType : "application/octet-stream";
|
this.mimeType = mimeType != null ? mimeType : "application/octet-stream";
|
||||||
@@ -163,20 +171,18 @@ public class MediaUploadAutomationRunner {
|
|||||||
this.overrideUploadUrl = overrideUploadUrl;
|
this.overrideUploadUrl = overrideUploadUrl;
|
||||||
this.mainHandler = new Handler(Looper.getMainLooper());
|
this.mainHandler = new Handler(Looper.getMainLooper());
|
||||||
this.pollRunnable = this::pollForResult;
|
this.pollRunnable = this::pollForResult;
|
||||||
|
this.overallTimeoutRunnable = this::handleOverallTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Real Android upload constructor: prefer native WebView file chooser with this Uri. */
|
||||||
* Legacy constructor (Uri-based). Kept for backward compat; prefer bytes constructor.
|
|
||||||
* Resolves bytes once so legacy callers still use DataTransfer by default.
|
|
||||||
*/
|
|
||||||
public MediaUploadAutomationRunner(
|
public MediaUploadAutomationRunner(
|
||||||
Context context, Uri fileUri, String fileName, String provider, MediaUploadCallback callback) {
|
Context context, Uri fileUri, String fileName, String provider, MediaUploadCallback callback) {
|
||||||
this(context, fileUri, fileName, provider, callback, null);
|
this(context, fileUri, fileName, provider, callback, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Legacy test constructor: overrideUploadUrl loads fixture instead of live provider URL.
|
* Uri constructor with optional fixture URL.
|
||||||
* Package-visible for instrumentation tests. Requires fileBytes to be passed via bytes constructor.
|
* Package-visible for instrumentation tests, though fixture tests usually use the bytes constructor.
|
||||||
*/
|
*/
|
||||||
MediaUploadAutomationRunner(
|
MediaUploadAutomationRunner(
|
||||||
Context context,
|
Context context,
|
||||||
@@ -186,15 +192,21 @@ public class MediaUploadAutomationRunner {
|
|||||||
MediaUploadCallback callback,
|
MediaUploadCallback callback,
|
||||||
String overrideUploadUrl) {
|
String overrideUploadUrl) {
|
||||||
this.context = context.getApplicationContext();
|
this.context = context.getApplicationContext();
|
||||||
|
this.fileUri = fileUri;
|
||||||
this.fileName = fileName != null ? fileName : "file";
|
this.fileName = fileName != null ? fileName : "file";
|
||||||
ResolvedFilePayload payload = resolveFilePayload(this.context, fileUri);
|
this.fileBytes = null;
|
||||||
this.fileBytes = payload.fileBytes;
|
ContentResolver resolver = this.context.getContentResolver();
|
||||||
this.mimeType = payload.mimeType;
|
String resolvedMime = fileUri != null ? resolver.getType(fileUri) : null;
|
||||||
|
this.mimeType =
|
||||||
|
(resolvedMime == null || resolvedMime.isEmpty())
|
||||||
|
? "application/octet-stream"
|
||||||
|
: resolvedMime;
|
||||||
this.provider = provider;
|
this.provider = provider;
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
this.overrideUploadUrl = overrideUploadUrl;
|
this.overrideUploadUrl = overrideUploadUrl;
|
||||||
this.mainHandler = new Handler(Looper.getMainLooper());
|
this.mainHandler = new Handler(Looper.getMainLooper());
|
||||||
this.pollRunnable = this::pollForResult;
|
this.pollRunnable = this::pollForResult;
|
||||||
|
this.overallTimeoutRunnable = this::handleOverallTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -209,6 +221,38 @@ public class MediaUploadAutomationRunner {
|
|||||||
Log.d(TAG, "[" + provider + "] " + stage + " elapsed=" + elapsedMs() + "ms");
|
Log.d(TAG, "[" + provider + "] " + stage + " elapsed=" + elapsedMs() + "ms");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean shouldUseFileChooser() {
|
||||||
|
return fileUri != null
|
||||||
|
&& overrideUploadUrl == null
|
||||||
|
&& !MediaUploadRecipes.PROVIDER_IMGBB.equals(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean ensureFilePayload() {
|
||||||
|
if (fileBytes != null && fileBytes.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (fileUri == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ResolvedFilePayload payload = resolveFilePayload(context, fileUri);
|
||||||
|
fileBytes = payload.fileBytes;
|
||||||
|
mimeType = payload.mimeType;
|
||||||
|
return fileBytes != null && fileBytes.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleOverallTimeout() {
|
||||||
|
if (finished) return;
|
||||||
|
finish(
|
||||||
|
new MediaUploadResult(
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
"Upload timeout",
|
||||||
|
STAGE_UPLOAD_TIMED_OUT,
|
||||||
|
elapsedMs(),
|
||||||
|
lastMatchedSelector,
|
||||||
|
triggerAttemptCount));
|
||||||
|
}
|
||||||
|
|
||||||
private void startWebView() {
|
private void startWebView() {
|
||||||
webView = new WebView(context);
|
webView = new WebView(context);
|
||||||
webView.setVisibility(android.view.View.GONE);
|
webView.setVisibility(android.view.View.GONE);
|
||||||
@@ -219,6 +263,13 @@ public class MediaUploadAutomationRunner {
|
|||||||
settings.setAllowContentAccess(true);
|
settings.setAllowContentAccess(true);
|
||||||
settings.setLoadWithOverviewMode(true);
|
settings.setLoadWithOverviewMode(true);
|
||||||
settings.setUseWideViewPort(true);
|
settings.setUseWideViewPort(true);
|
||||||
|
settings.setUserAgentString(
|
||||||
|
"Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36");
|
||||||
|
CookieManager cookieManager = CookieManager.getInstance();
|
||||||
|
cookieManager.setAcceptCookie(true);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
cookieManager.setAcceptThirdPartyCookies(webView, true);
|
||||||
|
}
|
||||||
|
|
||||||
webView.setWebChromeClient(
|
webView.setWebChromeClient(
|
||||||
new WebChromeClient() {
|
new WebChromeClient() {
|
||||||
@@ -227,10 +278,18 @@ public class MediaUploadAutomationRunner {
|
|||||||
WebView webView,
|
WebView webView,
|
||||||
ValueCallback<Uri[]> filePathCallback,
|
ValueCallback<Uri[]> filePathCallback,
|
||||||
FileChooserParams fileChooserParams) {
|
FileChooserParams fileChooserParams) {
|
||||||
// Passive fallback only: observe callback, do not drive chooser flow.
|
if (!shouldUseFileChooser()) {
|
||||||
Log.d(TAG, "[" + provider + "] chooser callback observed (passive fallback)");
|
return false;
|
||||||
|
}
|
||||||
logStage(STAGE_FILE_CHOOSER_CALLBACK);
|
logStage(STAGE_FILE_CHOOSER_CALLBACK);
|
||||||
return false;
|
MediaUploadAutomationRunner.this.filePathCallback = filePathCallback;
|
||||||
|
fileInjected = true;
|
||||||
|
schedulePoll();
|
||||||
|
if (fileUri != null) {
|
||||||
|
filePathCallback.onReceiveValue(new Uri[] {fileUri});
|
||||||
|
MediaUploadAutomationRunner.this.filePathCallback = null;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -239,7 +298,13 @@ public class MediaUploadAutomationRunner {
|
|||||||
@Override
|
@Override
|
||||||
public void onPageFinished(WebView view, String url) {
|
public void onPageFinished(WebView view, String url) {
|
||||||
logStage(STAGE_PAGE_LOADED);
|
logStage(STAGE_PAGE_LOADED);
|
||||||
if (fileBytes == null || fileBytes.length == 0) {
|
if (shouldUseFileChooser()) {
|
||||||
|
mainHandler.postDelayed(
|
||||||
|
MediaUploadAutomationRunner.this::triggerFileInput,
|
||||||
|
MediaUploadRecipes.TRIGGER_INITIAL_DELAY_MS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ensureFilePayload()) {
|
||||||
finish(
|
finish(
|
||||||
new MediaUploadResult(
|
new MediaUploadResult(
|
||||||
false,
|
false,
|
||||||
@@ -266,12 +331,70 @@ public class MediaUploadAutomationRunner {
|
|||||||
}
|
}
|
||||||
startTime = System.currentTimeMillis();
|
startTime = System.currentTimeMillis();
|
||||||
webView.loadUrl(uploadUrl);
|
webView.loadUrl(uploadUrl);
|
||||||
|
mainHandler.postDelayed(overallTimeoutRunnable, MediaUploadRecipes.getUploadTimeoutMs(provider));
|
||||||
Log.d(TAG, "Loaded " + uploadUrl + " for provider " + provider);
|
Log.d(TAG, "Loaded " + uploadUrl + " for provider " + provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void triggerFileInput() {
|
||||||
|
if (finished || fileInjected) return;
|
||||||
|
String js = MediaUploadRecipes.getTriggerFileInputJs(provider);
|
||||||
|
if (js == null) {
|
||||||
|
finish(
|
||||||
|
new MediaUploadResult(
|
||||||
|
false, null, "No trigger JS for " + provider, "no_recipe", elapsedMs(), null));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
triggerAttemptCount++;
|
||||||
|
webView.evaluateJavascript(
|
||||||
|
js,
|
||||||
|
value -> {
|
||||||
|
if (finished || fileInjected) return;
|
||||||
|
String raw = value == null ? "" : value.trim();
|
||||||
|
String unquoted =
|
||||||
|
raw.replaceAll("^\"|\"$", "")
|
||||||
|
.replace("\\u003d", "=")
|
||||||
|
.replace("\\\"", "\"")
|
||||||
|
.trim();
|
||||||
|
boolean matched = !"false".equals(raw) && unquoted.length() > 0;
|
||||||
|
if (matched) {
|
||||||
|
lastMatchedSelector = unquoted;
|
||||||
|
logStage(STAGE_SELECTOR_MATCHED);
|
||||||
|
}
|
||||||
|
scheduleTriggerRetry();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleTriggerRetry() {
|
||||||
|
if (finished || fileInjected) return;
|
||||||
|
long elapsed = elapsedMs();
|
||||||
|
if (elapsed >= MediaUploadRecipes.FILE_INPUT_TIMEOUT_MS) {
|
||||||
|
if (lastMatchedSelector != null && !dataTransferFallbackAttempted) {
|
||||||
|
dataTransferFallbackAttempted = true;
|
||||||
|
injectFileViaDataTransfer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String stage = lastMatchedSelector != null ? STAGE_CHOOSER_NOT_TRIGGERED : STAGE_INPUT_NOT_FOUND;
|
||||||
|
String error =
|
||||||
|
lastMatchedSelector != null
|
||||||
|
? "File chooser not triggered"
|
||||||
|
: "File input not found";
|
||||||
|
finish(
|
||||||
|
new MediaUploadResult(
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
error,
|
||||||
|
stage,
|
||||||
|
elapsed,
|
||||||
|
lastMatchedSelector,
|
||||||
|
triggerAttemptCount));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mainHandler.postDelayed(this::triggerFileInput, MediaUploadRecipes.TRIGGER_RETRY_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
private void injectFileViaDataTransfer() {
|
private void injectFileViaDataTransfer() {
|
||||||
if (finished || fileInjected) return;
|
if (finished || fileInjected) return;
|
||||||
if (fileBytes == null || fileBytes.length == 0) {
|
if (!ensureFilePayload()) {
|
||||||
finish(
|
finish(
|
||||||
new MediaUploadResult(
|
new MediaUploadResult(
|
||||||
false,
|
false,
|
||||||
@@ -353,7 +476,7 @@ public class MediaUploadAutomationRunner {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional submit step after DataTransfer injection: some providers need explicit submit.
|
// Optional submit step after file selection/injection: some providers need explicit submit.
|
||||||
// Retry submit attempts until one actually clicks; some pages render/enable controls late.
|
// Retry submit attempts until one actually clicks; some pages render/enable controls late.
|
||||||
if (fileInjected && !submitClicked) {
|
if (fileInjected && !submitClicked) {
|
||||||
String submitJs = MediaUploadRecipes.getSubmitClickJs(provider);
|
String submitJs = MediaUploadRecipes.getSubmitClickJs(provider);
|
||||||
@@ -422,25 +545,38 @@ public class MediaUploadAutomationRunner {
|
|||||||
if (finished) return;
|
if (finished) return;
|
||||||
finished = true;
|
finished = true;
|
||||||
mainHandler.removeCallbacks(pollRunnable);
|
mainHandler.removeCallbacks(pollRunnable);
|
||||||
|
mainHandler.removeCallbacks(overallTimeoutRunnable);
|
||||||
|
if (filePathCallback != null) {
|
||||||
|
try {
|
||||||
|
filePathCallback.onReceiveValue(null);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
filePathCallback = null;
|
||||||
|
}
|
||||||
tearDown();
|
tearDown();
|
||||||
callback.onComplete(result);
|
callback.onComplete(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void tearDown() {
|
private void tearDown() {
|
||||||
mainHandler.post(
|
Runnable cleanup =
|
||||||
() -> {
|
() -> {
|
||||||
if (webView != null) {
|
if (webView == null) {
|
||||||
try {
|
return;
|
||||||
webView.stopLoading();
|
|
||||||
webView.clearHistory();
|
|
||||||
webView.clearCache(true);
|
|
||||||
webView.clearSslPreferences();
|
|
||||||
webView.destroy();
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.w(TAG, "Teardown warning", e);
|
|
||||||
}
|
|
||||||
webView = null;
|
|
||||||
}
|
}
|
||||||
});
|
try {
|
||||||
|
webView.stopLoading();
|
||||||
|
webView.clearHistory();
|
||||||
|
webView.clearCache(true);
|
||||||
|
webView.clearSslPreferences();
|
||||||
|
webView.destroy();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "Teardown warning", e);
|
||||||
|
}
|
||||||
|
webView = null;
|
||||||
|
};
|
||||||
|
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||||
|
cleanup.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mainHandler.post(cleanup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,26 @@ package fivechan.android;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Provider-specific recipes for automated media upload via WebView.
|
* Provider-specific recipes for automated media upload via WebView.
|
||||||
* Reconciled with electron/media-upload-recipes.js for imgur.
|
* Reconciled with electron/media-upload-recipes.js for imgbb and imgur.
|
||||||
*
|
*
|
||||||
* Android vs Electron (prevent drift):
|
* Android vs Electron (prevent drift):
|
||||||
* - Android: imgur only; no catbox (uses OkHttp). Uses DataTransfer JS injection
|
* - Android app uploads use native catbox and WebView automation for imgbb. The imgur runner
|
||||||
* (no user activation); optional submit click after injection. onShowFileChooser kept as passive fallback.
|
* is retained for diagnostics/tests only. Real Uri attempts use the WebView file chooser
|
||||||
* - Electron: catbox/imgur. CDP DOM.setFileInputFiles + submit click. Catbox Electron-only.
|
* callback; fixtures/fallback use DataTransfer JS injection.
|
||||||
* - Selectors: file input, submit, success extractor, blocked indicators kept in sync for imgur.
|
* - Electron: catbox/imgur/imgbb. CDP DOM.setFileInputFiles + submit click.
|
||||||
|
* - Selectors: file input, submit, success extractor, blocked indicators kept in sync for imgbb
|
||||||
|
* and imgur.
|
||||||
*/
|
*/
|
||||||
public final class MediaUploadRecipes {
|
public final class MediaUploadRecipes {
|
||||||
|
|
||||||
public static final String PROVIDER_IMGUR = "imgur";
|
public static final String PROVIDER_IMGUR = "imgur";
|
||||||
|
public static final String PROVIDER_IMGBB = "imgbb";
|
||||||
|
|
||||||
/** Max time to wait for upload completion (ms). */
|
/** Max time to wait for upload completion (ms). */
|
||||||
public static final long UPLOAD_TIMEOUT_MS = 45_000;
|
public static final long UPLOAD_TIMEOUT_MS = 45_000;
|
||||||
|
public static final long IMGBB_UPLOAD_TIMEOUT_MS = 60_000;
|
||||||
/** Max time to wait for provider file input to be found/triggered (ms). */
|
/** Max time to wait for provider file input to be found/triggered (ms). */
|
||||||
public static final long FILE_INPUT_TIMEOUT_MS = 8_000;
|
public static final long FILE_INPUT_TIMEOUT_MS = 15_000;
|
||||||
/** Poll interval for success/blocked checks (ms). */
|
/** Poll interval for success/blocked checks (ms). */
|
||||||
public static final long POLL_INTERVAL_MS = 500;
|
public static final long POLL_INTERVAL_MS = 500;
|
||||||
/** Initial delay before first file-input trigger attempt (ms). SPAs may need settle time. */
|
/** Initial delay before first file-input trigger attempt (ms). SPAs may need settle time. */
|
||||||
@@ -27,15 +31,29 @@ public final class MediaUploadRecipes {
|
|||||||
|
|
||||||
private MediaUploadRecipes() {}
|
private MediaUploadRecipes() {}
|
||||||
|
|
||||||
|
public static boolean isWebViewProvider(String provider) {
|
||||||
|
return PROVIDER_IMGUR.equals(provider) || PROVIDER_IMGBB.equals(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAndroidUploadProvider(String provider) {
|
||||||
|
return PROVIDER_IMGBB.equals(provider);
|
||||||
|
}
|
||||||
|
|
||||||
public static String getUploadUrl(String provider) {
|
public static String getUploadUrl(String provider) {
|
||||||
if (PROVIDER_IMGUR.equals(provider)) {
|
if (PROVIDER_IMGUR.equals(provider)) {
|
||||||
return "https://imgur.com/upload";
|
return "https://imgur.com/upload";
|
||||||
}
|
}
|
||||||
|
if (PROVIDER_IMGBB.equals(provider)) {
|
||||||
|
return "https://imgbb.com/";
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns upload timeout. */
|
/** Returns upload timeout. */
|
||||||
public static long getUploadTimeoutMs(String provider) {
|
public static long getUploadTimeoutMs(String provider) {
|
||||||
|
if (PROVIDER_IMGBB.equals(provider)) {
|
||||||
|
return IMGBB_UPLOAD_TIMEOUT_MS;
|
||||||
|
}
|
||||||
return UPLOAD_TIMEOUT_MS;
|
return UPLOAD_TIMEOUT_MS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,9 +71,25 @@ public final class MediaUploadRecipes {
|
|||||||
"[type=\"submit\"]",
|
"[type=\"submit\"]",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (PROVIDER_IMGBB.equals(provider)) {
|
||||||
|
return buildImgbbSubmitClickJs();
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String buildImgbbSubmitClickJs() {
|
||||||
|
return "(function(){"
|
||||||
|
+ "var select=document.querySelector('#upload-expiration,select[name=\"upload-expiration\"]');"
|
||||||
|
+ "if(select){select.value='';select.dispatchEvent(new Event('change',{bubbles:true}));}"
|
||||||
|
+ "function visible(el){if(!el)return false;var r=el.getBoundingClientRect();var s=getComputedStyle(el);"
|
||||||
|
+ "return r.width>0&&r.height>0&&s.display!=='none'&&s.visibility!=='hidden'&&!el.disabled&&!(el.classList&&el.classList.contains('disabled'));}"
|
||||||
|
+ "var selectors=['button[data-action=\"upload\"]','button.btn.green','button[type=\"submit\"]','[data-action=\"upload\"]'];"
|
||||||
|
+ "for(var i=0;i<selectors.length;i++){var nodes=document.querySelectorAll(selectors[i]);"
|
||||||
|
+ "for(var j=0;j<nodes.length;j++){var n=nodes[j];var txt=((n.textContent||n.value||'')+'').toLowerCase();"
|
||||||
|
+ "if(visible(n)&&(txt.indexOf('upload')!==-1||n.getAttribute('data-action')==='upload')){n.click();return true;}}}"
|
||||||
|
+ "return false;})()";
|
||||||
|
}
|
||||||
|
|
||||||
private static String buildSubmitClickJs(String[] selectors) {
|
private static String buildSubmitClickJs(String[] selectors) {
|
||||||
StringBuilder sb = new StringBuilder("(function(){var s=[");
|
StringBuilder sb = new StringBuilder("(function(){var s=[");
|
||||||
for (int i = 0; i < selectors.length; i++) {
|
for (int i = 0; i < selectors.length; i++) {
|
||||||
@@ -99,6 +133,19 @@ public final class MediaUploadRecipes {
|
|||||||
fileName,
|
fileName,
|
||||||
mimeType);
|
mimeType);
|
||||||
}
|
}
|
||||||
|
if (PROVIDER_IMGBB.equals(provider)) {
|
||||||
|
return buildFileInjectionJs(
|
||||||
|
new String[] {
|
||||||
|
"#anywhere-upload-input",
|
||||||
|
"input[data-action=\"anywhere-upload-input\"]",
|
||||||
|
"input[type=\"file\"]",
|
||||||
|
"input[type=file]",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
base64Data,
|
||||||
|
fileName,
|
||||||
|
mimeType);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,6 +192,18 @@ public final class MediaUploadRecipes {
|
|||||||
},
|
},
|
||||||
false);
|
false);
|
||||||
}
|
}
|
||||||
|
if (PROVIDER_IMGBB.equals(provider)) {
|
||||||
|
return buildTriggerFileInputJs(
|
||||||
|
new String[] {
|
||||||
|
".btn.btn-big.blue",
|
||||||
|
"[data-action=\"top-bar-upload\"]",
|
||||||
|
"#anywhere-upload-input",
|
||||||
|
"input[data-action=\"anywhere-upload-input\"]",
|
||||||
|
"input[type=\"file\"]",
|
||||||
|
"input[type=file]",
|
||||||
|
},
|
||||||
|
false);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,13 +243,24 @@ public final class MediaUploadRecipes {
|
|||||||
"video source[src*=\"i.imgur.com\"]",
|
"video source[src*=\"i.imgur.com\"]",
|
||||||
"video[src*=\"i.imgur.com\"]",
|
"video[src*=\"i.imgur.com\"]",
|
||||||
};
|
};
|
||||||
return buildImgurSuccessJs(selectorCandidates);
|
return buildDirectHostSuccessJs(selectorCandidates, "i.imgur.com");
|
||||||
} else {
|
} else if (PROVIDER_IMGBB.equals(provider)) {
|
||||||
return null;
|
selectorCandidates =
|
||||||
|
new String[] {
|
||||||
|
"input[name=\"html-embed-medium\"]",
|
||||||
|
"textarea[name=\"html-embed-medium\"]",
|
||||||
|
"#uploaded-embed-code-1",
|
||||||
|
"input[value*=\"i.ibb.co\"]",
|
||||||
|
"textarea",
|
||||||
|
"img[src*=\"i.ibb.co\"]",
|
||||||
|
};
|
||||||
|
return buildDirectHostSuccessJs(selectorCandidates, "i.ibb.co");
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String buildImgurSuccessJs(String[] selectors) {
|
private static String buildDirectHostSuccessJs(String[] selectors, String directHost) {
|
||||||
|
String escapedDirectHost = escapeJs(directHost);
|
||||||
StringBuilder sb = new StringBuilder("(function(){var s=[");
|
StringBuilder sb = new StringBuilder("(function(){var s=[");
|
||||||
for (int i = 0; i < selectors.length; i++) {
|
for (int i = 0; i < selectors.length; i++) {
|
||||||
if (i > 0) sb.append(",");
|
if (i > 0) sb.append(",");
|
||||||
@@ -201,16 +271,26 @@ public final class MediaUploadRecipes {
|
|||||||
+ "function norm(v){if(!v)return null;v=String(v).trim();if(!v)return null;"
|
+ "function norm(v){if(!v)return null;v=String(v).trim();if(!v)return null;"
|
||||||
+ "if(v.indexOf('//')===0)v='https:'+v;if(v.indexOf('http')!==0)return null;return v;}"
|
+ "if(v.indexOf('//')===0)v='https:'+v;if(v.indexOf('http')!==0)return null;return v;}"
|
||||||
+ "function hasMediaExt(u){return /\\.(?:jpe?g|png|gif|webp|bmp|avif|mp4|webm)(?:[?#].*)?$/i.test(u);}"
|
+ "function hasMediaExt(u){return /\\.(?:jpe?g|png|gif|webp|bmp|avif|mp4|webm)(?:[?#].*)?$/i.test(u);}"
|
||||||
+ "function isDirectHost(u){try{var p=new URL(u,location.href);return p.hostname==='i.imgur.com';}catch(e){return false;}}"
|
+ "function isDirectHost(u){try{var p=new URL(u,location.href);return p.hostname==='"
|
||||||
|
+ escapedDirectHost
|
||||||
|
+ "';}catch(e){return false;}}"
|
||||||
+ "function pick(v){var u=norm(v);if(!u)return null;if(isDirectHost(u)&&hasMediaExt(u))return u;return null;}"
|
+ "function pick(v){var u=norm(v);if(!u)return null;if(isDirectHost(u)&&hasMediaExt(u))return u;return null;}"
|
||||||
|
+ "function pickFromText(v){var t=String(v||'');var m=t.match(/https?:\\/\\/[^\\s\"'<>\\[\\]]+/g)||[];"
|
||||||
|
+ "for(var k=0;k<m.length;k++){var r=pick(m[k]);if(r)return r;}return pick(t);}"
|
||||||
+ "for(var i=0;i<s.length;i++){try{var el=document.querySelector(s[i]);if(!el)continue;"
|
+ "for(var i=0;i<s.length;i++){try{var el=document.querySelector(s[i]);if(!el)continue;"
|
||||||
+ "var c=[el.value,el.getAttribute('value'),el.getAttribute('href'),el.href,el.getAttribute('content'),el.getAttribute('src'),el.src,el.getAttribute('data-src'),el.getAttribute('data-link'),el.getAttribute('data-clipboard-text'),el.textContent];"
|
+ "var c=[el.value,el.getAttribute('value'),el.getAttribute('href'),el.href,el.getAttribute('content'),el.getAttribute('src'),el.src,el.getAttribute('data-src'),el.getAttribute('data-link'),el.getAttribute('data-clipboard-text'),el.textContent];"
|
||||||
+ "for(var j=0;j<c.length;j++){var r=pick(c[j]);if(r)return r;}"
|
+ "for(var j=0;j<c.length;j++){var r=pickFromText(c[j]);if(r)return r;}"
|
||||||
+ "}catch(e){}}"
|
+ "}catch(e){}}"
|
||||||
+ "var og=document.querySelector('meta[property=\"og:image\"],meta[name=\"og:image\"],meta[name=\"twitter:image\"],meta[property=\"twitter:image\"]');"
|
+ "var og=document.querySelector('meta[property=\"og:image\"],meta[name=\"og:image\"],meta[name=\"twitter:image\"],meta[property=\"twitter:image\"]');"
|
||||||
+ "if(og){var ro=pick(og.getAttribute('content'));if(ro)return ro;}"
|
+ "if(og){var ro=pickFromText(og.getAttribute('content'));if(ro)return ro;}"
|
||||||
+ "var media=document.querySelector('img[src*=\"i.imgur.com\"],video source[src*=\"i.imgur.com\"],video[src*=\"i.imgur.com\"]');"
|
+ "var media=document.querySelector('img[src*=\""
|
||||||
+ "if(media){var rm=pick(media.getAttribute('src')||media.src);if(rm)return rm;}"
|
+ escapedDirectHost
|
||||||
|
+ "\"],video source[src*=\""
|
||||||
|
+ escapedDirectHost
|
||||||
|
+ "\"],video[src*=\""
|
||||||
|
+ escapedDirectHost
|
||||||
|
+ "\"]');"
|
||||||
|
+ "if(media){var rm=pickFromText(media.getAttribute('src')||media.src);if(rm)return rm;}"
|
||||||
+ "return null;})()");
|
+ "return null;})()");
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
@@ -243,6 +323,19 @@ public final class MediaUploadRecipes {
|
|||||||
".login",
|
".login",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (PROVIDER_IMGBB.equals(provider)) {
|
||||||
|
return buildBlockedJs(
|
||||||
|
new String[] {
|
||||||
|
"#challenge",
|
||||||
|
".captcha",
|
||||||
|
"[data-captcha]",
|
||||||
|
".g-recaptcha",
|
||||||
|
"#recaptcha",
|
||||||
|
".login-form",
|
||||||
|
".signin",
|
||||||
|
".login",
|
||||||
|
});
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ public class MediaUploadRecipesTest {
|
|||||||
public void uploadTimeout_exceedsFileInputTimeout() {
|
public void uploadTimeout_exceedsFileInputTimeout() {
|
||||||
assertTrue(
|
assertTrue(
|
||||||
MediaUploadRecipes.UPLOAD_TIMEOUT_MS > MediaUploadRecipes.FILE_INPUT_TIMEOUT_MS);
|
MediaUploadRecipes.UPLOAD_TIMEOUT_MS > MediaUploadRecipes.FILE_INPUT_TIMEOUT_MS);
|
||||||
|
assertTrue(
|
||||||
|
MediaUploadRecipes.IMGBB_UPLOAD_TIMEOUT_MS
|
||||||
|
> MediaUploadRecipes.FILE_INPUT_TIMEOUT_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -33,6 +36,13 @@ public class MediaUploadRecipesTest {
|
|||||||
MediaUploadRecipes.getUploadUrl(MediaUploadRecipes.PROVIDER_IMGUR));
|
MediaUploadRecipes.getUploadUrl(MediaUploadRecipes.PROVIDER_IMGUR));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getUploadUrl_imgbb() {
|
||||||
|
assertEquals(
|
||||||
|
"https://imgbb.com/",
|
||||||
|
MediaUploadRecipes.getUploadUrl(MediaUploadRecipes.PROVIDER_IMGBB));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getUploadUrl_unknownProvider_returnsNull() {
|
public void getUploadUrl_unknownProvider_returnsNull() {
|
||||||
assertNull(MediaUploadRecipes.getUploadUrl("catbox"));
|
assertNull(MediaUploadRecipes.getUploadUrl("catbox"));
|
||||||
@@ -48,6 +58,14 @@ public class MediaUploadRecipesTest {
|
|||||||
assertTrue(js.contains("data-file-input"));
|
assertTrue(js.contains("data-file-input"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getTriggerFileInputJs_imgbb_containsSelectors() {
|
||||||
|
String js = MediaUploadRecipes.getTriggerFileInputJs(MediaUploadRecipes.PROVIDER_IMGBB);
|
||||||
|
assertNotNull(js);
|
||||||
|
assertTrue(js.contains("anywhere-upload-input"));
|
||||||
|
assertTrue(js.contains("btn-big"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getTriggerFileInputJs_unknownProvider_returnsNull() {
|
public void getTriggerFileInputJs_unknownProvider_returnsNull() {
|
||||||
assertNull(MediaUploadRecipes.getTriggerFileInputJs("unknown"));
|
assertNull(MediaUploadRecipes.getTriggerFileInputJs("unknown"));
|
||||||
@@ -61,6 +79,15 @@ public class MediaUploadRecipesTest {
|
|||||||
assertTrue(js.contains("click"));
|
assertTrue(js.contains("click"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getSubmitClickJs_imgbb_setsNoAutodeleteAndClicksUpload() {
|
||||||
|
String js = MediaUploadRecipes.getSubmitClickJs(MediaUploadRecipes.PROVIDER_IMGBB);
|
||||||
|
assertNotNull(js);
|
||||||
|
assertTrue(js.contains("upload-expiration"));
|
||||||
|
assertTrue(js.contains("data-action"));
|
||||||
|
assertTrue(js.contains("click"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getSuccessJs_imgur_containsImgurSelectors() {
|
public void getSuccessJs_imgur_containsImgurSelectors() {
|
||||||
String js = MediaUploadRecipes.getSuccessJs(MediaUploadRecipes.PROVIDER_IMGUR);
|
String js = MediaUploadRecipes.getSuccessJs(MediaUploadRecipes.PROVIDER_IMGUR);
|
||||||
@@ -68,6 +95,14 @@ public class MediaUploadRecipesTest {
|
|||||||
assertTrue(js.contains("i.imgur.com"));
|
assertTrue(js.contains("i.imgur.com"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getSuccessJs_imgbb_containsImgbbSelectors() {
|
||||||
|
String js = MediaUploadRecipes.getSuccessJs(MediaUploadRecipes.PROVIDER_IMGBB);
|
||||||
|
assertNotNull(js);
|
||||||
|
assertTrue(js.contains("i.ibb.co"));
|
||||||
|
assertTrue(js.contains("html-embed-medium"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getBlockedJs_imgur_containsChallengeSelectors() {
|
public void getBlockedJs_imgur_containsChallengeSelectors() {
|
||||||
String js = MediaUploadRecipes.getBlockedJs(MediaUploadRecipes.PROVIDER_IMGUR);
|
String js = MediaUploadRecipes.getBlockedJs(MediaUploadRecipes.PROVIDER_IMGUR);
|
||||||
|
|||||||
Reference in New Issue
Block a user