6.7.0 - Alpha13 - http 模块请求相关方法修复 timeout/isInsecure 等选项功能, 支持更多 body 对象可用方法

This commit is contained in:
SuperMonster003
2025-12-27 13:29:23 +08:00
parent 91fc8bacfd
commit 79787b6b53
10 changed files with 662 additions and 556 deletions

View File

@@ -1,7 +1,7 @@
{
"$data": {
"v6.7.0": {
"released_date": "2025/12/25",
"released_date": "2025/12/26",
"feature": [
"插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮)",
"cvt 模块, 用于数据单位转换 (参阅 项目文档 > [单位转换](https://docs.autojs6.com/#/cvt))",
@@ -9,12 +9,12 @@
"zip 模块, 用于文件压缩与解压缩操作 (Ref to [Auto.js Pro](https://g.pro.autojs.org/)) (参阅 项目文档 > [Zip](https://docs.autojs6.com/#/zip))",
"mediainfo 模块, 用于查看媒体文件的详细信息 (参阅 项目文档 > [媒体信息](https://docs.autojs6.com/#/mediainfo))",
"cvt.bytes 方法, 用于字节数据单位转换 (参阅 项目文档 > [单位转换](https://docs.autojs6.com/#/cvt))",
"fmt.bytes 方法, 用于字节数据格式化 (参阅 项目文档 > [数据格式化](https://docs.autojs6.com/#/fmt))",
"fmt.bytes 方法, 用于字节数据格式化 (参阅 项目文档 > [格式化](https://docs.autojs6.com/#/fmt))",
"s13n.bytes 方法, 用于标准化字节数据 (参阅 项目文档 > [标准化](https://docs.autojs6.com/#/s13n))",
"app.isDualInstalled 方法, 用于检测双开应用是否已安装 (需要 Shizuku 或 Root 权限) _[`issue #450`](http://issues.autojs6.com/450)_",
"device.getSharedDeviceId 方法, 用于跨应用获取统一共享设备 ID _[`issue #455`](http://issues.autojs6.com/455)_",
"ui.navigationBarHeight 属性 (getter), 用于获取导航栏高度 _[`issue #456`](http://issues.autojs6.com/456)_",
"http 模块请求相关方法获取的 body 对象增加 stream/saveToFile/close 方法 _[`issue #452`](http://issues.autojs6.com/452)_",
"http 模块请求相关方法获取的 body 对象增加 stream/saveToFile/close 方法 _[`issue #452`](http://issues.autojs6.com/452)_",
"http 模块请求相关方法支持缓存控制选项参数 (cacheBody/bodyCacheThresholdBytes)",
"http 模块请求相关方法支持不安全选项参数 (isInsecure/insecure), 用于忽略证书相关异常 _[`issue #417`](http://issues.autojs6.com/417)_",
"http 模块请求相关方法支持 options.client 选项, 用于配置 OkHttpClient.Builder (如 followRedirects 等) _[`issue #454`](http://issues.autojs6.com/454)_",

View File

@@ -28,8 +28,8 @@ class RootAutomatorNativeObject(scriptRuntime: ScriptRuntime, waitForReady: Any?
defineProperty("__ra__", mRootAutomatorObject, READONLY or DONTENUM or PERMANENT)
}
override fun has(name: String?): Boolean {
return mRootAutomatorObject.has(name) || super.has(name)
override fun has(name: String, start: Scriptable): Boolean {
return mRootAutomatorObject.has(name, start) || super.has(name, start)
}
override fun get(name: String, start: Scriptable): Any? {

View File

@@ -2,82 +2,46 @@
package org.autojs.autojs.runtime.api.augment.http
import android.annotation.SuppressLint
import android.webkit.MimeTypeMap
import okhttp3.Call
import okhttp3.Callback
import okhttp3.FormBody
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.ResponseBody
import okio.BufferedSink
import org.autojs.autojs.annotation.RhinoFunctionBody
import org.autojs.autojs.annotation.RhinoRuntimeFunctionInterface
import org.autojs.autojs.annotation.RhinoStandardFunctionInterface
import org.autojs.autojs.core.http.MutableOkHttp
import org.autojs.autojs.extension.AnyExtensions.isJsFunction
import org.autojs.autojs.extension.AnyExtensions.isJsNullish
import org.autojs.autojs.extension.AnyExtensions.jsBrief
import org.autojs.autojs.extension.AnyExtensions.toRuntimePath
import org.autojs.autojs.extension.ArrayExtensions.toNativeArray
import org.autojs.autojs.extension.FlexibleArray
import org.autojs.autojs.extension.FlexibleArray.Companion.component1
import org.autojs.autojs.extension.FlexibleArray.Companion.component2
import org.autojs.autojs.extension.FlexibleArray.Companion.component3
import org.autojs.autojs.extension.FlexibleArray.Companion.component4
import org.autojs.autojs.extension.ScriptableExtensions.prop
import org.autojs.autojs.extension.ScriptableObjectExtensions.inquire
import org.autojs.autojs.pio.PFile
import org.autojs.autojs.pio.PFileInterface
import org.autojs.autojs.runtime.ScriptRuntime
import org.autojs.autojs.runtime.api.Mime
import org.autojs.autojs.runtime.api.StringReadable
import org.autojs.autojs.runtime.api.augment.Augmentable
import org.autojs.autojs.runtime.api.augment.continuation.Continuation
import org.autojs.autojs.runtime.api.augment.continuation.Creator
import org.autojs.autojs.runtime.api.augment.converter.core.Bytes
import org.autojs.autojs.runtime.api.augment.http.RequestBuilder.Companion.applyOkHttpClientBuilder
import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException
import org.autojs.autojs.util.RhinoUtils
import org.autojs.autojs.util.RhinoUtils.UNDEFINED
import org.autojs.autojs.util.RhinoUtils.coerceBoolean
import org.autojs.autojs.util.RhinoUtils.coerceIntNumber
import org.autojs.autojs.util.RhinoUtils.coerceLongNumber
import org.autojs.autojs.util.RhinoUtils.coerceNumber
import org.autojs.autojs.util.RhinoUtils.coerceObject
import org.autojs.autojs.util.RhinoUtils.coerceString
import org.autojs.autojs.util.RhinoUtils.isUiThread
import org.autojs.autojs.util.RhinoUtils.js_json_parse
import org.autojs.autojs.util.RhinoUtils.js_json_stringify
import org.autojs.autojs.util.RhinoUtils.newNativeObject
import org.autojs.autojs.util.RhinoUtils.withRhinoContext
import org.mozilla.javascript.BaseFunction
import org.mozilla.javascript.Context
import org.mozilla.javascript.Function
import org.mozilla.javascript.NativeArray
import org.mozilla.javascript.NativeObject
import org.mozilla.javascript.Scriptable
import org.mozilla.javascript.ScriptableObject.DONTENUM
import org.mozilla.javascript.ScriptableObject.PERMANENT
import org.mozilla.javascript.ScriptableObject.READONLY
import org.mozilla.javascript.Undefined
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.lang.reflect.Method
import java.net.URI
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
@Suppress("unused", "UNUSED_PARAMETER")
class Http(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime) {
@@ -97,20 +61,21 @@ class Http(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime) {
companion object : Augmentable() {
private const val METHOD_GET = "GET"
private const val METHOD_POST = "POST"
internal const val KEY_METHOD = "method"
internal const val KEY_CONTENT_TYPE = "contentType"
internal const val KEY_HEADERS = "headers"
internal const val KEY_FILES = "files"
internal const val KEY_BODY = "body"
internal const val KEY_CLIENT = "client"
internal const val KEY_TIMEOUT = "timeout"
private const val KEY_CLIENT = "client"
private const val KEY_METHOD = "method"
private const val KEY_CONTENT_TYPE = "contentType"
private const val KEY_HEADERS = "headers"
private const val KEY_FILES = "files"
private const val KEY_BODY = "body"
private const val KEY_TIMEOUT = "timeout"
private const val KEY_MAX_RETRIES = "maxRetries"
private const val KEY_CACHE_BODY = "cacheBody"
private const val KEY_BODY_CACHE_THRESHOLD_BYTES = "bodyCacheThresholdBytes"
private const val METHOD_GET = "GET"
private const val METHOD_POST = "POST"
private val DEFAULT_CONTENT_TYPE = Mime.APPLICATION_X_WWW_FORM_URLENCODED
@JvmField
@@ -164,7 +129,7 @@ class Http(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime) {
applyOkHttpClientBuilder(opt)
}
val cacheBody = opt.inquire(listOf(KEY_CACHE_BODY), ::coerceBoolean, DEFAULT_CACHE_BODY)
val cacheBody = opt.inquire(KEY_CACHE_BODY, ::coerceBoolean, DEFAULT_CACHE_BODY)
val cacheThreshold = coerceLongNumber(opt.prop(KEY_BODY_CACHE_THRESHOLD_BYTES), DEFAULT_BODY_CACHE_THRESHOLD_BYTES)
val newCall = scriptRuntime.http.client().newCall(buildRequestRhinoWithRuntime(scriptRuntime, url, options))
@@ -288,507 +253,6 @@ class Http(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime) {
}
}
private fun MutableOkHttp.applyOkHttpClientBuilder(opt: NativeObject) {
val clientProp = opt.prop(KEY_CLIENT).takeUnless { it.isJsNullish() }
require(clientProp is NativeObject?) { "Argument \"client\" ${clientProp.jsBrief()} for http.request must be a JavaScript Object" }
clientProp ?: return
val timeout = coerceLongNumber(opt.prop(KEY_TIMEOUT), DEFAULT_TIMEOUT)
val isInsecure = opt.inquire(listOf("isInsecure", "insecure"), ::coerceBoolean, false)
val builder = this.client().newBuilder()
.readTimeout(timeout, TimeUnit.MILLISECONDS)
.writeTimeout(timeout, TimeUnit.MILLISECONDS)
.connectTimeout(timeout, TimeUnit.MILLISECONDS)
val builderClass = OkHttpClient.Builder::class.java
fun coerceArg(paramType: Class<*>, value: Any?) = when {
paramType == java.lang.Boolean.TYPE || paramType == java.lang.Boolean::class.java -> coerceBoolean(value, false)
paramType == java.lang.Long.TYPE || paramType == java.lang.Long::class.java -> coerceLongNumber(value, 0L)
paramType == Integer.TYPE || paramType == Integer::class.java -> coerceIntNumber(value, 0)
paramType == java.lang.Double.TYPE || paramType == java.lang.Double::class.java -> coerceNumber(value, 0.0)
paramType == String::class.java -> coerceString(value, "")
paramType == TimeUnit::class.java -> when (value) {
is TimeUnit -> value
is String -> runCatching { TimeUnit.valueOf(value.trim().uppercase()) }.getOrElse {
throw WrappedIllegalArgumentException("Invalid TimeUnit string: $value")
}
else -> throw WrappedIllegalArgumentException("Invalid TimeUnit argument: ${value.jsBrief()}")
}
// Java object: Proxy/Dispatcher/ConnectionPool/Authenticator/SSLSocketFactory/...
paramType.isInstance(value) -> value
// Let reflection verify the type matching by itself.
// zh-CN: 让反射自行校验类型是否匹配.
else -> value
}
fun findCandidateMethods(name: String): List<Method> {
return builderClass.methods.filter { it.name == name && it.declaringClass == builderClass }
}
clientProp.forEach { entry ->
val (rawKey, rawVal) = entry
val methodName = coerceString(rawKey, "").trim()
if (methodName.isEmpty()) return@forEach
val candidates = findCandidateMethods(methodName)
require(candidates.isNotEmpty()) {
"No such Builder method: $methodName on ${builderClass.name}"
}
val zeroArg = candidates.firstOrNull { it.parameterCount == 0 }
require(zeroArg == null) {
"Builder method \"$methodName\" with 0 parameter is not allowed to be invoked via client options"
}
val twoArgs = candidates.firstOrNull { it.parameterCount == 2 }
if (twoArgs != null && rawVal is List<*> && rawVal.size == 2) {
val args2 = arrayOf(coerceArg(twoArgs.parameterTypes[0], rawVal[0]), coerceArg(twoArgs.parameterTypes[1], rawVal[1]))
twoArgs.invoke(builder, args2[0], args2[1])
return@forEach
}
val oneArg = candidates.firstOrNull { it.parameterCount == 1 }
if (oneArg != null) {
val paramTypes = oneArg.parameterTypes
val arg0 = coerceArg(paramTypes[0], rawVal)
oneArg.invoke(builder, arg0)
return@forEach
}
val supported = candidates.joinToString { "(${it.parameterTypes.joinToString { p -> p.simpleName }})" }
throw WrappedIllegalArgumentException(
"Builder method \"$methodName\" is not invokable with 1 or 2 parameters via client options. Supported overloads: $supported"
)
}
if (isInsecure) {
@SuppressLint("CustomX509TrustManager")
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) = Unit
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) = Unit
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
})
val sslContext = SSLContext.getInstance("TLS").apply {
init(null, trustAllCerts, SecureRandom())
}
val trustManager = trustAllCerts.first() as X509TrustManager
builder.sslSocketFactory(sslContext.socketFactory, trustManager)
builder.hostnameVerifier { _, _ -> true }
}
// Apply the new Builder to the internal client.
// zh-CN: 应用新的 Builder 到内部客户端.
muteClient(builder)
}
private class ResponseWrapper(
private val scriptRuntime: ScriptRuntime,
private val res: Response,
private val cacheBody: Boolean,
private val cacheThresholdBytes: Long,
) {
private val mRequest = res.request
var resBodyString: String? = null
var resBodyBytes: ByteArray? = null
fun wrap() = newNativeObject().apply {
put("request", this, mRequest)
put("statusMessage", this, res.message)
put("statusCode", this, res.code)
put("body", this, getBody())
put("headers", this, getHeaders())
put("url", this, mRequest.url)
put("method", this, mRequest.method)
}
private fun getBody(): ResponseBodyNativeObject {
// Returns a non-null value if this response
// was passed to Callback.onResponse
// or returned from Call.execute.
val resBody = res.body!!
return ResponseBodyNativeObject(scriptRuntime, resBody, this, cacheBody, cacheThresholdBytes).also {
it.defineFunctionProperties(arrayOf("string", "bytes", "json", "stream", "saveToFile", "close"), it.javaClass, READONLY or PERMANENT)
it.defineProperty(KEY_CONTENT_TYPE, { resBody.contentType() }, null, READONLY or PERMANENT)
}
}
private fun getHeaders(): NativeObject {
val result = newNativeObject()
val headers = res.headers
for (i in 0 until headers.size) {
val name = headers.name(i).lowercase()
val value = headers.value(i)
if (!result.containsKey(name)) {
result.put(name, result, value)
continue
}
val list = mutableListOf<Any?>()
val origin = result.prop(name)
if (origin !is NativeArray) {
list += origin
} else {
list.addAll(origin)
}
list += value
result.put(name, result, list.toNativeArray())
}
return result
}
}
class HttpSaveResult @JvmOverloads constructor(
private val resultCode: Int,
private val outPath: String?,
private val bytesCopied: Long,
private val error: Throwable? = null,
) : NativeObject(), StringReadable {
init {
RhinoUtils.initNativeObjectPrototype(this)
defineProperty("code", { resultCode }, null, READONLY or PERMANENT)
defineProperty("path", { outPath }, null, READONLY or PERMANENT)
defineProperty("bytesCopied", { bytesCopied }, null, READONLY or PERMANENT)
defineProperty("success", { resultCode == RESULT_OK }, null, READONLY or PERMANENT)
defineProperty("error", { error }, null, READONLY or PERMANENT)
defineFunctionProperties(arrayOf("isSuccess"), javaClass, READONLY or PERMANENT)
}
override fun toStringReadable(): String = listOf(
"${HttpSaveResult::class.java.simpleName} {",
" code: ${resultCode},",
" bytesCopied: $bytesCopied (${Bytes.string(bytesCopied.toDouble(), useSpace = true, strict = true)}),",
" path: '${outPath}',",
" error: ${error?.message?.take(256)?.let { "'$it'" }},",
"}",
).joinToString("\n")
companion object {
@JvmField
val RESULT_OK = 0
private const val RESULT_FAILED_GENERIC = -1
fun ok(path: String, bytesCopied: Long) =
HttpSaveResult(RESULT_OK, path, bytesCopied, null)
fun fail(path: String?, bytesCopied: Long, e: Throwable?) =
HttpSaveResult(RESULT_FAILED_GENERIC, path, bytesCopied, e)
@JvmStatic
@RhinoStandardFunctionInterface
fun isSuccess(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): Boolean = ensureArgumentsIsEmpty(args) {
val o = thisObj as HttpSaveResult
o.resultCode == RESULT_OK
}
}
}
@Suppress("unused")
private class ResponseBodyNativeObject(
val scriptRuntime: ScriptRuntime,
val resBody: ResponseBody,
val responseWrapper: ResponseWrapper,
private val cacheBody: Boolean,
private val cacheThresholdBytes: Long,
) : NativeObject() {
init {
RhinoUtils.initNativeObjectPrototype(this)
}
// Record whether it has been explicitly closed.
// zh-CN: 记录是否已显式关闭.
@Volatile
private var closed = false
private fun ensureOpen() {
if (closed) throw IllegalStateException("Response body already closed")
}
private fun autoCloseIfNeeded() {
// Close immediately after reading the complete content to avoid resource leaks;
// stream() close is handled by the caller.
// zh-CN: 读取完整内容后立即关闭, 避免资源泄露; stream() 由调用者负责 close().
if (!closed) {
runCatching { resBody.close() }
closed = true
}
}
private fun shouldCache(lengthHint: Long?): Boolean {
if (!cacheBody) return false
if (lengthHint == null || lengthHint < 0) return true
return lengthHint <= cacheThresholdBytes
}
companion object : FlexibleArray() {
@JvmStatic
@RhinoStandardFunctionInterface
fun string(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): String = ensureArgumentsIsEmpty(args) {
val o = thisObj as ResponseBodyNativeObject
o.responseWrapper.resBodyString?.let { return@ensureArgumentsIsEmpty it }
o.ensureOpen()
val contentLength = runCatching {
o.resBody.contentLength()
}.getOrDefault(-1L)
val str = o.resBody.string()
if (o.shouldCache(contentLength)) {
o.responseWrapper.resBodyString = str
}
// string() can safely close after consuming the stream.
// zh-CN: string() 消费流后可安全关闭.
o.autoCloseIfNeeded()
return@ensureArgumentsIsEmpty str
}
@JvmStatic
@RhinoStandardFunctionInterface
fun bytes(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): ByteArray = ensureArgumentsIsEmpty(args) {
val o = thisObj as ResponseBodyNativeObject
o.responseWrapper.resBodyBytes?.let { return@ensureArgumentsIsEmpty it }
o.ensureOpen()
val contentLength = runCatching {
o.resBody.contentLength()
}.getOrDefault(-1L)
val data = o.resBody.bytes()
if (o.shouldCache(contentLength)) {
o.responseWrapper.resBodyBytes = data
}
// bytes() 消费流后可安全关闭
o.autoCloseIfNeeded()
return@ensureArgumentsIsEmpty data
}
@JvmStatic
@RhinoStandardFunctionInterface
fun json(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): Any? = ensureArgumentsIsEmpty(args) {
val str = string(cx, thisObj, args, funObj)
runCatching {
return@ensureArgumentsIsEmpty js_json_parse(str)
}.onFailure {
throw IllegalStateException("Failed to parse JSON. Body string may be not in JSON format")
}
}
@JvmStatic
@RhinoStandardFunctionInterface
fun stream(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): InputStream = ensureArgumentsIsEmpty(args) {
val o = thisObj as ResponseBodyNativeObject
o.ensureOpen()
// Don't auto-close; let the caller handle it, supporting streaming copy.
// zh-CN: 不自动关闭; 交给调用者处理, 支持流式拷贝.
o.resBody.byteStream()
}
// Save directly to file (avoid loading large responses into memory).
// zh-CN: 直接保存到文件 (避免将大型响应加载到内存中).
@JvmStatic
@RhinoStandardFunctionInterface
fun saveToFile(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): HttpSaveResult = ensureArgumentsLengthInRange(args, 1..2) { argList ->
val o = thisObj as ResponseBodyNativeObject
o.ensureOpen()
val (pathRaw, bufSizeRaw) = argList
val path = coerceString(pathRaw, "").toRuntimePath(o.scriptRuntime)
val bufSize = coerceIntNumber(bufSizeRaw, 0).let {
if (it > 0) it else 8192
}
val file = PFile(path)
val isDirectory = file.isDirectory || path.endsWith("/")
if (isDirectory) {
throw WrappedIllegalArgumentException("Path \"$path\" must be a file path instead of a directory path")
}
val buffer = ByteArray(bufSize)
var copied = 0L
var input: InputStream? = null
var output: OutputStream? = null
return@ensureArgumentsLengthInRange try {
input = o.resBody.byteStream()
output = file.outputStream()
while (true) {
val read = input.read(buffer)
if (read == -1) break
output.write(buffer, 0, read)
copied += read
}
output.flush()
HttpSaveResult.ok(path, copied)
} catch (e: Throwable) {
HttpSaveResult.fail(path, copied, e)
} finally {
runCatching { input?.close() }
runCatching { output?.close() }
o.autoCloseIfNeeded()
}
}
// Explicit close.
// zh-CN: 显式关闭.
@JvmStatic
@RhinoStandardFunctionInterface
fun close(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): Undefined = ensureArgumentsIsEmpty(args) {
val o = thisObj as ResponseBodyNativeObject
if (!o.closed) {
runCatching { o.resBody.close() }
o.closed = true
}
UNDEFINED
}
}
}
private class RequestBuilder(private val scriptRuntime: ScriptRuntime, private val url: Any?, options: NativeObject = newNativeObject()) {
private val mRequest = Request.Builder()
private val mRequestBuilderHelper = RequestBuilderHelper(options)
fun build(): Request {
mRequest.url(mRequestBuilderHelper.getUrl(coerceString(url)))
mRequestBuilderHelper.setHeaders(mRequest)
mRequestBuilderHelper.setMethod(scriptRuntime, mRequest)
return mRequest.build()
}
}
private class RequestBuilderHelper(private val options: NativeObject) {
@Suppress("HttpUrlsUsage")
fun getUrl(url: String) = when {
url.matches(Regex("^https?://.*")) -> url
else -> "http://$url"
}
fun setHeaders(request: Request.Builder) {
val headers = options.prop(KEY_HEADERS)
if (headers.isJsNullish()) return
require(headers is NativeObject) { "Property headers ${headers.jsBrief()} for builder of http.request must be a JavaScript Object" }
headers.forEach { entry ->
val (key, value) = entry
when (value) {
is NativeArray -> value.forEach { setHeader(request, key, it) }
else -> setHeader(request, key, value)
}
}
}
fun setMethod(scriptRuntime: ScriptRuntime, request: Request.Builder) {
val method = coerceString(options.prop(KEY_METHOD))
// require(method is String) { "Property method is required for header options" }
when {
!options.prop(KEY_BODY).isJsNullish() -> {
request.method(method, parseBody())
}
!options.prop(KEY_FILES).isJsNullish() -> {
request.method(method, parseMultipart(scriptRuntime))
}
else -> {
request.method(method, null)
}
}
}
fun parseBody(): RequestBody = when (val body = options.prop(KEY_BODY)) {
is RequestBody -> body
is String -> {
val mediaType = options.prop(KEY_CONTENT_TYPE).takeUnless { it.isJsNullish() }
body.toRequestBody(Context.toString(mediaType).toMediaTypeOrNull())
}
is BaseFunction -> object : RequestBody() {
override fun contentType(): MediaType? {
val mediaType = options.prop(KEY_CONTENT_TYPE).takeUnless { it.isJsNullish() }
return Context.toString(mediaType).toMediaTypeOrNull()
}
override fun writeTo(sink: BufferedSink) {
withRhinoContext { cx ->
body.call(cx, body, body, arrayOf(sink))
}
}
}
else -> throw WrappedIllegalArgumentException("Unknown type of body for header options")
}
fun parseMultipart(scriptRuntime: ScriptRuntime): MultipartBody {
val builder = MultipartBody.Builder().setType(MultipartBody.FORM)
val files = options.prop(KEY_FILES)
if (files.isJsNullish()) return builder.build()
require(files is NativeObject) { "Property files ${files.jsBrief()} for builder of http.request must be a JavaScript Object" }
files.forEach { entry ->
val (key, value) = entry
when (value) {
is String -> {
builder.addFormDataPart(coerceString(key), value)
}
is NativeArray -> when (value.length) {
2L -> {
val (fileName, path) = value
val file = if (path is URI) PFile(path) else PFile(path.toRuntimePath(scriptRuntime))
val mimeType = parseMimeType(file.extension)
val requestBody = file.asRequestBody(mimeType.toMediaTypeOrNull())
processFile(builder, key, fileName, requestBody)
}
3L -> {
val (fileName, mimeType, path) = value
val file = if (path is URI) PFile(path) else PFile(path.toRuntimePath(scriptRuntime))
val requestBody = file.asRequestBody(coerceString(mimeType).toMediaTypeOrNull())
processFile(builder, key, fileName, requestBody)
}
else -> listOf(
"Array value \"value\" for property \"files\"",
"in RequestBuilderHelper#parseMultipart",
"must be of length 2 or 3 instead of ${value.length}",
).joinToString(" ").let { throw WrappedIllegalArgumentException(it) }
}
is PFileInterface -> {
val path = value.path
val file = PFile(path.toRuntimePath(scriptRuntime))
val fileName = file.name
val mimeType = parseMimeType(file.extension)
val requestBody = file.asRequestBody(mimeType.toMediaTypeOrNull())
processFile(builder, key, fileName, requestBody)
}
else -> listOf(
"Value \"value\" for property \"files\"",
"in RequestBuilderHelper#parseMultipart",
"must be either a string, an array",
"or a JavaScript object",
).joinToString(" ").let { throw WrappedIllegalArgumentException(it) }
}
}
return builder.build()
}
fun parseMimeType(ext: String) = when {
ext.isNotEmpty() -> {
MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: Mime.APPLICATION_OCTET_STREAM
}
else -> Mime.APPLICATION_OCTET_STREAM
}
private fun setHeader(request: Request.Builder, key: Any?, it: Any?) {
request.header(coerceString(key), Context.toString(it))
}
private fun processFile(builder: MultipartBody.Builder, key: Any?, fileName: Any?, requestBody: RequestBody) {
builder.addFormDataPart(coerceString(key), fileName as? String, requestBody)
}
}
}
}

View File

@@ -0,0 +1,60 @@
package org.autojs.autojs.runtime.api.augment.http
import org.autojs.autojs.annotation.RhinoStandardFunctionInterface
import org.autojs.autojs.extension.FlexibleArray.Companion.ensureArgumentsIsEmpty
import org.autojs.autojs.runtime.api.StringReadable
import org.autojs.autojs.runtime.api.augment.converter.core.Bytes
import org.autojs.autojs.util.RhinoUtils
import org.mozilla.javascript.Context
import org.mozilla.javascript.Function
import org.mozilla.javascript.NativeObject
import org.mozilla.javascript.Scriptable
@Suppress("unused")
class HttpSaveResult @JvmOverloads constructor(
private val resultCode: Int,
private val outPath: String?,
private val bytesCopied: Long,
private val error: Throwable? = null,
) : NativeObject(), StringReadable {
init {
RhinoUtils.initNativeObjectPrototype(this)
defineProperty("code", { resultCode }, null, READONLY or PERMANENT)
defineProperty("path", { outPath }, null, READONLY or PERMANENT)
defineProperty("bytesCopied", { bytesCopied }, null, READONLY or PERMANENT)
defineProperty("success", { resultCode == RESULT_OK }, null, READONLY or PERMANENT)
defineProperty("error", { error }, null, READONLY or PERMANENT)
defineFunctionProperties(arrayOf("isSuccess"), javaClass, READONLY or PERMANENT)
}
override fun toStringReadable(): String = listOf(
"${HttpSaveResult::class.java.simpleName} {",
" code: ${resultCode},",
" bytesCopied: $bytesCopied (${Bytes.string(bytesCopied.toDouble(), useSpace = true, strict = true)}),",
" path: '${outPath}',",
" error: ${error?.message?.take(256)?.let { "'$it'" }},",
"}",
).joinToString("\n")
companion object {
const val RESULT_OK = 0
private const val RESULT_FAILED_GENERIC = -1
fun ok(path: String, bytesCopied: Long) =
HttpSaveResult(RESULT_OK, path, bytesCopied, null)
fun fail(path: String?, bytesCopied: Long, e: Throwable?) =
HttpSaveResult(RESULT_FAILED_GENERIC, path, bytesCopied, e)
@JvmStatic
@RhinoStandardFunctionInterface
fun isSuccess(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): Boolean = ensureArgumentsIsEmpty(args) {
val o = thisObj as HttpSaveResult
o.resultCode == RESULT_OK
}
}
}

View File

@@ -0,0 +1,144 @@
package org.autojs.autojs.runtime.api.augment.http
import android.annotation.SuppressLint
import okhttp3.OkHttpClient
import okhttp3.Request
import org.autojs.autojs.core.http.MutableOkHttp
import org.autojs.autojs.extension.AnyExtensions.isJsNullish
import org.autojs.autojs.extension.AnyExtensions.jsBrief
import org.autojs.autojs.extension.ScriptableExtensions.prop
import org.autojs.autojs.extension.ScriptableObjectExtensions.inquire
import org.autojs.autojs.runtime.ScriptRuntime
import org.autojs.autojs.runtime.api.augment.http.Http.Companion.DEFAULT_TIMEOUT
import org.autojs.autojs.runtime.api.augment.http.Http.Companion.KEY_CLIENT
import org.autojs.autojs.runtime.api.augment.http.Http.Companion.KEY_TIMEOUT
import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException
import org.autojs.autojs.util.RhinoUtils
import org.autojs.autojs.util.RhinoUtils.coerceBoolean
import org.autojs.autojs.util.RhinoUtils.coerceIntNumber
import org.autojs.autojs.util.RhinoUtils.coerceLongNumber
import org.autojs.autojs.util.RhinoUtils.coerceNumber
import org.autojs.autojs.util.RhinoUtils.coerceString
import org.mozilla.javascript.NativeObject
import java.lang.reflect.Method
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
class RequestBuilder(
private val scriptRuntime: ScriptRuntime,
private val url: Any?,
options: NativeObject = RhinoUtils.newNativeObject(),
) {
private val mRequest = Request.Builder()
private val mRequestBuilderHelper = RequestBuilderHelper(options)
fun build(): Request {
mRequest.url(mRequestBuilderHelper.getUrl(coerceString(url)))
mRequestBuilderHelper.setHeaders(mRequest)
mRequestBuilderHelper.setMethod(scriptRuntime, mRequest)
return mRequest.build()
}
companion object {
fun MutableOkHttp.applyOkHttpClientBuilder(opt: NativeObject) {
val clientProp = opt.prop(KEY_CLIENT).takeUnless { it.isJsNullish() }
require(clientProp is NativeObject?) { "Argument \"client\" ${clientProp.jsBrief()} for http.request must be a JavaScript Object" }
val timeout = coerceLongNumber(opt.prop(KEY_TIMEOUT), DEFAULT_TIMEOUT)
val isInsecure = opt.inquire(listOf("isInsecure", "insecure"), ::coerceBoolean, false)
val builder = this.client().newBuilder()
.readTimeout(timeout, TimeUnit.MILLISECONDS)
.writeTimeout(timeout, TimeUnit.MILLISECONDS)
.connectTimeout(timeout, TimeUnit.MILLISECONDS)
val builderClass = OkHttpClient.Builder::class.java
fun coerceArg(paramType: Class<*>, value: Any?) = when {
paramType == java.lang.Boolean.TYPE || paramType == Boolean::class.java -> coerceBoolean(value, false)
paramType == java.lang.Long.TYPE || paramType == Long::class.java -> coerceLongNumber(value, 0L)
paramType == Integer.TYPE || paramType == Int::class.java -> coerceIntNumber(value, 0)
paramType == java.lang.Double.TYPE || paramType == Double::class.java -> coerceNumber(value, 0.0)
paramType == String::class.java -> coerceString(value, "")
paramType == TimeUnit::class.java -> when (value) {
is TimeUnit -> value
is String -> runCatching { TimeUnit.valueOf(value.trim().uppercase()) }.getOrElse {
throw WrappedIllegalArgumentException("Invalid TimeUnit string: $value")
}
else -> throw WrappedIllegalArgumentException("Invalid TimeUnit argument: ${value.jsBrief()}")
}
// Java object: Proxy/Dispatcher/ConnectionPool/Authenticator/SSLSocketFactory/...
paramType.isInstance(value) -> value
// Let reflection verify the type matching by itself.
// zh-CN: 让反射自行校验类型是否匹配.
else -> value
}
fun findCandidateMethods(name: String): List<Method> {
return builderClass.methods.filter { it.name == name && it.declaringClass == builderClass }
}
clientProp?.forEach { entry ->
val (rawKey, rawVal) = entry
val methodName = coerceString(rawKey, "").trim()
if (methodName.isEmpty()) return@forEach
val candidates = findCandidateMethods(methodName)
require(candidates.isNotEmpty()) {
"No such Builder method: $methodName on ${builderClass.name}"
}
val zeroArg = candidates.firstOrNull { it.parameterCount == 0 }
require(zeroArg == null) {
"Builder method \"$methodName\" with 0 parameter is not allowed to be invoked via client options"
}
val twoArgs = candidates.firstOrNull { it.parameterCount == 2 }
if (twoArgs != null && rawVal is List<*> && rawVal.size == 2) {
val args2 = arrayOf(coerceArg(twoArgs.parameterTypes[0], rawVal[0]), coerceArg(twoArgs.parameterTypes[1], rawVal[1]))
twoArgs.invoke(builder, args2[0], args2[1])
return@forEach
}
val oneArg = candidates.firstOrNull { it.parameterCount == 1 }
if (oneArg != null) {
val paramTypes = oneArg.parameterTypes
val arg0 = coerceArg(paramTypes[0], rawVal)
oneArg.invoke(builder, arg0)
return@forEach
}
val supported = candidates.joinToString { "(${it.parameterTypes.joinToString { p -> p.simpleName }})" }
throw WrappedIllegalArgumentException(
"Builder method \"$methodName\" is not invokable with 1 or 2 parameters via client options. Supported overloads: $supported"
)
}
if (isInsecure) {
@SuppressLint("CustomX509TrustManager")
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) = Unit
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) = Unit
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
})
val sslContext = SSLContext.getInstance("TLS").apply {
init(null, trustAllCerts, SecureRandom())
}
val trustManager = trustAllCerts.first() as X509TrustManager
builder.sslSocketFactory(sslContext.socketFactory, trustManager)
builder.hostnameVerifier { _, _ -> true }
}
// Apply the new Builder to the internal client.
// zh-CN: 应用新的 Builder 到内部客户端.
muteClient(builder)
}
}
}

