Added support for non-ascii characters in subscriptions, collections and artifacts

This commit is contained in:
Peng-YM 2022-05-30 13:47:54 +08:00
parent 4dbafb77c5
commit e28e440dc5
9 changed files with 80 additions and 106 deletions

View File

@ -6,7 +6,7 @@
* *
* *
* Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket! * Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket!
* @updated: 2022/5/26 上午11:17:01 * @updated: 2022/5/30 下午1:47:20
* @version: 1.6 * @version: 1.6
* @author: Peng-YM * @author: Peng-YM
* @github: https://github.com/Peng-YM/Sub-Store * @github: https://github.com/Peng-YM/Sub-Store

View File

@ -30,7 +30,8 @@ export default function register($app) {
} }
async function getArtifact(req, res) { async function getArtifact(req, res) {
const name = req.params.name; let { name } = req.params;
name = decodeURIComponent(name);
const action = req.query.action; const action = req.query.action;
const allArtifacts = $.read(ARTIFACTS_KEY); const allArtifacts = $.read(ARTIFACTS_KEY);
const artifact = allArtifacts[name]; const artifact = allArtifacts[name];
@ -61,14 +62,15 @@ async function getArtifact(req, res) {
console.log(JSON.stringify(artifact, null, 2)); console.log(JSON.stringify(artifact, null, 2));
try { try {
const resp = await syncArtifact({ const resp = await syncArtifact({
[artifact.name]: { content: output }, [encodeURIComponent(artifact.name)]: {
content: output,
},
}); });
artifact.updated = new Date().getTime(); artifact.updated = new Date().getTime();
const body = JSON.parse(resp.body); const body = JSON.parse(resp.body);
artifact.url = body.files[artifact.name].raw_url.replace( artifact.url = body.files[
/\/raw\/[^/]*\/(.*)/, encodeURIComponent(artifact.name)
'/raw/$1', ].raw_url.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
);
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
res.json({ res.json({
status: 'success', status: 'success',
@ -104,25 +106,19 @@ function createArtifact(req, res) {
message: `远程配置${artifact.name}已存在!`, message: `远程配置${artifact.name}已存在!`,
}); });
} else { } else {
if (/^[\w-_.]*$/.test(artifact.name)) { allArtifacts[artifact.name] = artifact;
allArtifacts[artifact.name] = artifact; $.write(allArtifacts, ARTIFACTS_KEY);
$.write(allArtifacts, ARTIFACTS_KEY); res.status(201).json({
res.status(201).json({ status: 'success',
status: 'success', data: artifact,
data: artifact, });
});
} else {
res.status(500).json({
status: 'failed',
message: `远程配置名称 ${artifact.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
});
}
} }
} }
function updateArtifact(req, res) { function updateArtifact(req, res) {
const allArtifacts = $.read(ARTIFACTS_KEY); const allArtifacts = $.read(ARTIFACTS_KEY);
const oldName = req.params.name; let oldName = req.params.name;
oldName = decodeURIComponent(oldName);
const artifact = allArtifacts[oldName]; const artifact = allArtifacts[oldName];
if (artifact) { if (artifact) {
$.info(`正在更新远程配置:${artifact.name}`); $.info(`正在更新远程配置:${artifact.name}`);
@ -215,7 +211,8 @@ async function cronSyncArtifacts(_, res) {
} }
async function deleteArtifact(req, res) { async function deleteArtifact(req, res) {
const name = req.params.name; let { name } = req.params;
name = decodeURIComponent(name);
$.info(`正在删除远程配置:${name}`); $.info(`正在删除远程配置:${name}`);
const allArtifacts = $.read(ARTIFACTS_KEY); const allArtifacts = $.read(ARTIFACTS_KEY);
try { try {
@ -223,10 +220,11 @@ async function deleteArtifact(req, res) {
if (!artifact) throw new Error(`远程配置:${name}不存在!`); if (!artifact) throw new Error(`远程配置:${name}不存在!`);
if (artifact.updated) { if (artifact.updated) {
// delete gist // delete gist
await syncArtifact({ const files = {};
filename: name, files[encodeURIComponent(artifact.name)] = {
content: '', content: '',
}); };
await syncArtifact(files);
} }
// delete local cache // delete local cache
delete allArtifacts[name]; delete allArtifacts[name];
@ -235,9 +233,7 @@ async function deleteArtifact(req, res) {
status: 'success', status: 'success',
}); });
} catch (err) { } catch (err) {
// delete local cache $.error(`无法删除远程配置:${name},原因:${err}`);
delete allArtifacts[name];
$.write(allArtifacts, ARTIFACTS_KEY);
res.status(500).json({ res.status(500).json({
status: 'failed', status: 'failed',
message: `无法删除远程配置:${name}, 原因:${err}`, message: `无法删除远程配置:${name}, 原因:${err}`,

View File

@ -20,7 +20,9 @@ export default function register($app) {
// collection API // collection API
async function downloadCollection(req, res) { async function downloadCollection(req, res) {
const { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name);
const { raw } = req.query || 'false'; const { raw } = req.query || 'false';
const platform = const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON'; req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
@ -90,24 +92,17 @@ function createCollection(req, res) {
message: `订阅集${collection.name}已存在!`, message: `订阅集${collection.name}已存在!`,
}); });
} }
// validate name allCol[collection.name] = collection;
if (/^[\w-_]*$/.test(collection.name)) { $.write(allCol, COLLECTIONS_KEY);
allCol[collection.name] = collection; res.status(201).json({
$.write(allCol, COLLECTIONS_KEY); status: 'success',
res.status(201).json({ data: collection,
status: 'success', });
data: collection,
});
} else {
res.status(500).json({
status: 'failed',
message: `订阅集名称 ${collection.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
});
}
} }
function getCollection(req, res) { function getCollection(req, res) {
const { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name);
const collection = $.read(COLLECTIONS_KEY)[name]; const collection = $.read(COLLECTIONS_KEY)[name];
if (collection) { if (collection) {
res.json({ res.json({
@ -123,7 +118,8 @@ function getCollection(req, res) {
} }
function updateCollection(req, res) { function updateCollection(req, res) {
const { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name);
let collection = req.body; let collection = req.body;
const allCol = $.read(COLLECTIONS_KEY); const allCol = $.read(COLLECTIONS_KEY);
if (allCol[name]) { if (allCol[name]) {
@ -149,7 +145,8 @@ function updateCollection(req, res) {
} }
function deleteCollection(req, res) { function deleteCollection(req, res) {
const { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name);
$.info(`正在删除组合订阅:${name}`); $.info(`正在删除组合订阅:${name}`);
let allCol = $.read(COLLECTIONS_KEY); let allCol = $.read(COLLECTIONS_KEY);
delete allCol[name]; delete allCol[name];

View File

@ -17,7 +17,9 @@ export default function register($app) {
// subscriptions API // subscriptions API
async function downloadSubscription(req, res) { async function downloadSubscription(req, res) {
const { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name);
const { raw } = req.query || 'false'; const { raw } = req.query || 'false';
const platform = const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON'; req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
@ -80,24 +82,17 @@ function createSubscription(req, res) {
message: `订阅${sub.name}已存在!`, message: `订阅${sub.name}已存在!`,
}); });
} }
// validate name allSubs[sub.name] = sub;
if (/^[\w-_]*$/.test(sub.name)) { $.write(allSubs, SUBS_KEY);
allSubs[sub.name] = sub; res.status(201).json({
$.write(allSubs, SUBS_KEY); status: 'success',
res.status(201).json({ data: sub,
status: 'success', });
data: sub,
});
} else {
res.status(500).json({
status: 'failed',
message: `订阅名称 ${sub.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
});
}
} }
function getSubscription(req, res) { function getSubscription(req, res) {
const { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name);
const sub = $.read(SUBS_KEY)[name]; const sub = $.read(SUBS_KEY)[name];
if (sub) { if (sub) {
res.json({ res.json({
@ -113,7 +108,8 @@ function getSubscription(req, res) {
} }
function updateSubscription(req, res) { function updateSubscription(req, res) {
const { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name);
let sub = req.body; let sub = req.body;
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
if (allSubs[name]) { if (allSubs[name]) {
@ -152,7 +148,8 @@ function updateSubscription(req, res) {
} }
function deleteSubscription(req, res) { function deleteSubscription(req, res) {
const { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name);
$.info(`删除订阅:${name}...`); $.info(`删除订阅:${name}...`);
// delete from subscriptions // delete from subscriptions
let allSubs = $.read(SUBS_KEY); let allSubs = $.read(SUBS_KEY);

File diff suppressed because one or more lines are too long

View File

@ -48,7 +48,7 @@ const router = new Router({
name: "collection-edit", name: "collection-edit",
component: SubEditor, component: SubEditor,
props: {isCollection: true}, props: {isCollection: true},
meta: {title: "订阅编辑"} meta: {title: "组合订阅编辑"}
} }
] ]
}); });

View File

@ -27,7 +27,6 @@
<v-text-field <v-text-field
v-model="currentArtifact.name" v-model="currentArtifact.name"
:disabled="editing" :disabled="editing"
:rules="validations.nameRules"
clear-icon="clear" clear-icon="clear"
clearable clearable
label="配置名称" label="配置名称"
@ -155,7 +154,7 @@
</v-btn> </v-btn>
</template> </template>
<v-list dense> <v-list dense>
<v-list-item v-if="artifact.url" @click="copy(artifact.url)"> <v-list-item v-if="artifact.url" @click="copy(artifact)">
<v-list-item-title>复制</v-list-item-title> <v-list-item-title>复制</v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item @click="editArtifact(artifact)"> <v-list-item @click="editArtifact(artifact)">
@ -200,10 +199,6 @@ export default {
editing: null, editing: null,
formValid: false, formValid: false,
validations: { validations: {
nameRules: [
v => !!v || "订阅名称不能为空!",
v => /^[\w-_.]*$/.test(v) || "订阅名称只能包含英文字符、横杠、点和下划线!"
],
required: [ required: [
v => !!v || "不能为空!" v => !!v || "不能为空!"
] ]
@ -299,9 +294,11 @@ export default {
this.editing = false; this.editing = false;
}, },
copy(url) { copy(artifact) {
this.$clipboard(url); if (artifact.url) {
this.$store.commit("SET_SUCCESS_MESSAGE", "成功复制配置链接"); this.$clipboard(artifact.url + (isPlainName(artifact.name) ? '' : `#${artifact.name}`));
this.$store.commit("SET_SUCCESS_MESSAGE", "成功复制配置链接");
}
}, },
preview(name) { preview(name) {
@ -360,6 +357,10 @@ export default {
} }
} }
} }
function isPlainName(name) {
return /^[\w-_]*$/.test(name);
}
</script> </script>
<style scoped> <style scoped>

View File

@ -3,7 +3,7 @@
<v-card class="mb-4"> <v-card class="mb-4">
<v-subheader>订阅配置</v-subheader> <v-subheader>订阅配置</v-subheader>
<v-form v-model="formState.basicValid" class="pl-4 pr-4 pb-0"> <v-form v-model="formState.basicValid" class="pl-4 pr-4 pb-0">
<v-text-field v-model="options.name" :rules="validations.nameRules" class="mt-2" clear-icon="clear" clearable <v-text-field v-model="options.name" class="mt-2" clear-icon="clear" clearable
label="订阅名称" placeholder="填入订阅名称,名称需唯一" required /> label="订阅名称" placeholder="填入订阅名称,名称需唯一" required />
<!--For Subscription--> <!--For Subscription-->
<v-radio-group v-if="!isCollection" v-model="options.source" class="mt-0 mb-0"> <v-radio-group v-if="!isCollection" v-model="options.source" class="mt-0 mb-0">
@ -20,7 +20,7 @@
<v-col></v-col> <v-col></v-col>
</v-row> </v-row>
</v-radio-group> </v-radio-group>
<v-textarea v-if="options.source !== 'local'" v-model="options.url" :rules="validations.urlRules" auto-grow <v-textarea v-if="!isCollection && options.source !== 'local'" v-model="options.url" :rules="validations.urlRules" auto-grow
class="mt-0" clear-icon="clear" clearable label="订阅链接" placeholder="填入机场原始订阅链接" required rows="2" /> class="mt-0" clear-icon="clear" clearable label="订阅链接" placeholder="填入机场原始订阅链接" required rows="2" />
<v-textarea v-if="options.source === 'local'" v-model="options.content" clear-icon="clear" clearable <v-textarea v-if="options.source === 'local'" v-model="options.content" clear-icon="clear" clearable
label="订阅内容" placeholder="填入原始订阅内容" autogrow rows="5" row-height="15" class="mt-0"> label="订阅内容" placeholder="填入原始订阅内容" autogrow rows="5" row-height="15" class="mt-0">
@ -300,10 +300,6 @@ export default {
imported: "", imported: "",
dialog: false, dialog: false,
validations: { validations: {
nameRules: [
v => !!v || "订阅名称不能为空!",
v => /^[\w-_]*$/.test(v) || "订阅名称只能包含英文字符、横杠和下划线!"
],
urlRules: [ urlRules: [
v => this.options.source === 'remote' && (!!v || "订阅链接不能为空!"), v => this.options.source === 'remote' && (!!v || "订阅链接不能为空!"),
v => this.options.source === 'remote' && (/^https?:\/\//.test(v) || "订阅链接不合法!") v => this.options.source === 'remote' && (/^https?:\/\//.test(v) || "订阅链接不合法!")
@ -335,7 +331,7 @@ export default {
}, },
created() { created() {
const name = this.$route.params.name; const name = decodeURIComponent(this.$route.params.name);
let source; let source;
if (this.isCollection) { if (this.isCollection) {
source = (typeof name === 'undefined' || name === 'UNTITLED') ? {} : this.$store.state.collections[name]; source = (typeof name === 'undefined' || name === 'UNTITLED') ? {} : this.$store.state.collections[name];

View File

@ -195,19 +195,7 @@ export default {
{ {
title: "删除", title: "删除",
action: "DELETE" action: "DELETE"
}, }
// {
// title: "",
// action: "DUPLICATE"
// }
// {
// title: "",
// action: "MOVE_UP"
// },
// {
// title: "",
// action: "MOVE_DOWN"
// }
] ]
} }
}, },
@ -236,27 +224,22 @@ export default {
console.log(`${action} --> ${sub.name}`); console.log(`${action} --> ${sub.name}`);
switch (action) { switch (action) {
case 'COPY': case 'COPY':
this.$clipboard(`${this.subscriptionBaseURL}/download/${sub.name}`); this.$clipboard(`${this.subscriptionBaseURL}/download/${encodeURIComponent(sub.name)}${isPlainName(sub.name) ? '' : '#' + sub.name}`);
this.$store.commit("SET_SUCCESS_MESSAGE", "成功复制订阅链接"); this.$store.commit("SET_SUCCESS_MESSAGE", "成功复制订阅链接");
break break
case 'EDIT': case 'EDIT':
this.$router.push(`/sub-edit/${sub.name}`); this.$router.push(`/sub-edit/${encodeURIComponent(sub.name)}`);
break break
case 'DELETE': case 'DELETE':
this.$store.dispatch("DELETE_SUBSCRIPTION", sub.name); this.$store.dispatch("DELETE_SUBSCRIPTION", sub.name);
break break
case 'MOVE_UP':
this.moveUpSubscription(sub.name);
break
case 'MOVE_DOWN':
break
} }
}, },
collectionMenu(action, collection) { collectionMenu(action, collection) {
console.log(`${action} --> ${collection.name}`); console.log(`${action} --> ${collection.name}`);
switch (action) { switch (action) {
case 'COPY': case 'COPY':
this.$clipboard(`${this.subscriptionBaseURL}/download/collection/${collection.name}`); this.$clipboard(`${this.subscriptionBaseURL}/download/collection/${encodeURIComponent(collection.name)}${isPlainName(collection.name) ? '' : '#' + collection.name}`);
this.$store.commit("SET_SUCCESS_MESSAGE", "成功复制订阅链接"); this.$store.commit("SET_SUCCESS_MESSAGE", "成功复制订阅链接");
break break
case 'EDIT': case 'EDIT':
@ -269,10 +252,10 @@ export default {
}, },
preview(item, type = 'sub') { preview(item, type = 'sub') {
if (type === 'sub') { if (type === 'sub') {
this.url = `${BACKEND_BASE}/download/${item.name}`; this.url = `${BACKEND_BASE}/download/${encodeURIComponent(item.name)}`;
this.sub = item.url; this.sub = item.url;
} else { } else {
this.url = `${BACKEND_BASE}/download/collection/${item.name}` this.url = `${BACKEND_BASE}/download/collection/${encodeURIComponent(item.name)}`
} }
this.showProxyList = true; this.showProxyList = true;
}, },
@ -295,6 +278,10 @@ export default {
} }
} }
} }
function isPlainName(name) {
return /^[\w-_]*$/.test(name);
}
</script> </script>
<style scoped> <style scoped>