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 android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
@@ -16,6 +17,7 @@ import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@@ -38,7 +40,13 @@ public class ImgurLiveUploadTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Imgur upload is no longer supported on Android WebView; keep for manual diagnostics.")
|
||||
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<>();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
MediaUploadCallback callback =
|
||||
|
||||
+5
-2
@@ -10,6 +10,7 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@@ -17,13 +18,15 @@ import org.junit.runner.RunWith;
|
||||
* Instrumentation tests for MediaUploadAutomationRunner against controlled HTML fixtures.
|
||||
* Uses DataTransfer injection (no chooser). Simulates: delayed DOM, missing selectors,
|
||||
* 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)
|
||||
public class MediaUploadAutomationRunnerTest {
|
||||
|
||||
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). */
|
||||
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;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
import androidx.activity.result.ActivityResult;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
@@ -29,13 +25,6 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
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")
|
||||
public class FileUploaderPlugin extends Plugin {
|
||||
private static final String TAG = "FileUploaderPlugin";
|
||||
@@ -46,9 +35,10 @@ public class FileUploaderPlugin extends Plugin {
|
||||
@PluginMethod
|
||||
public void pickAndUploadMedia(PluginCall call) {
|
||||
Log.d(TAG, "pickAndUploadMedia called");
|
||||
List<String> providerOrder = parseProviderOrder(call);
|
||||
List<String> providerOrder = getProviderOrder(call);
|
||||
if (providerOrder.isEmpty()) {
|
||||
providerOrder.add(PROVIDER_CATBOX);
|
||||
call.reject("No supported upload providers selected");
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
@@ -66,9 +56,12 @@ public class FileUploaderPlugin extends Plugin {
|
||||
Object o = arr.get(i);
|
||||
if (o instanceof String) {
|
||||
String p = (String) o;
|
||||
if (PROVIDER_CATBOX.equals(p)
|
||||
|| MediaUploadRecipes.PROVIDER_IMGUR.equals(p)) {
|
||||
if (PROVIDER_CATBOX.equals(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) {
|
||||
@@ -79,6 +72,14 @@ public class FileUploaderPlugin extends Plugin {
|
||||
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
|
||||
private void pickFileResult(PluginCall call, ActivityResult result) {
|
||||
Log.d(TAG, "pickFileResult callback received");
|
||||
@@ -103,9 +104,10 @@ public class FileUploaderPlugin extends Plugin {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> providerOrder = parseProviderOrder(call);
|
||||
List<String> providerOrder = getProviderOrder(call);
|
||||
if (providerOrder.isEmpty()) {
|
||||
providerOrder.add(PROVIDER_CATBOX);
|
||||
call.reject("No supported upload providers selected");
|
||||
return;
|
||||
}
|
||||
|
||||
new Thread(
|
||||
@@ -143,7 +145,7 @@ public class FileUploaderPlugin extends Plugin {
|
||||
}
|
||||
attempt.put("error", res.error);
|
||||
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);
|
||||
attempt.put("success", res.success);
|
||||
if (res.success) {
|
||||
@@ -195,82 +197,18 @@ public class FileUploaderPlugin extends Plugin {
|
||||
}
|
||||
|
||||
private MediaUploadResult uploadToCatboxSync(Uri fileUri) {
|
||||
try {
|
||||
File file = FileUtils.getFileFromUri(getContext(), fileUri);
|
||||
if (file == null) {
|
||||
return new MediaUploadResult(false, null, "Could not resolve file");
|
||||
}
|
||||
JSObject statusUpdate = new JSObject();
|
||||
statusUpdate.put("status", "Uploading to catbox.moe...");
|
||||
notifyListeners("uploadStatus", statusUpdate);
|
||||
|
||||
JSObject statusUpdate = new JSObject();
|
||||
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());
|
||||
}
|
||||
return CatboxUploader.upload(getContext(), fileUri, CATBOX_TIMEOUT_SEC);
|
||||
}
|
||||
|
||||
private MediaUploadResult uploadViaWebViewSync(Uri fileUri, String provider) {
|
||||
ContentResolver resolver = getContext().getContentResolver();
|
||||
byte[] fileBytes;
|
||||
String mimeType;
|
||||
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());
|
||||
}
|
||||
JSObject statusUpdate = new JSObject();
|
||||
statusUpdate.put("status", "Uploading to " + provider + "...");
|
||||
notifyListeners("uploadStatus", statusUpdate);
|
||||
|
||||
final byte[] finalFileBytes = fileBytes;
|
||||
final String finalMimeType = mimeType;
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<MediaUploadResult> resultRef = new AtomicReference<>();
|
||||
|
||||
@@ -291,12 +229,10 @@ public class FileUploaderPlugin extends Plugin {
|
||||
MediaUploadAutomationRunner runner =
|
||||
new MediaUploadAutomationRunner(
|
||||
getContext(),
|
||||
finalFileBytes,
|
||||
fileUri,
|
||||
fileName,
|
||||
finalMimeType,
|
||||
provider,
|
||||
callback,
|
||||
null);
|
||||
callback);
|
||||
runner.run();
|
||||
});
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ import android.annotation.SuppressLint;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.ValueCallback;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebSettings;
|
||||
@@ -63,9 +65,9 @@ interface MediaUploadCallback {
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-interactive WebView automation for imgur uploads.
|
||||
* Primary path injects file bytes with DataTransfer, then polls for success/blocked.
|
||||
* Legacy Uri chooser interception is retained only as passive fallback.
|
||||
* Non-interactive WebView automation for provider uploads.
|
||||
* Android uploads use the WebView file chooser when providers support it. ImgBB and fixture
|
||||
* tests use DataTransfer byte injection, then poll for success/blocked.
|
||||
*/
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
public class MediaUploadAutomationRunner {
|
||||
@@ -80,28 +82,33 @@ public class MediaUploadAutomationRunner {
|
||||
static final String STAGE_SUCCESS_SELECTOR_MATCHED = "success_selector_matched";
|
||||
static final String STAGE_BLOCKED_DETECTED = "blocked_detected";
|
||||
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_UPLOAD_TIMED_OUT = "upload_timed_out";
|
||||
|
||||
private final Context context;
|
||||
private final Uri fileUri;
|
||||
private final String fileName;
|
||||
private final byte[] fileBytes;
|
||||
private final String mimeType;
|
||||
private byte[] fileBytes;
|
||||
private String mimeType;
|
||||
private final String provider;
|
||||
private final MediaUploadCallback callback;
|
||||
/** When non-null (test fixtures), use instead of getUploadUrl(provider). */
|
||||
private final String overrideUploadUrl;
|
||||
|
||||
private WebView webView;
|
||||
private ValueCallback<Uri[]> filePathCallback;
|
||||
private final Handler mainHandler;
|
||||
private final Runnable pollRunnable;
|
||||
private final Runnable overallTimeoutRunnable;
|
||||
private boolean finished;
|
||||
private boolean fileInjected;
|
||||
private boolean dataTransferFallbackAttempted;
|
||||
private boolean submitClicked;
|
||||
private long startTime;
|
||||
/** Last matched selector (for diagnostics). */
|
||||
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 static final class ResolvedFilePayload {
|
||||
@@ -155,6 +162,7 @@ public class MediaUploadAutomationRunner {
|
||||
MediaUploadCallback callback,
|
||||
String overrideUploadUrl) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.fileUri = null;
|
||||
this.fileName = fileName != null ? fileName : "file";
|
||||
this.fileBytes = fileBytes;
|
||||
this.mimeType = mimeType != null ? mimeType : "application/octet-stream";
|
||||
@@ -163,20 +171,18 @@ public class MediaUploadAutomationRunner {
|
||||
this.overrideUploadUrl = overrideUploadUrl;
|
||||
this.mainHandler = new Handler(Looper.getMainLooper());
|
||||
this.pollRunnable = this::pollForResult;
|
||||
this.overallTimeoutRunnable = this::handleOverallTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy constructor (Uri-based). Kept for backward compat; prefer bytes constructor.
|
||||
* Resolves bytes once so legacy callers still use DataTransfer by default.
|
||||
*/
|
||||
/** Real Android upload constructor: prefer native WebView file chooser with this Uri. */
|
||||
public MediaUploadAutomationRunner(
|
||||
Context context, Uri fileUri, String fileName, String provider, MediaUploadCallback callback) {
|
||||
this(context, fileUri, fileName, provider, callback, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy test constructor: overrideUploadUrl loads fixture instead of live provider URL.
|
||||
* Package-visible for instrumentation tests. Requires fileBytes to be passed via bytes constructor.
|
||||
* Uri constructor with optional fixture URL.
|
||||
* Package-visible for instrumentation tests, though fixture tests usually use the bytes constructor.
|
||||
*/
|
||||
MediaUploadAutomationRunner(
|
||||
Context context,
|
||||
@@ -186,15 +192,21 @@ public class MediaUploadAutomationRunner {
|
||||
MediaUploadCallback callback,
|
||||
String overrideUploadUrl) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.fileUri = fileUri;
|
||||
this.fileName = fileName != null ? fileName : "file";
|
||||
ResolvedFilePayload payload = resolveFilePayload(this.context, fileUri);
|
||||
this.fileBytes = payload.fileBytes;
|
||||
this.mimeType = payload.mimeType;
|
||||
this.fileBytes = null;
|
||||
ContentResolver resolver = this.context.getContentResolver();
|
||||
String resolvedMime = fileUri != null ? resolver.getType(fileUri) : null;
|
||||
this.mimeType =
|
||||
(resolvedMime == null || resolvedMime.isEmpty())
|
||||
? "application/octet-stream"
|
||||
: resolvedMime;
|
||||
this.provider = provider;
|
||||
this.callback = callback;
|
||||
this.overrideUploadUrl = overrideUploadUrl;
|
||||
this.mainHandler = new Handler(Looper.getMainLooper());
|
||||
this.pollRunnable = this::pollForResult;
|
||||
this.overallTimeoutRunnable = this::handleOverallTimeout;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
@@ -209,6 +221,38 @@ public class MediaUploadAutomationRunner {
|
||||
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() {
|
||||
webView = new WebView(context);
|
||||
webView.setVisibility(android.view.View.GONE);
|
||||
@@ -219,6 +263,13 @@ public class MediaUploadAutomationRunner {
|
||||
settings.setAllowContentAccess(true);
|
||||
settings.setLoadWithOverviewMode(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(
|
||||
new WebChromeClient() {
|
||||
@@ -227,10 +278,18 @@ public class MediaUploadAutomationRunner {
|
||||
WebView webView,
|
||||
ValueCallback<Uri[]> filePathCallback,
|
||||
FileChooserParams fileChooserParams) {
|
||||
// Passive fallback only: observe callback, do not drive chooser flow.
|
||||
Log.d(TAG, "[" + provider + "] chooser callback observed (passive fallback)");
|
||||
if (!shouldUseFileChooser()) {
|
||||
return false;
|
||||
}
|
||||
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
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
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(
|
||||
new MediaUploadResult(
|
||||
false,
|
||||
@@ -266,12 +331,70 @@ public class MediaUploadAutomationRunner {
|
||||
}
|
||||
startTime = System.currentTimeMillis();
|
||||
webView.loadUrl(uploadUrl);
|
||||
mainHandler.postDelayed(overallTimeoutRunnable, MediaUploadRecipes.getUploadTimeoutMs(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() {
|
||||
if (finished || fileInjected) return;
|
||||
if (fileBytes == null || fileBytes.length == 0) {
|
||||
if (!ensureFilePayload()) {
|
||||
finish(
|
||||
new MediaUploadResult(
|
||||
false,
|
||||
@@ -353,7 +476,7 @@ public class MediaUploadAutomationRunner {
|
||||
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.
|
||||
if (fileInjected && !submitClicked) {
|
||||
String submitJs = MediaUploadRecipes.getSubmitClickJs(provider);
|
||||
@@ -422,25 +545,38 @@ public class MediaUploadAutomationRunner {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
mainHandler.removeCallbacks(pollRunnable);
|
||||
mainHandler.removeCallbacks(overallTimeoutRunnable);
|
||||
if (filePathCallback != null) {
|
||||
try {
|
||||
filePathCallback.onReceiveValue(null);
|
||||
} catch (Exception ignored) {}
|
||||
filePathCallback = null;
|
||||
}
|
||||
tearDown();
|
||||
callback.onComplete(result);
|
||||
}
|
||||
|
||||
private void tearDown() {
|
||||
mainHandler.post(
|
||||
Runnable cleanup =
|
||||
() -> {
|
||||
if (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 (webView == null) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
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.
|
||||
* 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: imgur only; no catbox (uses OkHttp). Uses DataTransfer JS injection
|
||||
* (no user activation); optional submit click after injection. onShowFileChooser kept as passive fallback.
|
||||
* - Electron: catbox/imgur. CDP DOM.setFileInputFiles + submit click. Catbox Electron-only.
|
||||
* - Selectors: file input, submit, success extractor, blocked indicators kept in sync for imgur.
|
||||
* - Android app uploads use native catbox and WebView automation for imgbb. The imgur runner
|
||||
* is retained for diagnostics/tests only. Real Uri attempts use the WebView file chooser
|
||||
* callback; fixtures/fallback use DataTransfer JS injection.
|
||||
* - 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 static final String PROVIDER_IMGUR = "imgur";
|
||||
public static final String PROVIDER_IMGBB = "imgbb";
|
||||
|
||||
/** Max time to wait for upload completion (ms). */
|
||||
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). */
|
||||
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). */
|
||||
public static final long POLL_INTERVAL_MS = 500;
|
||||
/** Initial delay before first file-input trigger attempt (ms). SPAs may need settle time. */
|
||||
@@ -27,15 +31,29 @@ public final class 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) {
|
||||
if (PROVIDER_IMGUR.equals(provider)) {
|
||||
return "https://imgur.com/upload";
|
||||
}
|
||||
if (PROVIDER_IMGBB.equals(provider)) {
|
||||
return "https://imgbb.com/";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Returns upload timeout. */
|
||||
public static long getUploadTimeoutMs(String provider) {
|
||||
if (PROVIDER_IMGBB.equals(provider)) {
|
||||
return IMGBB_UPLOAD_TIMEOUT_MS;
|
||||
}
|
||||
return UPLOAD_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
@@ -53,9 +71,25 @@ public final class MediaUploadRecipes {
|
||||
"[type=\"submit\"]",
|
||||
});
|
||||
}
|
||||
if (PROVIDER_IMGBB.equals(provider)) {
|
||||
return buildImgbbSubmitClickJs();
|
||||
}
|
||||
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) {
|
||||
StringBuilder sb = new StringBuilder("(function(){var s=[");
|
||||
for (int i = 0; i < selectors.length; i++) {
|
||||
@@ -99,6 +133,19 @@ public final class MediaUploadRecipes {
|
||||
fileName,
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -145,6 +192,18 @@ public final class MediaUploadRecipes {
|
||||
},
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -184,13 +243,24 @@ public final class MediaUploadRecipes {
|
||||
"video source[src*=\"i.imgur.com\"]",
|
||||
"video[src*=\"i.imgur.com\"]",
|
||||
};
|
||||
return buildImgurSuccessJs(selectorCandidates);
|
||||
} else {
|
||||
return null;
|
||||
return buildDirectHostSuccessJs(selectorCandidates, "i.imgur.com");
|
||||
} else if (PROVIDER_IMGBB.equals(provider)) {
|
||||
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=[");
|
||||
for (int i = 0; i < selectors.length; i++) {
|
||||
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;"
|
||||
+ "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 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 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;"
|
||||
+ "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){}}"
|
||||
+ "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;}"
|
||||
+ "var media=document.querySelector('img[src*=\"i.imgur.com\"],video source[src*=\"i.imgur.com\"],video[src*=\"i.imgur.com\"]');"
|
||||
+ "if(media){var rm=pick(media.getAttribute('src')||media.src);if(rm)return rm;}"
|
||||
+ "if(og){var ro=pickFromText(og.getAttribute('content'));if(ro)return ro;}"
|
||||
+ "var media=document.querySelector('img[src*=\""
|
||||
+ 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 sb.toString();
|
||||
}
|
||||
@@ -243,6 +323,19 @@ public final class MediaUploadRecipes {
|
||||
".login",
|
||||
});
|
||||
}
|
||||
if (PROVIDER_IMGBB.equals(provider)) {
|
||||
return buildBlockedJs(
|
||||
new String[] {
|
||||
"#challenge",
|
||||
".captcha",
|
||||
"[data-captcha]",
|
||||
".g-recaptcha",
|
||||
"#recaptcha",
|
||||
".login-form",
|
||||
".signin",
|
||||
".login",
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ public class MediaUploadRecipesTest {
|
||||
public void uploadTimeout_exceedsFileInputTimeout() {
|
||||
assertTrue(
|
||||
MediaUploadRecipes.UPLOAD_TIMEOUT_MS > MediaUploadRecipes.FILE_INPUT_TIMEOUT_MS);
|
||||
assertTrue(
|
||||
MediaUploadRecipes.IMGBB_UPLOAD_TIMEOUT_MS
|
||||
> MediaUploadRecipes.FILE_INPUT_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -33,6 +36,13 @@ public class MediaUploadRecipesTest {
|
||||
MediaUploadRecipes.getUploadUrl(MediaUploadRecipes.PROVIDER_IMGUR));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getUploadUrl_imgbb() {
|
||||
assertEquals(
|
||||
"https://imgbb.com/",
|
||||
MediaUploadRecipes.getUploadUrl(MediaUploadRecipes.PROVIDER_IMGBB));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getUploadUrl_unknownProvider_returnsNull() {
|
||||
assertNull(MediaUploadRecipes.getUploadUrl("catbox"));
|
||||
@@ -48,6 +58,14 @@ public class MediaUploadRecipesTest {
|
||||
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
|
||||
public void getTriggerFileInputJs_unknownProvider_returnsNull() {
|
||||
assertNull(MediaUploadRecipes.getTriggerFileInputJs("unknown"));
|
||||
@@ -61,6 +79,15 @@ public class MediaUploadRecipesTest {
|
||||
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
|
||||
public void getSuccessJs_imgur_containsImgurSelectors() {
|
||||
String js = MediaUploadRecipes.getSuccessJs(MediaUploadRecipes.PROVIDER_IMGUR);
|
||||
@@ -68,6 +95,14 @@ public class MediaUploadRecipesTest {
|
||||
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
|
||||
public void getBlockedJs_imgur_containsChallengeSelectors() {
|
||||
String js = MediaUploadRecipes.getBlockedJs(MediaUploadRecipes.PROVIDER_IMGUR);
|
||||
|
||||
Reference in New Issue
Block a user