View File

@@ -0,0 +1,156 @@
package org.autojs.autojs.runtime.api.augment.http
import android.webkit.MimeTypeMap
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okio.BufferedSink
import org.autojs.autojs.extension.AnyExtensions.isJsNullish
import org.autojs.autojs.extension.AnyExtensions.isJsNumber
import org.autojs.autojs.extension.AnyExtensions.isJsString
import org.autojs.autojs.extension.AnyExtensions.jsBrief
import org.autojs.autojs.extension.AnyExtensions.toRuntimePath
import org.autojs.autojs.extension.ScriptableExtensions.prop
import org.autojs.autojs.pio.PFile
import org.autojs.autojs.pio.PFileInterface
import org.autojs.autojs.runtime.ScriptRuntime
import org.autojs.autojs.runtime.api.Mime
import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException
import org.autojs.autojs.util.RhinoUtils
import org.mozilla.javascript.BaseFunction
import org.mozilla.javascript.Context
import org.mozilla.javascript.NativeArray
import org.mozilla.javascript.NativeObject
import java.net.URI
class RequestBuilderHelper(private val options: NativeObject) {
@Suppress("HttpUrlsUsage")
fun getUrl(url: String) = when {
url.matches(Regex("^https?://.*")) -> url
else -> "http://$url"
}
fun setHeaders(request: Request.Builder) {
val headers = options.prop(Http.KEY_HEADERS)
if (headers.isJsNullish()) return
require(headers is NativeObject) { "Property headers ${headers.jsBrief()} for builder of http.request must be a JavaScript Object" }
headers.forEach { entry ->
val (key, value) = entry
when (value) {
is NativeArray -> value.forEach { setHeader(request, key, it) }
else -> setHeader(request, key, value)
}
}
}
fun setMethod(scriptRuntime: ScriptRuntime, request: Request.Builder) {
val method = RhinoUtils.coerceString(options.prop(Http.KEY_METHOD))
// require(method is String) { "Property method is required for header options" }
when {
!options.prop(Http.KEY_BODY).isJsNullish() -> {
request.method(method, parseBody())
}
!options.prop(Http.KEY_FILES).isJsNullish() -> {
request.method(method, parseMultipart(scriptRuntime))
}
else -> {
request.method(method, null)
}
}
}
fun parseBody(): RequestBody = when (val body = options.prop(Http.KEY_BODY)) {
is RequestBody -> body
is String -> {
val mediaType = options.prop(Http.KEY_CONTENT_TYPE).takeUnless { it.isJsNullish() }
body.toRequestBody(Context.toString(mediaType).toMediaTypeOrNull())
}
is BaseFunction -> object : RequestBody() {
override fun contentType(): MediaType? {
val mediaType = options.prop(Http.KEY_CONTENT_TYPE).takeUnless { it.isJsNullish() }
return Context.toString(mediaType).toMediaTypeOrNull()
}
override fun writeTo(sink: BufferedSink) {
RhinoUtils.withRhinoContext { cx ->
body.call(cx, body, body, arrayOf(sink))
}
}
}
else -> throw WrappedIllegalArgumentException("Unknown type of body for header options")
}
fun parseMultipart(scriptRuntime: ScriptRuntime): MultipartBody {
val builder = MultipartBody.Builder().setType(MultipartBody.FORM)
val files = options.prop(Http.KEY_FILES)
if (files.isJsNullish()) return builder.build()
require(files is NativeObject) { "Property files ${files.jsBrief()} for builder of http.request must be a JavaScript Object" }
files.forEach { entry ->
val (key, value) = entry
when {
value.isJsString() || value.isJsNumber() -> {
builder.addFormDataPart(RhinoUtils.coerceString(key), RhinoUtils.coerceString(value))
}
value is NativeArray -> when (value.length) {
2L -> {
val (fileName, path) = value
val file = if (path is URI) PFile(path) else PFile(path.toRuntimePath(scriptRuntime))
val mimeType = parseMimeType(file.extension)
val requestBody = file.asRequestBody(mimeType.toMediaTypeOrNull())
processFile(builder, key, fileName, requestBody)
}
3L -> {
val (fileName, mimeType, path) = value
val file = if (path is URI) PFile(path) else PFile(path.toRuntimePath(scriptRuntime))
val requestBody = file.asRequestBody(RhinoUtils.coerceString(mimeType).toMediaTypeOrNull())
processFile(builder, key, fileName, requestBody)
}
else -> listOf(
"Array value \"value\" for property \"files\"",
"in RequestBuilderHelper#parseMultipart",
"must be of length 2 or 3 instead of ${value.length}",
).joinToString(" ").let { throw WrappedIllegalArgumentException(it) }
}
value is PFileInterface -> {
val path = value.path
val file = PFile(path.toRuntimePath(scriptRuntime))
val fileName = file.name
val mimeType = parseMimeType(file.extension)
val requestBody = file.asRequestBody(mimeType.toMediaTypeOrNull())
processFile(builder, key, fileName, requestBody)
}
else -> listOf(
"Value \"value\" ${value.jsBrief()} for property \"files\"",
"in RequestBuilderHelper#parseMultipart",
"must be either a string, an array",
"or a JavaScript object",
).joinToString(" ").let { throw WrappedIllegalArgumentException(it) }
}
}
return builder.build()
}
fun parseMimeType(ext: String) = when {
ext.isNotEmpty() -> {
MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: Mime.APPLICATION_OCTET_STREAM
}
else -> Mime.APPLICATION_OCTET_STREAM
}
private fun setHeader(request: Request.Builder, key: Any?, it: Any?) {
request.header(RhinoUtils.coerceString(key), Context.toString(it))
}
private fun processFile(builder: MultipartBody.Builder, key: Any?, fileName: Any?, requestBody: RequestBody) {
builder.addFormDataPart(RhinoUtils.coerceString(key), fileName as? String, requestBody)
}
}

