pull/5/head
fengyuecanzhu 4 years ago
parent f5a5d76b1e
commit b9a66814c1
  1. 78
      app/src/main/java/xyz/fycz/myreader/greendao/service/CookieStore.kt
  2. 28
      app/src/main/java/xyz/fycz/myreader/greendao/service/api/CookieManager.kt
  3. 3
      app/src/main/java/xyz/fycz/myreader/model/third2/analyzeRule/AnalyzeRule.java
  4. 10
      app/src/main/java/xyz/fycz/myreader/ui/activity/BookDetailedActivity.java
  5. 7
      app/src/main/java/xyz/fycz/myreader/ui/activity/SourceLoginActivity.java
  6. 2
      app/src/main/java/xyz/fycz/myreader/ui/activity/SplashActivity.java
  7. 365
      app/src/main/java/xyz/fycz/myreader/util/help/JSExtensions.kt
  8. 347
      app/src/main/java/xyz/fycz/myreader/util/help/JsExtensions.java
  9. 141
      app/src/main/java/xyz/fycz/myreader/util/utils/EncoderUtils.kt
  10. 96
      app/src/main/java/xyz/fycz/myreader/util/utils/FileUtils.java
  11. 73
      app/src/main/java/xyz/fycz/myreader/util/utils/MD5Utils.java
  12. 76
      app/src/main/java/xyz/fycz/myreader/util/utils/MD5Utils.kt
  13. 21
      app/src/main/java/xyz/fycz/myreader/util/utils/NetworkUtils.java
  14. 38
      app/src/main/java/xyz/fycz/myreader/util/utils/OkHttpUtils.java
  15. 3
      app/src/main/java/xyz/fycz/myreader/webapi/ThirdSourceApi.java