View File

@@ -0,0 +1,203 @@
package org.autojs.autojs.runtime.api.augment.http
import okhttp3.ResponseBody
import org.autojs.autojs.annotation.RhinoStandardFunctionInterface
import org.autojs.autojs.extension.AnyExtensions.toRuntimePath
import org.autojs.autojs.extension.FlexibleArray
import org.autojs.autojs.extension.FlexibleArray.Companion.component1
import org.autojs.autojs.extension.FlexibleArray.Companion.component2
import org.autojs.autojs.extension.ScriptableExtensions.prop
import org.autojs.autojs.pio.PFile
import org.autojs.autojs.runtime.ScriptRuntime
import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException
import org.autojs.autojs.util.RhinoUtils
import org.autojs.autojs.util.RhinoUtils.getRhinoStandardFunctionMethods
import org.autojs.autojs.util.RhinoUtils.withRhinoContext
import org.mozilla.javascript.BaseFunction
import org.mozilla.javascript.BoundFunction
import org.mozilla.javascript.Context
import org.mozilla.javascript.Function
import org.mozilla.javascript.NativeObject
import org.mozilla.javascript.Scriptable
import org.mozilla.javascript.Undefined
import java.io.InputStream
import java.io.OutputStream
import org.mozilla.javascript.ScriptRuntime as RhinoScriptRuntime
@Suppress("unused")
class ResponseBodyNativeObject(
val scriptRuntime: ScriptRuntime,
val resBody: ResponseBody,
val responseWrapper: ResponseWrapper,
private val cacheBody: Boolean,
private val cacheThresholdBytes: Long,
) : NativeObject() {
private val mResBodyObject: Scriptable by lazy {
RhinoScriptRuntime.toObject(scriptRuntime.topLevelScope, resBody)
}
init {
RhinoUtils.initNativeObjectPrototype(this)
}
// Record whether it has been explicitly closed.
// zh-CN: 记录是否已显式关闭.
@Volatile
var closed = false
override fun has(name: String, start: Scriptable): Boolean {
return mResBodyObject.has(name, start) || super.has(name, start)
}
override fun get(name: String, start: Scriptable): Any? {
val rhinoMethods = Companion::class.java.getRhinoStandardFunctionMethods()
rhinoMethods.firstOrNull { it.name == name }?.let {
return super.get(name, start)
}
return when (val o = mResBodyObject.prop(name)) {
is BaseFunction -> withRhinoContext { cx ->
BoundFunction(cx, mResBodyObject, o, mResBodyObject, arrayOf())
}
else -> super.get(name, start)
}
}
fun ensureOpen() {
if (closed) throw IllegalStateException("Response body already closed")
}
fun autoCloseIfNeeded() {
// Close immediately after reading the complete content to avoid resource leaks;
// stream() close is handled by the caller.
// zh-CN: 读取完整内容后立即关闭, 避免资源泄露; stream() 由调用者负责 close().
if (!closed) {
runCatching { resBody.close() }
closed = true
}
}
fun shouldCache(lengthHint: Long?): Boolean {
if (!cacheBody) return false
if (lengthHint == null || lengthHint < 0) return true
return lengthHint <= cacheThresholdBytes
}
companion object : FlexibleArray() {
@JvmStatic
@RhinoStandardFunctionInterface
fun string(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): String = ensureArgumentsIsEmpty(args) {
val o = thisObj as ResponseBodyNativeObject
o.responseWrapper.resBodyString?.let { return@ensureArgumentsIsEmpty it }
o.ensureOpen()
val contentLength = runCatching {
o.resBody.contentLength()
}.getOrDefault(-1L)
val str = o.resBody.string()
if (o.shouldCache(contentLength)) {
o.responseWrapper.resBodyString = str
}
// string() can safely close after consuming the stream.
// zh-CN: string() 消费流后可安全关闭.
o.autoCloseIfNeeded()
return@ensureArgumentsIsEmpty str
}
@JvmStatic
@RhinoStandardFunctionInterface
fun bytes(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): ByteArray = ensureArgumentsIsEmpty(args) {
val o = thisObj as ResponseBodyNativeObject
o.responseWrapper.resBodyBytes?.let { return@ensureArgumentsIsEmpty it }
o.ensureOpen()
val contentLength = runCatching {
o.resBody.contentLength()
}.getOrDefault(-1L)
val data = o.resBody.bytes()
if (o.shouldCache(contentLength)) {
o.responseWrapper.resBodyBytes = data
}
// bytes() 消费流后可安全关闭
o.autoCloseIfNeeded()
return@ensureArgumentsIsEmpty data
}
@JvmStatic
@RhinoStandardFunctionInterface
fun json(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): Any? = ensureArgumentsIsEmpty(args) {
val str = string(cx, thisObj, args, funObj)
runCatching {
return@ensureArgumentsIsEmpty RhinoUtils.js_json_parse(str)
}.onFailure {
throw IllegalStateException("Failed to parse JSON. Body string may be not in JSON format")
}
}
@JvmStatic
@RhinoStandardFunctionInterface
fun stream(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): InputStream = ensureArgumentsIsEmpty(args) {
val o = thisObj as ResponseBodyNativeObject
o.ensureOpen()
// Don't auto-close; let the caller handle it, supporting streaming copy.
// zh-CN: 不自动关闭; 交给调用者处理, 支持流式拷贝.
o.resBody.byteStream()
}
// Save directly to file (avoid loading large responses into memory).
// zh-CN: 直接保存到文件 (避免将大型响应加载到内存中).
@JvmStatic
@RhinoStandardFunctionInterface
fun saveToFile(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): HttpSaveResult = ensureArgumentsLengthInRange(args, 1..2) { argList ->
val o = thisObj as ResponseBodyNativeObject
o.ensureOpen()
val (pathRaw, bufSizeRaw) = argList
val path = RhinoUtils.coerceString(pathRaw, "").toRuntimePath(o.scriptRuntime)
val bufSize = RhinoUtils.coerceIntNumber(bufSizeRaw, 0).let {
if (it > 0) it else 8192
}
val file = PFile(path)
val isDirectory = file.isDirectory || path.endsWith("/")
if (isDirectory) {
throw WrappedIllegalArgumentException("Path \"$path\" must be a file path instead of a directory path")
}
val buffer = ByteArray(bufSize)
var copied = 0L
var input: InputStream? = null
var output: OutputStream? = null
return@ensureArgumentsLengthInRange try {
input = o.resBody.byteStream()
output = file.outputStream()
while (true) {
val read = input.read(buffer)
if (read == -1) break
output.write(buffer, 0, read)
copied += read
}
output.flush()
HttpSaveResult.ok(path, copied)
} catch (e: Throwable) {
HttpSaveResult.fail(path, copied, e)
} finally {
runCatching { input?.close() }
runCatching { output?.close() }
o.autoCloseIfNeeded()
}
}
// Explicit close.
// zh-CN: 显式关闭.
@JvmStatic
@RhinoStandardFunctionInterface
fun close(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): Undefined = ensureArgumentsIsEmpty(args) {
val o = thisObj as ResponseBodyNativeObject
if (!o.closed) {
runCatching { o.resBody.close() }
o.closed = true
}
RhinoUtils.UNDEFINED
}
}
}

View File

@@ -0,0 +1,70 @@
package org.autojs.autojs.runtime.api.augment.http
import okhttp3.Response
import org.autojs.autojs.extension.ArrayExtensions.toNativeArray
import org.autojs.autojs.extension.ScriptableExtensions.prop
import org.autojs.autojs.runtime.ScriptRuntime
import org.autojs.autojs.util.RhinoUtils.newNativeObject
import org.mozilla.javascript.NativeArray
import org.mozilla.javascript.NativeObject
import org.mozilla.javascript.ScriptableObject.PERMANENT
import org.mozilla.javascript.ScriptableObject.READONLY
class ResponseWrapper(
private val scriptRuntime: ScriptRuntime,
private val res: Response,
private val cacheBody: Boolean,
private val cacheThresholdBytes: Long,
) {
private val mRequest = res.request
var resBodyString: String? = null
var resBodyBytes: ByteArray? = null
fun wrap() = newNativeObject().apply {
put("request", this, mRequest)
put("statusMessage", this, res.message)
put("statusCode", this, res.code)
put("body", this, getBody())
put("headers", this, getHeaders())
put("url", this, mRequest.url)
put("method", this, mRequest.method)
}
private fun getBody(): ResponseBodyNativeObject {
// Returns a non-null value if this response
// was passed to Callback.onResponse
// or returned from Call.execute.
val resBody = res.body!!
return ResponseBodyNativeObject(scriptRuntime, resBody, this, cacheBody, cacheThresholdBytes).also {
it.defineFunctionProperties(arrayOf("string", "bytes", "json", "stream", "saveToFile", "close"), it.javaClass, READONLY or PERMANENT)
it.defineProperty(Http.KEY_CONTENT_TYPE, { resBody.contentType() }, null, READONLY or PERMANENT)
}
}
private fun getHeaders(): NativeObject {
val result = newNativeObject()
val headers = res.headers
for (i in 0 until headers.size) {
val name = headers.name(i).lowercase()
val value = headers.value(i)
if (!result.containsKey(name)) {
result.put(name, result, value)
continue
}
val list = mutableListOf<Any?>()
val origin = result.prop(name)
if (origin !is NativeArray) {
list += origin
} else {
list.addAll(origin)
}
list += value
result.put(name, result, list.toNativeArray())
}
return result
}
}