@ -0,0 +1,78 @@
@file:Suppress("unused")
package xyz.fycz.myreader.greendao.service
import android.text.TextUtils
import xyz.fycz.myreader.greendao.DbManager
import xyz.fycz.myreader.greendao.entity.CookieBean
import xyz.fycz.myreader.greendao.service.api.CookieManager
import xyz.fycz.myreader.util.utils.NetworkUtils
object CookieStore : CookieManager {
override fun setCookie(url: String, cookie: String?) {
val cookieBean = CookieBean(NetworkUtils.getSubDomain(url), cookie ?: "")
DbManager.getDaoSession().cookieBeanDao.insertOrReplace(cookieBean)
}
override fun replaceCookie(url: String, cookie: String) {
if (TextUtils.isEmpty(url) || TextUtils.isEmpty(cookie)) {
return
}
val oldCookie = getCookie(url)
if (TextUtils.isEmpty(oldCookie)) {
setCookie(url, cookie)
} else {
val cookieMap = cookieToMap(oldCookie)
cookieMap.putAll(cookieToMap(cookie))
val newCookie = mapToCookie(cookieMap)
setCookie(url, newCookie)
}
}
override fun getCookie(url: String): String {
val cookieBean = DbManager.getDaoSession().cookieBeanDao.load(NetworkUtils.getSubDomain(url))
return cookieBean?.cookie ?: ""
}
override fun removeCookie(url: String) {
DbManager.getDaoSession().cookieBeanDao.deleteByKey(NetworkUtils.getSubDomain(url))
}
override fun cookieToMap(cookie: String): MutableMap<String, String> {
val cookieMap = mutableMapOf<String, String>()
if (cookie.isBlank()) {
return cookieMap
}
val pairArray = cookie.split(";".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
for (pair in pairArray) {
val pairs = pair.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (pairs.size == 1) {
continue
}
val key = pairs[0].trim { it <= ' ' }
val value = pairs[1]
if (value.isNotBlank() || value.trim { it <= ' ' } == "null") {
cookieMap[key] = value.trim { it <= ' ' }
}
}
return cookieMap
}
override fun mapToCookie(cookieMap: Map<String, String>?): String? {
if (cookieMap == null || cookieMap.isEmpty()) {
return null
}
val builder = StringBuilder()
for (key in cookieMap.keys) {
val value = cookieMap[key]
if (value?.isNotBlank() == true) {
builder.append(key)
.append("=")
.append(value)
.append(";")
}
}
return builder.deleteCharAt(builder.lastIndexOf(";")).toString()
}
}

@ -0,0 +1,28 @@
package xyz.fycz.myreader.greendao.service.api
interface CookieManager {
/**
* 保存cookie
*/
fun setCookie(url: String, cookie: String?)
/**
* 替换cookie
*/
fun replaceCookie(url: String, cookie: String)
/**
* 获取cookie
*/
fun getCookie(url: String): String
/**
* 移除cookie
*/
fun removeCookie(url: String)
fun cookieToMap(cookie: String): MutableMap<String, String>
fun mapToCookie(cookieMap: Map<String, String>?): String?
}

@ -18,6 +18,7 @@ import java.util.regex.Pattern;
import javax.script.SimpleBindings; import javax.script.SimpleBindings;
import xyz.fycz.myreader.greendao.entity.Book; import xyz.fycz.myreader.greendao.entity.Book;
import xyz.fycz.myreader.greendao.service.CookieStore;
import xyz.fycz.myreader.util.StringHelper; import xyz.fycz.myreader.util.StringHelper;
import xyz.fycz.myreader.util.help.JsExtensions; import xyz.fycz.myreader.util.help.JsExtensions;
import xyz.fycz.myreader.util.utils.NetworkUtils; import xyz.fycz.myreader.util.utils.NetworkUtils;
@ -533,9 +534,9 @@ public class AnalyzeRule implements JsExtensions {
private Object evalJS(String jsStr, Object result) throws Exception { private Object evalJS(String jsStr, Object result) throws Exception {
SimpleBindings bindings = new SimpleBindings(); SimpleBindings bindings = new SimpleBindings();
bindings.put("java", this); bindings.put("java", this);
bindings.put("cookie", CookieStore.INSTANCE);
bindings.put("result", result); bindings.put("result", result);
bindings.put("baseUrl", baseUrl); bindings.put("baseUrl", baseUrl);
bindings.put("book", book);
return SCRIPT_ENGINE.eval(jsStr, bindings); return SCRIPT_ENGINE.eval(jsStr, bindings);
} }

@ -318,6 +318,7 @@ public class BookDetailedActivity extends BaseActivity {
mSourceDialog.setOnSourceChangeListener((bean, pos) -> { mSourceDialog.setOnSourceChangeListener((bean, pos) -> {
Book bookTem = (Book) mBook.clone(); Book bookTem = (Book) mBook.clone();
bookTem.setChapterUrl(bean.getChapterUrl()); bookTem.setChapterUrl(bean.getChapterUrl());
bookTem.setInfoUrl(bean.getInfoUrl());
if (!StringHelper.isEmpty(bean.getImgUrl())) { if (!StringHelper.isEmpty(bean.getImgUrl())) {
bookTem.setImgUrl(bean.getImgUrl()); bookTem.setImgUrl(bean.getImgUrl());
} }
@ -327,6 +328,15 @@ public class BookDetailedActivity extends BaseActivity {
if (!StringHelper.isEmpty(bean.getDesc())) { if (!StringHelper.isEmpty(bean.getDesc())) {
bookTem.setDesc(bean.getDesc()); bookTem.setDesc(bean.getDesc());
} }
if (!StringHelper.isEmpty(bean.getUpdateDate())){
bookTem.setUpdateDate(bean.getUpdateDate());
}
if (!StringHelper.isEmpty(bean.getWordCount())){
bookTem.setWordCount(bean.getWordCount());
}
if (!StringHelper.isEmpty(bean.getStatus())){
bookTem.setStatus(bean.getStatus());
}
bookTem.setSource(bean.getSource()); bookTem.setSource(bean.getSource());
if (isCollected) { if (isCollected) {
mBookService.updateBook(mBook, bookTem); mBookService.updateBook(mBook, bookTem);

@ -15,9 +15,8 @@ import xyz.fycz.myreader.R;
import xyz.fycz.myreader.base.BaseActivity; import xyz.fycz.myreader.base.BaseActivity;
import xyz.fycz.myreader.common.APPCONST; import xyz.fycz.myreader.common.APPCONST;
import xyz.fycz.myreader.databinding.ActivitySourceLoginBinding; import xyz.fycz.myreader.databinding.ActivitySourceLoginBinding;
import xyz.fycz.myreader.greendao.DbManager;
import xyz.fycz.myreader.greendao.entity.CookieBean;
import xyz.fycz.myreader.greendao.entity.rule.BookSource; import xyz.fycz.myreader.greendao.entity.rule.BookSource;
import xyz.fycz.myreader.greendao.service.CookieStore;
import xyz.fycz.myreader.util.ToastUtils; import xyz.fycz.myreader.util.ToastUtils;
/** /**
@ -67,14 +66,14 @@ public class SourceLoginActivity extends BaseActivity {
@Override @Override
public void onPageStarted(WebView view, String url, Bitmap favicon) { public void onPageStarted(WebView view, String url, Bitmap favicon) {
String cookie = cookieManager.getCookie(url); String cookie = cookieManager.getCookie(url);
DbManager.getDaoSession().getCookieBeanDao().insertOrReplace(new CookieBean(bookSource.getLoginUrl(), cookie)); CookieStore.INSTANCE.setCookie(bookSource.getSourceUrl(), cookie);
super.onPageStarted(view, url, favicon); super.onPageStarted(view, url, favicon);
} }
@Override @Override
public void onPageFinished(WebView view, String url) { public void onPageFinished(WebView view, String url) {
String cookie = cookieManager.getCookie(url); String cookie = cookieManager.getCookie(url);
DbManager.getDaoSession().getCookieBeanDao().insertOrReplace(new CookieBean(bookSource.getLoginUrl(), cookie)); CookieStore.INSTANCE.setCookie(bookSource.getSourceUrl(), cookie);
if (checking) if (checking)
finish(); finish();
else else

@ -269,7 +269,7 @@ public class SplashActivity extends BaseActivity {
SharedPreUtils preUtils = SharedPreUtils.getInstance(); SharedPreUtils preUtils = SharedPreUtils.getInstance();
String splashImageMD5 = preUtils.getString("splashImageMD5"); String splashImageMD5 = preUtils.getString("splashImageMD5");
if (!imgFile.exists() || preUtils.getBoolean("needUdSI") || if (!imgFile.exists() || preUtils.getBoolean("needUdSI") ||
!splashImageMD5.equals(MD5Utils.getFileMD5s(imgFile, 16))) { !splashImageMD5.equals(MD5Utils.INSTANCE.getFileMD5s(imgFile, 16))) {
if ("".equals(splashImageMD5)) return; if ("".equals(splashImageMD5)) return;
downLoadImage(); downLoadImage();
return; return;

@ -0,0 +1,365 @@
package xyz.fycz.myreader.util.help
import android.util.Base64
import android.util.Log
import androidx.annotation.Keep
import org.jsoup.Connection
import org.jsoup.Jsoup
import xyz.fycz.myreader.greendao.service.CookieStore
import xyz.fycz.myreader.model.third2.analyzeRule.AnalyzeUrl
import xyz.fycz.myreader.util.ZipUtils
import xyz.fycz.myreader.util.utils.*
import java.io.File
import java.io.IOException
import java.net.URLEncoder
import java.text.SimpleDateFormat
import java.util.*
/**
* @author fengyue
* @date 2021/5/15 19:26
*/
@Keep
@Suppress("unused")
interface JSExtensions {
/**
* js实现跨域访问,不能删
*/
fun ajax(urlStr: String?): String? {
return try {
val analyzeUrl = AnalyzeUrl(urlStr)
OkHttpUtils.getStrResponse(analyzeUrl).blockingFirst().body()
} catch (e: Exception) {
e.localizedMessage
}
}
/**
* js实现压缩文件解压
*/
fun unzipFile(zipPath: String): String {
if (zipPath.isEmpty()) return ""
val unzipPath = FileUtils.getCachePath() + File.separator + FileUtils.getNameExcludeExtension(zipPath)
FileUtils.deleteFile(unzipPath)
val zipFile = FileUtils.getFile(zipPath)
val unzipFolder = FileUtils.getFolder(unzipPath)
ZipUtils.unzipFile(zipFile, unzipFolder)
FileUtils.deleteFile(zipPath)
return unzipPath
}
/**
* js实现文件夹内所有文件读取
*/
fun getTxtInFolder(unzipPath: String): String {
if (unzipPath.isEmpty()) return ""
val unzipFolder = FileUtils.getFolder(unzipPath)
val contents = StringBuilder()
unzipFolder.listFiles().let {
if (it != null) {
for (f in it) {
val charsetName = FileUtils.getFileCharset(f)
contents.append(String(f.readBytes(), charset(charsetName)))
.append("\n")
}
contents.deleteCharAt(contents.length - 1)
}
}
FileUtils.deleteFile(unzipPath)
return contents.toString()
}
/**
* js实现重定向拦截,不能删
*/
@Throws(IOException::class)
operator fun get(urlStr: String?, headers: Map<String?, String?>?): Connection.Response? {
return Jsoup.connect(urlStr)
.sslSocketFactory(SSLSocketClient.createSSLSocketFactory())
.ignoreContentType(true)
.followRedirects(false)
.headers(headers)
.method(Connection.Method.GET)
.execute()
}
/**
* js实现重定向拦截,不能删
*/
@Throws(IOException::class)
fun post(
urlStr: String?,
body: String?,
headers: Map<String?, String?>?
): Connection.Response? {
return Jsoup.connect(urlStr)
.sslSocketFactory(SSLSocketClient.createSSLSocketFactory())
.ignoreContentType(true)
.followRedirects(false)
.requestBody(body)
.headers(headers)
.method(Connection.Method.POST)
.execute()
}
/**
*js实现读取cookie
*/
fun getCookie(tag: String, key: String? = null): String {
val cookie = CookieStore.getCookie(tag)
val cookieMap = CookieStore.cookieToMap(cookie)
return if (key != null) {
cookieMap[key] ?: ""
} else {
cookie
}
}
/**
* js实现解码,不能删
*/
fun base64Decode(str: String): String {
return EncoderUtils.base64Decode(str, Base64.NO_WRAP)
}
fun base64Decode(str: String, flags: Int): String {
return EncoderUtils.base64Decode(str, flags)
}
fun base64DecodeToByteArray(str: String?): ByteArray? {
if (str.isNullOrBlank()) {
return null
}
return Base64.decode(str, Base64.DEFAULT)
}
fun base64DecodeToByteArray(str: String?, flags: Int): ByteArray? {
if (str.isNullOrBlank()) {
return null
}
return Base64.decode(str, flags)
}
fun base64Encode(str: String): String? {
return EncoderUtils.base64Encode(str, Base64.NO_WRAP)
}
fun base64Encode(str: String, flags: Int): String? {
return EncoderUtils.base64Encode(str, flags)
}
fun md5Encode(str: String): String {
return MD5Utils.md5Encode(str)
}
fun md5Encode16(str: String): String {
return MD5Utils.md5Encode16(str)
}
/**
* 时间格式化
*/
fun timeFormat(time: Long): String {
val sdf = SimpleDateFormat("yyyy/MM/dd HH:mm")
return sdf.format(Date(time))
}
/**
* utf8编码转gbk编码
*/
fun utf8ToGbk(str: String): String {
val utf8 = String(str.toByteArray(charset("UTF-8")))
val unicode = String(utf8.toByteArray(), charset("UTF-8"))
return String(unicode.toByteArray(charset("GBK")))
}
fun encodeURI(str: String): String {
return try {
URLEncoder.encode(str, "UTF-8")
} catch (e: Exception) {
""
}
}
fun encodeURI(str: String, enc: String): String {
return try {
URLEncoder.encode(str, enc)
} catch (e: Exception) {
""
}
}
fun htmlFormat(str: String): String {
return StringUtils.formatHtml(str)
}
/**
* 读取本地文件
*/
fun readFile(path: String): ByteArray {
return File(path).readBytes()
}
fun readTxtFile(path: String): String {
val f = File(path)
val charsetName = FileUtils.getFileCharset(f)
return String(f.readBytes(), charset(charsetName))
}
fun readTxtFile(path: String, charsetName: String): String {
return String(File(path).readBytes(), charset(charsetName))
}
/**
* 输出调试日志
*/
fun log(msg: String): String {
Log.d("JS", msg)
return msg
}
/**
* AES 解码为 ByteArray
* @param str 传入的AES加密的数据
* @param key AES 解密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
fun aesDecodeToByteArray(
str: String,
key: String,
transformation: String,
iv: String = ""
): ByteArray? {
return EncoderUtils.decryptAES(
data = str.encodeToByteArray(),
key = key.encodeToByteArray(),
transformation,
iv.encodeToByteArray()
)
}
/**
* AES 解码为 String
* @param str 传入的AES加密的数据
* @param key AES 解密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
fun aesDecodeToString(
str: String,
key: String,
transformation: String,
iv: String = ""
): String? {
return aesDecodeToByteArray(str, key, transformation, iv)?.let { String(it) }
}
/**
* 已经base64的AES 解码为 ByteArray
* @param str 传入的AES Base64加密的数据
* @param key AES 解密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
fun aesBase64DecodeToByteArray(
str: String,
key: String,
transformation: String,
iv: String = ""
): ByteArray? {
return EncoderUtils.decryptBase64AES(
data = str.encodeToByteArray(),
key = key.encodeToByteArray(),
transformation,
iv.encodeToByteArray()
)
}
/**
* 已经base64的AES 解码为 String
* @param str 传入的AES Base64加密的数据
* @param key AES 解密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
fun aesBase64DecodeToString(
str: String,
key: String,
transformation: String,
iv: String = ""
): String? {
return aesBase64DecodeToByteArray(str, key, transformation, iv)?.let { String(it) }
}
/**
* 加密aes为ByteArray
* @param data 传入的原始数据
* @param key AES加密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
fun aesEncodeToByteArray(
data: String, key: String, transformation: String,
iv: String = ""
): ByteArray? {
return EncoderUtils.encryptAES(
data.encodeToByteArray(),
key = key.encodeToByteArray(),
transformation,
iv.encodeToByteArray()
)
}
/**
* 加密aes为String
* @param data 传入的原始数据
* @param key AES加密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
fun aesEncodeToString(
data: String, key: String, transformation: String,
iv: String = ""
): String? {
return aesEncodeToByteArray(data, key, transformation, iv)?.let { String(it) }
}
/**
* 加密aes后Base64化的ByteArray
* @param data 传入的原始数据
* @param key AES加密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
fun aesEncodeToBase64ByteArray(
data: String, key: String, transformation: String,
iv: String = ""
): ByteArray? {
return EncoderUtils.encryptAES2Base64(
data.encodeToByteArray(),
key = key.encodeToByteArray(),
transformation,
iv.encodeToByteArray()
)
}
/**
* 加密aes后Base64化的String
* @param data 传入的原始数据
* @param key AES加密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
fun aesEncodeToBase64String(
data: String, key: String, transformation: String,
iv: String = ""
): String? {
return aesEncodeToBase64ByteArray(data, key, transformation, iv)?.let { String(it) }
}
}

@ -1,22 +1,38 @@
package xyz.fycz.myreader.util.help; package xyz.fycz.myreader.util.help;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import org.jsoup.Connection; import org.jsoup.Connection;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import xyz.fycz.myreader.greendao.service.CookieStore;
import xyz.fycz.myreader.model.third2.analyzeRule.AnalyzeUrl; import xyz.fycz.myreader.model.third2.analyzeRule.AnalyzeUrl;
import xyz.fycz.myreader.util.ZipUtils;
import xyz.fycz.myreader.util.utils.EncoderUtils;
import xyz.fycz.myreader.util.utils.EncodingDetect;
import xyz.fycz.myreader.util.utils.FileUtils;
import xyz.fycz.myreader.util.utils.MD5Utils;
import xyz.fycz.myreader.util.utils.OkHttpUtils; import xyz.fycz.myreader.util.utils.OkHttpUtils;
import xyz.fycz.myreader.util.utils.StringUtils; import xyz.fycz.myreader.util.utils.StringUtils;
@SuppressWarnings({"unused", "WeakerAccess"}) @SuppressWarnings({"unused", "WeakerAccess"})
public interface JsExtensions { public interface JsExtensions {
String TAG = JsExtensions.class.getSimpleName();
/** /**
* js实现跨域访问,不能删 * js实现跨域访问,不能删
@ -31,27 +47,41 @@ public interface JsExtensions {
} }
/** /**
* js实现解码,不能删 * js实现压缩文件解压
*/ */
default String base64Decoder(String base64) { default String unzipFile(String zipPath) throws IOException {
return StringUtils.base64Decode(base64); if (zipPath.isEmpty()) return "";
String unzipPath = FileUtils.getCachePath() + File.separator + FileUtils.getNameExcludeExtension(zipPath);
FileUtils.deleteFile(unzipPath);
File zipFile = FileUtils.getFile(zipPath);
File unzipFolder = FileUtils.getFolder(unzipPath);
ZipUtils.unzipFile(zipFile, unzipFolder);
FileUtils.deleteFile(zipPath);
return unzipPath;
} }
/** /**
* 章节数转数字 * js实现文件夹内所有文件读取
*/ */
default String toNumChapter(String s) { default String getTxtInFolder(String unzipPath) throws UnsupportedEncodingException {
if (s == null) { if (unzipPath.isEmpty()) return "";
return null; File unzipFolder = FileUtils.getFolder(unzipPath);
StringBuilder contents = new StringBuilder();
File[] files = unzipFolder.listFiles();
if (files != null) {
for (File file : files) {
String charsetName = FileUtils.getFileCharset(file);
contents.append(new String(FileUtils.getBytes(file), Charset.forName(charsetName)))
.append("\n");
} }
Pattern pattern = Pattern.compile("(第)(.+?)(章)"); if (contents.length() > 0)
Matcher matcher = pattern.matcher(s); contents.deleteCharAt(contents.length() - 1);
if (matcher.find()) {
return matcher.group(1) + StringUtils.stringToInt(matcher.group(2)) + matcher.group(3);
} }
return s; FileUtils.deleteFile(unzipPath);
return contents.toString();
} }
/** /**
* js实现重定向拦截,不能删 * js实现重定向拦截,不能删
*/ */
@ -79,6 +109,79 @@ public interface JsExtensions {
.execute(); .execute();
} }
/**
* js实现读取cookie
*/
default String getCookie(String tag, String key) {
String cookie = CookieStore.INSTANCE.getCookie(tag);
Map<String, String> cookieMap = CookieStore.INSTANCE.cookieToMap(cookie);
if (key != null) {
return cookieMap.get(key) == null ? "" : cookieMap.get(key);
} else {
return cookie;
}
}
default String getCookie(String tag) {
return getCookie(tag, null);
}
/**
* js实现解码,不能删
*/
default String base64Decoder(String base64) {
return EncoderUtils.INSTANCE.base64Decode(base64, Base64.NO_WRAP);
}
default String base64Decoder(String base64, int flags) {
return EncoderUtils.INSTANCE.base64Decode(base64, flags);
}
default byte[] base64DecodeToByteArray(String str) {
if (TextUtils.isEmpty(str)) {
return null;
}
return Base64.decode(str, Base64.DEFAULT);
}
default byte[] base64DecodeToByteArray(String str, int flags) {
if (TextUtils.isEmpty(str)) {
return null;
}
return Base64.decode(str, flags);
}
default String base64Encode(String base64) {
return EncoderUtils.INSTANCE.base64Encode(base64, Base64.NO_WRAP);
}
default String base64Encode(String base64, int flags) {
return EncoderUtils.INSTANCE.base64Encode(base64, flags);
}
default String md5Encode(String str) {
return MD5Utils.INSTANCE.md5Encode(str);
}
default String md5Encode16(String str) {
return MD5Utils.INSTANCE.md5Encode16(str);
}
/**
* 章节数转数字
*/
default String toNumChapter(String s) {
if (s == null) {
return null;
}
Pattern pattern = Pattern.compile("(第)(.+?)(章)");
Matcher matcher = pattern.matcher(s);
if (matcher.find()) {
return matcher.group(1) + StringUtils.stringToInt(matcher.group(2)) + matcher.group(3);
}
return s;
}
/** /**
* 时间格式化 * 时间格式化
*/ */
@ -86,4 +189,224 @@ public interface JsExtensions {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm"); SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm");
return sdf.format(new Date(time)); return sdf.format(new Date(time));
} }
/**
* utf8编码转gbk编码
*/
default String utf8ToGbk(String str) {
String utf8 = new String(str.getBytes(StandardCharsets.UTF_8));
String unicode = new String(utf8.getBytes(), StandardCharsets.UTF_8);
return new String(unicode.getBytes(Charset.forName("GBK")));
}
default String encodeURI(String str) {
try {
return URLEncoder.encode(str, "UTF-8");
} catch (Exception e) {
return "";
}
}
default String encodeURI(String str, String enc) {
try {
return URLEncoder.encode(str, enc);
} catch (Exception e) {
return "";
}
}
/**
* 读取本地文件
*/
default byte[] readFile(String path) {
return FileUtils.getBytes(new File(path));
}
default String readTxtFile(String path) {
File f = new File(path);
String charsetName = FileUtils.getFileCharset(f);
return new String(FileUtils.getBytes(f), Charset.forName(charsetName));
}
default String readTxtFile(String path, String charsetName) {
return new String(FileUtils.getBytes(new File(path)), Charset.forName(charsetName));
}
/**
* 输出调试日志
*/
default String log(String msg) {
Log.d(TAG, msg);
return msg;
}
/**
* AES 解码为 ByteArray
*
* @param str 传入的AES加密的数据
* @param key AES 解密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
default byte[] aesDecodeToByteArray(String str, String key, String transformation, String iv) {
return EncoderUtils.INSTANCE.decryptAES(
str.getBytes(StandardCharsets.UTF_8),
key.getBytes(StandardCharsets.UTF_8),
transformation,
iv.getBytes(StandardCharsets.UTF_8)
);
}
default byte[] aesDecodeToByteArray(String str, String key, String transformation) {
return aesDecodeToByteArray(str, key, transformation, "");
}
/**
* AES 解码为 String
*
* @param str 传入的AES加密的数据
* @param key AES 解密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
default String aesDecodeToString(String str, String key, String transformation, String iv) {
byte[] bytes = aesDecodeToByteArray(str, key, transformation, iv);
if (bytes == null) {
return "";
}
return new String(bytes);
}
default String aesDecodeToString(String str, String key, String transformation) {
return aesDecodeToString(str, key, transformation, "");
}
/**
* 已经base64的AES 解码为 ByteArray
*
* @param str 传入的AES Base64加密的数据
* @param key AES 解密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
default byte[] aesBase64DecodeToByteArray(String str, String key, String transformation, String iv) {
return EncoderUtils.INSTANCE.decryptBase64AES(
str.getBytes(StandardCharsets.UTF_8),
key.getBytes(StandardCharsets.UTF_8),
transformation,
iv.getBytes(StandardCharsets.UTF_8)
);
}
default byte[] aesBase64DecodeToByteArray(String str, String key, String transformation) {
return aesBase64DecodeToByteArray(str, key, transformation, "");
}
/**
* 已经base64的AES 解码为 String
*
* @param str 传入的AES Base64加密的数据
* @param key AES 解密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
default String aesBase64DecodeToString(String str, String key, String transformation, String iv) {
byte[] bytes = aesBase64DecodeToByteArray(str, key, transformation, iv);
if (bytes == null) {
return "";
}
return new String(bytes);
}
default String aesBase64DecodeToString(String str, String key, String transformation) {
return aesBase64DecodeToString(str, key, transformation, "");
}
/**
* 加密aes为ByteArray
*
* @param data 传入的原始数据
* @param key AES加密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
default byte[] aesEncodeToByteArray(String data, String key, String transformation, String iv) {
return EncoderUtils.INSTANCE.encryptAES(
data.getBytes(StandardCharsets.UTF_8),
key.getBytes(StandardCharsets.UTF_8),
transformation,
iv.getBytes(StandardCharsets.UTF_8)
);
}
default byte[] aesEncodeToByteArray(String data, String key, String transformation) {
return aesEncodeToByteArray(data, key, transformation, "");
}
/**
* 加密aes为String
*
* @param data 传入的原始数据
* @param key AES加密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
default String aesEncodeToString(String data, String key, String transformation, String iv) {
byte[] bytes = aesEncodeToByteArray(data, key, transformation, iv);
if (bytes == null) {
return "";
}
return new String(bytes);
}
default String aesEncodeToString(String str, String key, String transformation) {
return aesEncodeToString(str, key, transformation, "");
}
/**
* 加密aes后Base64化的ByteArray
*
* @param data 传入的原始数据
* @param key AES加密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
default byte[] aesEncodeToBase64ByteArray(String data, String key, String transformation, String iv) {
return EncoderUtils.INSTANCE.encryptAES2Base64(
data.getBytes(StandardCharsets.UTF_8),
key.getBytes(StandardCharsets.UTF_8),
transformation,
iv.getBytes(StandardCharsets.UTF_8)
);
}
default byte[] aesEncodeToBase64ByteArray(String data, String key, String transformation) {
return aesEncodeToBase64ByteArray(data, key, transformation, "");
}
/**
* 加密aes后Base64化的String
*
* @param data 传入的原始数据
* @param key AES加密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
default String aesEncodeToBase64String(String data, String key, String transformation, String iv) {
byte[] bytes = aesEncodeToBase64ByteArray(data, key, transformation, iv);
if (bytes == null) {
return "";
}
return new String(bytes);
}
default String aesEncodeToBase64String(String str, String key, String transformation) {
return aesEncodeToBase64String(str, key, transformation, "");
}
} }

@ -0,0 +1,141 @@
package xyz.fycz.myreader.util.utils
import android.util.Base64
import java.security.spec.AlgorithmParameterSpec
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
@Suppress("unused")
object EncoderUtils {
@JvmOverloads
fun base64Decode(str: String, flags: Int = Base64.DEFAULT): String {
val bytes = Base64.decode(str, flags)
return String(bytes)
}
@JvmOverloads
fun base64Encode(str: String, flags: Int = Base64.NO_WRAP): String? {
return Base64.encodeToString(str.toByteArray(), flags)
}
//////////AES Start
/**
* Return the Base64-encode bytes of AES encryption.
*
* @param data The data.
* @param key The key.
* @param transformation The name of the transformation, e.g., *DES/CBC/PKCS5Padding*.
* @param iv The buffer with the IV. The contents of the
* buffer are copied to protect against subsequent modification.
* @return the Base64-encode bytes of AES encryption
*/
fun encryptAES2Base64(
data: ByteArray?,
key: ByteArray?,
transformation: String?,
iv: ByteArray?
): ByteArray? {
return Base64.encode(encryptAES(data, key, transformation, iv), Base64.NO_WRAP)
}
/**
* Return the bytes of AES encryption.
*
* @param data The data.
* @param key The key.
* @param transformation The name of the transformation, e.g., *DES/CBC/PKCS5Padding*.
* @param iv The buffer with the IV. The contents of the
* buffer are copied to protect against subsequent modification.
* @return the bytes of AES encryption
*/
fun encryptAES(
data: ByteArray?,
key: ByteArray?,
transformation: String?,
iv: ByteArray?
): ByteArray? {
return symmetricTemplate(data, key, "AES", transformation!!, iv, true)
}
/**
* Return the bytes of AES decryption for Base64-encode bytes.
*
* @param data The data.
* @param key The key.
* @param transformation The name of the transformation, e.g., *DES/CBC/PKCS5Padding*.
* @param iv The buffer with the IV. The contents of the
* buffer are copied to protect against subsequent modification.
* @return the bytes of AES decryption for Base64-encode bytes
*/
fun decryptBase64AES(
data: ByteArray?,
key: ByteArray?,
transformation: String?,
iv: ByteArray?
): ByteArray? {
return decryptAES(Base64.decode(data, Base64.NO_WRAP), key, transformation, iv)
}
/**
* Return the bytes of AES decryption.
*
* @param data The data.
* @param key The key.
* @param transformation The name of the transformation, e.g., *DES/CBC/PKCS5Padding*.
* @param iv The buffer with the IV. The contents of the
* buffer are copied to protect against subsequent modification.
* @return the bytes of AES decryption
*/
fun decryptAES(
data: ByteArray?,
key: ByteArray?,
transformation: String?,
iv: ByteArray?
): ByteArray? {
return symmetricTemplate(data, key, "AES", transformation!!, iv, false)
}
/**
* Return the bytes of symmetric encryption or decryption.
*
* @param data The data.
* @param key The key.
* @param algorithm The name of algorithm.
* @param transformation The name of the transformation, e.g., <i>DES/CBC/PKCS5Padding</i>.
* @param isEncrypt True to encrypt, false otherwise.
* @return the bytes of symmetric encryption or decryption
*/
private fun symmetricTemplate(
data: ByteArray?,
key: ByteArray?,
algorithm: String,
transformation: String,
iv: ByteArray?,
isEncrypt: Boolean
): ByteArray? {
return if (data == null || data.isEmpty() || key == null || key.isEmpty()) null else try {
val keySpec = SecretKeySpec(key, algorithm)
val cipher = Cipher.getInstance(transformation)
if (iv == null || iv.isEmpty()) {
cipher.init(if (isEncrypt) Cipher.ENCRYPT_MODE else Cipher.DECRYPT_MODE, keySpec)
} else {
val params: AlgorithmParameterSpec = IvParameterSpec(iv)
cipher.init(
if (isEncrypt) Cipher.ENCRYPT_MODE else Cipher.DECRYPT_MODE,
keySpec,
params
)
}
cipher.doFinal(data)
} catch (e: Throwable) {
e.printStackTrace()
null
}
}
}

@ -256,7 +256,7 @@ public class FileUtils {
FileOutputStream fos = null; FileOutputStream fos = null;
File temFile = null; File temFile = null;
try { try {
temFile = getFile(APPCONST.TEM_FILE_DIR + "tem.fy"); temFile = getFile(getCachePath() + File.separator + "tem.fy");
fis = new FileInputStream(file); fis = new FileInputStream(file);
fos = new FileOutputStream(temFile); fos = new FileOutputStream(temFile);
//用10kb作为试探 //用10kb作为试探
@ -878,83 +878,6 @@ public class FileUtils {
return null; return null;
} }
public static byte[] File2byte(String filePath)
{
byte[] buffer = null;
try
{
File file = new File(filePath);
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int n;
while ((n = fis.read(b)) != -1)
{
bos.write(b, 0, n);
}
fis.close();
bos.close();
buffer = bos.toByteArray();
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
catch (IOException e)
{
e.printStackTrace();
}
return buffer;
}
public static void byte2File(byte[] buf, String filePath, String fileName)
{
BufferedOutputStream bos = null;
FileOutputStream fos = null;
File file = null;
try
{
File dir = new File(filePath);
if (!dir.exists() && dir.isDirectory())
{
dir.mkdirs();
}
file = new File(filePath + File.separator + fileName);
fos = new FileOutputStream(file);
bos = new BufferedOutputStream(fos);
bos.write(buf);
}
catch (Exception e)
{
e.printStackTrace();
}
finally
{
if (bos != null)
{
try
{
bos.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
if (fos != null)
{
try
{
fos.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
}
public static String getFileSuffix(String filePath) { public static String getFileSuffix(String filePath) {
File file = new File(filePath); File file = new File(filePath);
return getFileSuffix(file); return getFileSuffix(file);
@ -968,4 +891,21 @@ public class FileUtils {
int dotIndex = fileName.lastIndexOf("."); int dotIndex = fileName.lastIndexOf(".");
return dotIndex > 0 ? fileName.substring(dotIndex) : ""; return dotIndex > 0 ? fileName.substring(dotIndex) : "";
} }
/**
* 获取文件名不包括扩展名
*/
public static String getNameExcludeExtension(String filePath) {
try {
String fileName = new File(filePath).getName();
int lastIndexOf = fileName.lastIndexOf(".");
if (lastIndexOf != -1) {
fileName = fileName.substring(0, lastIndexOf);
}
return fileName;
} catch (Exception e) {
return "";
}
}
} }

@ -1,73 +0,0 @@
package xyz.fycz.myreader.util.utils;
import java.io.File;
import java.io.FileInputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import xyz.fycz.myreader.util.IOUtils;
public class MD5Utils {
public static String strToMd5By32(String str) {
String reStr = null;
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] bytes = md5.digest(str.getBytes());
StringBuilder stringBuffer = new StringBuilder();
for (byte b : bytes) {
int bt = b & 0xff;
if (bt < 16) {
stringBuffer.append(0);
}
stringBuffer.append(Integer.toHexString(bt));
}
reStr = stringBuffer.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return reStr;
}
public static String strToMd5By16(String str) {
String reStr = strToMd5By32(str);
if (reStr != null) {
reStr = reStr.substring(8, 24);
}
return reStr;
}
/**
* 获取单个文件的MD5值
* @param file 文件
* @param radix 16 32 64
*
* @return
*/
public static String getFileMD5s(File file, int radix) {
if (!file.isFile()) {
return null;
}
MessageDigest digest = null;
FileInputStream in = null;
byte[] buffer = new byte[1024];
int len;
try {
digest = MessageDigest.getInstance("MD5");
in = new FileInputStream(file);
while ((len = in.read(buffer, 0, 1024)) != -1) {
digest.update(buffer, 0, len);
}
} catch (Exception e) {
e.printStackTrace();
return null;
}finally {
IOUtils.close(in);
}
BigInteger bigInt = new BigInteger(1, digest.digest());
return bigInt.toString(radix);
}
}

@ -0,0 +1,76 @@
package xyz.fycz.myreader.util.utils
import xyz.fycz.myreader.util.IOUtils
import java.io.File
import java.io.FileInputStream
import java.math.BigInteger
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import kotlin.experimental.and
/**
* 将字符串转化为MD5
*/
@Suppress("unused")
object MD5Utils {
fun md5Encode(str: String?): String {
if (str == null) return ""
var reStr = ""
try {
val md5: MessageDigest = MessageDigest.getInstance("MD5")
val bytes: ByteArray = md5.digest(str.toByteArray())
val stringBuffer: StringBuilder = StringBuilder()
for (b in bytes) {
val bt: Int = b.toInt() and 0xff
if (bt < 16) {
stringBuffer.append(0)
}
stringBuffer.append(Integer.toHexString(bt))
}
reStr = stringBuffer.toString()
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
}
return reStr
}
fun md5Encode16(str: String): String {
var reStr = md5Encode(str)
reStr = reStr.substring(8, 24)
return reStr
}
/**
* 获取单个文件的MD5值
* @param file 文件
* @param radix 16 32 64
*
* @return
*/
fun getFileMD5s(file: File, radix: Int): String? {
if (!file.isFile) {
return null
}
var digest: MessageDigest? = null
var `in`: FileInputStream? = null
val buffer = ByteArray(1024)
var len: Int
try {
digest = MessageDigest.getInstance("MD5")
`in` = FileInputStream(file)
while (`in`.read(buffer, 0, 1024).also { len = it } != -1) {
digest.update(buffer, 0, len)
}
} catch (e: Exception) {
e.printStackTrace()
return null
} finally {
IOUtils.close(`in`)
}
val bigInt = BigInteger(1, digest!!.digest())
return bigInt.toString(radix)
}
}

@ -50,6 +50,7 @@ public class NetworkUtils {
return false; return false;
} }
} }
public static String getUrl(Response response) { public static String getUrl(Response response) {
okhttp3.Response networkResponse = response.networkResponse(); okhttp3.Response networkResponse = response.networkResponse();
if (networkResponse != null) { if (networkResponse != null) {
@ -104,11 +105,31 @@ public class NetworkUtils {
} }
return relativePath; return relativePath;
} }
public static boolean isUrl(String urlStr) { public static boolean isUrl(String urlStr) {
if (urlStr == null) return false; if (urlStr == null) return false;
String regex = "^(https?)://.+$";//设置正则表达式 String regex = "^(https?)://.+$";//设置正则表达式
return urlStr.matches(regex); return urlStr.matches(regex);
} }
public static String getBaseUrl(String url) {
if (url == null || !url.startsWith("http")) return null;
int index = url.indexOf("/", 9);
if (index == -1) {
return url;
} else return url.substring(0, index);
}
public static String getSubDomain(String url) {
String baseUrl = getBaseUrl(url);
if (baseUrl == null) return "";
if (baseUrl.indexOf(".") == baseUrl.lastIndexOf(".")) {
return baseUrl.substring(baseUrl.lastIndexOf("/") + 1);
} else {
return baseUrl.substring(baseUrl.indexOf(".") + 1);
}
}
/** /**
* Ipv4 address check. * Ipv4 address check.
*/ */

@ -15,24 +15,30 @@ import org.json.JSONObject;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import io.reactivex.Observable;
import okhttp3.*;
import xyz.fycz.myreader.application.App;
import xyz.fycz.myreader.common.APPCONST;
import xyz.fycz.myreader.entity.StrResponse;
import xyz.fycz.myreader.greendao.DbManager;
import xyz.fycz.myreader.greendao.entity.CookieBean;
import xyz.fycz.myreader.greendao.entity.rule.BookSource;
import xyz.fycz.myreader.model.third2.analyzeRule.AnalyzeUrl;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import io.reactivex.Observable;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import xyz.fycz.myreader.application.App;
import xyz.fycz.myreader.common.APPCONST;
import xyz.fycz.myreader.entity.StrResponse;
import xyz.fycz.myreader.greendao.entity.rule.BookSource;
import xyz.fycz.myreader.greendao.service.CookieStore;
import xyz.fycz.myreader.model.third2.analyzeRule.AnalyzeUrl;
import static xyz.fycz.myreader.util.help.SSLSocketClient.createSSLSocketFactory; import static xyz.fycz.myreader.util.help.SSLSocketClient.createSSLSocketFactory;
import static xyz.fycz.myreader.util.help.SSLSocketClient.createTrustAllManager; import static xyz.fycz.myreader.util.help.SSLSocketClient.createTrustAllManager;
import static xyz.fycz.myreader.util.help.SSLSocketClient.getHeaderInterceptor; import static xyz.fycz.myreader.util.help.SSLSocketClient.getHeaderInterceptor;
@ -82,7 +88,7 @@ public class OkHttpUtils {
if (body == null) { if (body == null) {
return ""; return "";
} else { } else {
String bodyStr = new String(body.bytes(), encodeType); String bodyStr = new String(body.bytes(), Charset.forName(encodeType));
Log.d("Http: read finish", bodyStr); Log.d("Http: read finish", bodyStr);
return bodyStr; return bodyStr;
} }
@ -253,8 +259,7 @@ public class OkHttpUtils {
webView.setWebViewClient(new WebViewClient() { webView.setWebViewClient(new WebViewClient() {
@Override @Override
public void onPageFinished(WebView view, String url) { public void onPageFinished(WebView view, String url) {
DbManager.getDaoSession().getCookieBeanDao() CookieStore.INSTANCE.setCookie(tag, cookieManager.getCookie(webView.getUrl()));
.insertOrReplace(new CookieBean(tag, cookieManager.getCookie(webView.getUrl())));
handler.postDelayed(retryRunnable, 1000); handler.postDelayed(retryRunnable, 1000);
} }
}); });
@ -286,7 +291,7 @@ public class OkHttpUtils {
} }
String cookie = cookieBuilder.toString(); String cookie = cookieBuilder.toString();
if (!TextUtils.isEmpty(cookie)) { if (!TextUtils.isEmpty(cookie)) {
DbManager.getDaoSession().getCookieBeanDao().insertOrReplace(new CookieBean(tag, cookie)); CookieStore.INSTANCE.setCookie(tag, cookie);
} }
} }
e.onNext(response); e.onNext(response);
@ -304,10 +309,7 @@ public class OkHttpUtils {
public static Map<String, String> getCookies(String url) { public static Map<String, String> getCookies(String url) {
Map<String, String> cookieMap = new HashMap<>(); Map<String, String> cookieMap = new HashMap<>();
if (url != null) { if (url != null) {
CookieBean cookie = DbManager.getDaoSession().getCookieBeanDao().load(url); cookieMap.put("Cookie", CookieStore.INSTANCE.getCookie(url));
if (cookie != null) {
cookieMap.put("Cookie", cookie.getCookie());
}
} }
return cookieMap; return cookieMap;
} }

@ -1,6 +1,5 @@
package xyz.fycz.myreader.webapi; package xyz.fycz.myreader.webapi;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
@ -10,10 +9,8 @@ import io.reactivex.ObservableOnSubscribe;
import io.reactivex.ObservableSource; import io.reactivex.ObservableSource;
import io.reactivex.functions.Function; import io.reactivex.functions.Function;
import xyz.fycz.myreader.entity.SearchBookBean; import xyz.fycz.myreader.entity.SearchBookBean;
import xyz.fycz.myreader.greendao.DbManager;
import xyz.fycz.myreader.greendao.entity.Book; import xyz.fycz.myreader.greendao.entity.Book;
import xyz.fycz.myreader.greendao.entity.Chapter; import xyz.fycz.myreader.greendao.entity.Chapter;
import xyz.fycz.myreader.greendao.entity.CookieBean;
import xyz.fycz.myreader.greendao.entity.rule.BookSource; import xyz.fycz.myreader.greendao.entity.rule.BookSource;
import xyz.fycz.myreader.model.mulvalmap.ConMVMap; import xyz.fycz.myreader.model.mulvalmap.ConMVMap;
import xyz.fycz.myreader.model.third2.analyzeRule.AnalyzeUrl; import xyz.fycz.myreader.model.third2.analyzeRule.AnalyzeUrl;

Loading…
Cancel
Save