View File

@@ -7,6 +7,7 @@ import android.os.Handler
import android.os.Looper
import android.os.Parcelable
import org.autojs.autojs.AutoJs
import org.autojs.autojs.annotation.RhinoStandardFunctionInterface
import org.autojs.autojs.core.automator.UiObjectCollection
import org.autojs.autojs.extension.AnyExtensions.isJsNullish
import org.autojs.autojs.extension.AnyExtensions.jsBrief
@@ -50,6 +51,7 @@ import org.mozilla.javascript.Wrapper
import org.mozilla.javascript.json.JsonParser
import java.io.Serializable
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import java.math.BigInteger
import kotlin.math.floor
import kotlin.math.roundToInt
@@ -909,6 +911,13 @@ object RhinoUtils {
return null
}
@JvmStatic
fun Class<*>.getRhinoStandardFunctionMethods(): List<Method> {
return this.declaredMethods.filter {
it.isAnnotationPresent(RhinoStandardFunctionInterface::class.java)
}
}
class ObsoletedRhinoFunctionException(funcName: String) : Exception(
"Function \"$funcName\" can no longer be used as it has been obsoleted",
)

View File

@@ -1,5 +1,5 @@
#Fri Dec 26 16:11:42 CST 2025
BUILD_TIME=1766736702142
#Sat Dec 27 13:20:38 CST 2025
BUILD_TIME=1766812838968
COMPILE_SDK_VERSION=36
IMAGE_QUANT_CMAKE_VERSION=3.22.1
IMAGE_QUANT_NDK_VERSION=26.1.10909125
@@ -27,6 +27,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
TARGET_SDK_VERSION=36
TARGET_SDK_VERSION_INRT=29
VERSION_BUILD=3551
VERSION_BUILD=3554
VERSION_NAME=6.7.0 Alpha13
VSCODE_EXT_REQUIRED_VERSION=1.0.8