diff --git a/CHANGELOG.md b/CHANGELOG.md index ac740f23c78a74ad02610a11bf6f0c9e41871329..6a53ed8f970b9d0d4c35c97788a7d320f7e61878 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Support pagination of local emoji packs and files - Add MRF Activity Expiration setting - Add ability to disable multi-factor authentication for a user +- Add ability to manually evict and ban URLs from the Pleroma MediaProxy cache +- Add Invalidation settings on MediaProxy tab - Ability to configure S3 settings on Upload tab - Show number of open reports in Sidebar Menu - Add confirmation message when deleting a user @@ -30,6 +32,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Remove ability to moderate users that don't have valid nickname - Displays both labels and description in the header of group of settiings - Ability to add custom values in Pleroma.Upload.Filter.Mogrify setting +- Change types of the following settings: ':groups', ':replace', ':federated_timeline_removal', ':reject', ':match_actor'. Update functions that parses and wraps settings data according to this change. +- Move rendering Crontab setting from a separate component to EditableKeyword component - Show only those MRF settings that have been enabled in MRF Policies setting - Move Auto Linker settings to Link Formatter Tab as its configuration was moved to :pleroma, Pleroma.Formatter diff --git a/src/api/mediaProxyCache.js b/src/api/mediaProxyCache.js new file mode 100644 index 0000000000000000000000000000000000000000..0822d984214f4df6c2c98003061c2eec318d8752 --- /dev/null +++ b/src/api/mediaProxyCache.js @@ -0,0 +1,34 @@ +import request from '@/utils/request' +import { getToken } from '@/utils/auth' +import { baseName } from './utils' + +export async function listBannedUrls(page, authHost, token) { + return await request({ + baseURL: baseName(authHost), + url: `/api/pleroma/admin/media_proxy_caches?page=${page}`, + method: 'get', + headers: authHeaders(token) + }) +} + +export async function purgeUrls(urls, ban, authHost, token) { + return await request({ + baseURL: baseName(authHost), + url: `/api/pleroma/admin/media_proxy_caches/purge`, + method: 'post', + headers: authHeaders(token), + data: { urls, ban } + }) +} + +export async function removeBannedUrls(urls, authHost, token) { + return await request({ + baseURL: baseName(authHost), + url: `/api/pleroma/admin/media_proxy_caches/delete`, + method: 'post', + headers: authHeaders(token), + data: { urls } + }) +} + +const authHeaders = (token) => token ? { 'Authorization': `Bearer ${getToken()}` } : {} diff --git a/src/lang/en.js b/src/lang/en.js index 3b644597981df9dc74b8a81a7b66eb5df2ad4351..478a5cc7767218ba7482ebac1964b7e1196b98b8 100644 --- a/src/lang/en.js +++ b/src/lang/en.js @@ -65,8 +65,11 @@ export default { externalLink: 'External Link', users: 'Users', reports: 'Reports', + invites: 'Invites', + statuses: 'Statuses', settings: 'Settings', moderationLog: 'Moderation Log', + mediaProxyCache: 'MediaProxy Cache', 'emoji-packs': 'Emoji packs' }, navbar: { @@ -89,6 +92,19 @@ export default { pleromaFELoginFailed: 'Failed to login via PleromaFE, please login with username/password', pleromaFELoginSucceed: 'Logged in via PleromaFE' }, + mediaProxyCache: { + mediaProxyCache: 'MediaProxy Cache', + ban: 'Ban', + url: 'URL', + evict: 'Evict', + evictedMessage: 'This URL was evicted', + actions: 'Actions', + remove: 'Remove from Cachex', + evictObjectsHeader: 'Evict object from the MediaProxy cache', + listBannedUrlsHeader: 'List of all banned MediaProxy URLs', + multipleInput: 'You can enter a single URL or several comma separated links', + removeSelected: 'Remove Selected' + }, documentation: { documentation: 'Documentation', github: 'Github Repository' diff --git a/src/router/index.js b/src/router/index.js index 40d4d7cd176441d38b1821633f2c23c8d1d2eb5f..b61e7b448e8546fedb0050d8f77bd4bc0db463d1 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -16,7 +16,7 @@ const settings = { path: 'index', component: () => import('@/views/settings/index'), name: 'Settings', - meta: { title: 'Settings', icon: 'settings', noCache: true } + meta: { title: 'settings', icon: 'settings', noCache: true } } ] } @@ -30,7 +30,7 @@ const statuses = { path: 'index', component: () => import('@/views/statuses/index'), name: 'Statuses', - meta: { title: 'Statuses', icon: 'form', noCache: true } + meta: { title: 'statuses', icon: 'form', noCache: true } } ] } @@ -44,7 +44,7 @@ const reports = { path: 'index', component: () => import('@/views/reports/index'), name: 'Reports', - meta: { title: 'Reports', icon: 'documentation', noCache: true } + meta: { title: 'reports', icon: 'documentation', noCache: true } } ] } @@ -58,7 +58,7 @@ const invites = { path: 'index', component: () => import('@/views/invites/index'), name: 'Invites', - meta: { title: 'Invites', icon: 'guide', noCache: true } + meta: { title: 'invites', icon: 'guide', noCache: true } } ] } @@ -72,7 +72,7 @@ const emojiPacks = { path: 'index', component: () => import('@/views/emojiPacks/index'), name: 'Emoji Packs', - meta: { title: 'Emoji Packs', icon: 'eye-open', noCache: true } + meta: { title: 'emoji-packs', icon: 'eye-open', noCache: true } } ] } @@ -91,6 +91,20 @@ const moderationLog = { ] } +const mediaProxyCacheDisabled = disabledFeatures.includes('media-proxy-cache') +const mediaProxyCache = { + path: '/media_proxy_cache', + component: Layout, + children: [ + { + path: 'index', + component: () => import('@/views/mediaProxyCache/index'), + name: 'MediaProxy Cache', + meta: { title: 'mediaProxyCache', icon: 'example', noCache: true } + } + ] +} + export const constantRouterMap = [ { path: '/redirect', @@ -159,6 +173,7 @@ export const asyncRouterMap = [ ...(invitesDisabled ? [] : [invites]), ...(emojiPacksDisabled ? [] : [emojiPacks]), ...(moderationLogDisabled ? [] : [moderationLog]), + ...(mediaProxyCacheDisabled ? [] : [mediaProxyCache]), ...(settingsDisabled ? [] : [settings]), { path: '/users/:id', diff --git a/src/store/index.js b/src/store/index.js index e2fcd651768e6391e2a3e6685fd69eb4660ed1a7..bd4a6e5b0776fd2828d379d34a140ca7963c078b 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -5,6 +5,7 @@ import emojiPacks from './modules/emojiPacks' import errorLog from './modules/errorLog' import getters from './getters' import invites from './modules/invites' +import mediaProxyCache from './modules/mediaProxyCache' import moderationLog from './modules/moderationLog' import peers from './modules/peers' import permission from './modules/permission' @@ -24,8 +25,9 @@ const store = new Vuex.Store({ app, errorLog, emojiPacks, - moderationLog, invites, + mediaProxyCache, + moderationLog, peers, permission, relays, diff --git a/src/store/modules/mediaProxyCache.js b/src/store/modules/mediaProxyCache.js new file mode 100644 index 0000000000000000000000000000000000000000..815c14e493659e7a7dbef7bd5082602306fed43a --- /dev/null +++ b/src/store/modules/mediaProxyCache.js @@ -0,0 +1,53 @@ +import { listBannedUrls, purgeUrls, removeBannedUrls } from '@/api/mediaProxyCache' +import { Message } from 'element-ui' +import i18n from '@/lang' + +const mediaProxyCache = { + state: { + bannedUrls: [], + bannedUrlsCount: 0, + currentPage: 1, + loading: false + }, + mutations: { + SET_BANNED_URLS: (state, urls) => { + state.bannedUrls = urls.map(el => { return { url: el } }) + }, + SET_BANNED_URLS_COUNT: (state, count) => { + state.bannedUrlsCount = count + }, + SET_LOADING: (state, status) => { + state.loading = status + }, + SET_PAGE: (state, page) => { + state.currentPage = page + } + }, + actions: { + async ListBannedUrls({ commit, getters }, page) { + commit('SET_LOADING', true) + const response = await listBannedUrls(page, getters.authHost, getters.token) + commit('SET_BANNED_URLS', response.data.urls) + // commit('SET_BANNED_URLS_COUNT', count) + commit('SET_PAGE', page) + commit('SET_LOADING', false) + }, + async PurgeUrls({ dispatch, getters, state }, { urls, ban }) { + await purgeUrls(urls, ban, getters.authHost, getters.token) + Message({ + message: i18n.t('mediaProxyCache.evictedMessage'), + type: 'success', + duration: 5 * 1000 + }) + if (ban) { + dispatch('ListBannedUrls', state.currentPage) + } + }, + async RemoveBannedUrls({ dispatch, getters, state }, urls) { + await removeBannedUrls(urls, getters.authHost, getters.token) + dispatch('ListBannedUrls', state.currentPage) + } + } +} + +export default mediaProxyCache diff --git a/src/store/modules/normalizers.js b/src/store/modules/normalizers.js index 349cebffefcde7030e5f8e56a27f14c8fea5244a..c7fc33e8fc56be9d7ea0a940d136369a46c79f43 100644 --- a/src/store/modules/normalizers.js +++ b/src/store/modules/normalizers.js @@ -71,18 +71,16 @@ export const parseTuples = (tuples, key) => { return [...acc, { [mascot.tuple[0]]: { ...mascot.tuple[1], id: `f${(~~(Math.random() * 1e8)).toString(16)}` }}] }, []) } else if (Array.isArray(item.tuple[1]) && - (item.tuple[0] === ':groups' || item.tuple[0] === ':replace' || item.tuple[0] === ':retries')) { - accum[item.tuple[0]] = item.tuple[1].reduce((acc, group) => { - return [...acc, { [group.tuple[0]]: { value: group.tuple[1], id: `f${(~~(Math.random() * 1e8)).toString(16)}` }}] - }, []) - } else if (item.tuple[0] === ':crontab') { - accum[item.tuple[0]] = item.tuple[1].reduce((acc, group) => { - return { ...acc, [group.tuple[1]]: group.tuple[0] } - }, {}) - } else if (item.tuple[0] === ':match_actor') { - accum[item.tuple[0]] = Object.keys(item.tuple[1]).reduce((acc, regex) => { - return [...acc, { [regex]: { value: item.tuple[1][regex], id: `f${(~~(Math.random() * 1e8)).toString(16)}` }}] - }, []) + (item.tuple[0] === ':groups' || item.tuple[0] === ':replace' || item.tuple[0] === ':retries' || item.tuple[0] === ':headers' || item.tuple[0] === ':crontab')) { + if (item.tuple[0] === ':crontab') { + accum[item.tuple[0]] = item.tuple[1].reduce((acc, group) => { + return [...acc, { [group.tuple[1]]: { value: group.tuple[0], id: `f${(~~(Math.random() * 1e8)).toString(16)}` }}] + }, []) + } else { + accum[item.tuple[0]] = item.tuple[1].reduce((acc, group) => { + return [...acc, { [group.tuple[0]]: { value: group.tuple[1], id: `f${(~~(Math.random() * 1e8)).toString(16)}` }}] + }, []) + } } else if (item.tuple[0] === ':icons') { accum[item.tuple[0]] = item.tuple[1].map(icon => { return Object.keys(icon).map(name => { @@ -103,7 +101,13 @@ export const parseTuples = (tuples, key) => { } else if (item.tuple[0] === ':ip') { accum[item.tuple[0]] = item.tuple[1].tuple.join('.') } else if (item.tuple[1] && typeof item.tuple[1] === 'object') { - accum[item.tuple[0]] = parseObject(item.tuple[1]) + if (item.tuple[0] === ':params' || item.tuple[0] === ':match_actor') { + accum[item.tuple[0]] = Object.keys(item.tuple[1]).reduce((acc, key) => { + return [...acc, { [key]: { value: item.tuple[1][key], id: `f${(~~(Math.random() * 1e8)).toString(16)}` }}] + }, []) + } else { + accum[item.tuple[0]] = parseObject(item.tuple[1]) + } } else { accum[item.tuple[0]] = item.tuple[1] } @@ -214,11 +218,11 @@ export const wrapUpdatedSettings = (group, settings, currentState) => { const wrapValues = (settings, currentState) => { return Object.keys(settings).map(setting => { const [type, value] = settings[setting] - if ( - type === 'keyword' || - type.includes('keyword') || - type.includes('tuple') && type.includes('list') || - setting === ':replace' + if (type === 'keyword' || + (Array.isArray(type) && ( + type.includes('keyword') || + (type.includes('tuple') && type.includes('list')) + )) ) { return { 'tuple': [setting, wrapValues(value, currentState)] } } else if (prependWithСolon(type, value)) { @@ -231,15 +235,16 @@ const wrapValues = (settings, currentState) => { return { 'tuple': [value, setting] } } else if (type === 'map') { const mapValue = Object.keys(value).reduce((acc, key) => { - acc[key] = setting === ':match_actor' ? value[key] : value[key][1] + acc[key] = value[key][1] + return acc + }, {}) + return { 'tuple': [setting, { ...currentState[setting], ...mapValue }] } + } else if (type.includes('map')) { + const mapValue = Object.keys(value).reduce((acc, key) => { + acc[key] = value[key][1] return acc }, {}) - const mapCurrentState = setting === ':match_actor' - ? currentState[setting].reduce((acc, element) => { - return { ...acc, ...{ [Object.keys(element)[0]]: Object.values(element)[0].value }} - }, {}) - : currentState[setting] - return { 'tuple': [setting, { ...mapCurrentState, ...mapValue }] } + return { 'tuple': [setting, mapValue] } } else if (setting === ':ip') { const ip = value.split('.').map(s => parseInt(s, 10)) return { 'tuple': [setting, { 'tuple': ip }] } diff --git a/src/views/mediaProxyCache/index.vue b/src/views/mediaProxyCache/index.vue new file mode 100644 index 0000000000000000000000000000000000000000..6c37082b4ea8064100fd4cfa80d57100cfd160ad --- /dev/null +++ b/src/views/mediaProxyCache/index.vue @@ -0,0 +1,157 @@ +<template> + <div class="media-proxy-cache-container"> + <div class="media-proxy-cache-header-container"> + <h1>{{ $t('mediaProxyCache.mediaProxyCache') }}</h1> + <reboot-button/> + </div> + <p class="media-proxy-cache-header">{{ $t('mediaProxyCache.evictObjectsHeader') }}</p> + <div class="url-input-container"> + <el-input + :placeholder="$t('mediaProxyCache.url')" + v-model="urls" + type="textarea" + autosize + clearable + class="url-input"/> + <el-checkbox v-model="ban">{{ $t('mediaProxyCache.ban') }}</el-checkbox> + <el-button class="evict-button" @click="evictURL">{{ $t('mediaProxyCache.evict') }}</el-button> + </div> + <span class="expl url-input-expl">{{ $t('mediaProxyCache.multipleInput') }}</span> + <p class="media-proxy-cache-header">{{ $t('mediaProxyCache.listBannedUrlsHeader') }}</p> + <el-table + v-loading="loading" + :data="bannedUrls" + class="banned-urls-table" + @selection-change="handleSelectionChange">> + <el-table-column + type="selection" + align="center" + width="55"/> + <el-table-column + :min-width="isDesktop ? 320 : 120" + prop="url"/> + <el-table-column> + <template slot="header"> + <el-button + :disabled="removeSelectedDisabled" + size="mini" + class="remove-url-button" + @click="removeSelected()">{{ $t('mediaProxyCache.removeSelected') }}</el-button> + </template> + <template slot-scope="scope"> + <el-button + size="mini" + class="remove-url-button" + @click="removeUrl(scope.row.url)">{{ $t('mediaProxyCache.remove') }}</el-button> + </template> + </el-table-column> + </el-table> + </div> +</template> + +<script> +import RebootButton from '@/components/RebootButton' + +export default { + name: 'MediaProxyCache', + components: { RebootButton }, + data() { + return { + urls: '', + ban: false, + selectedUrls: [] + } + }, + computed: { + bannedUrls() { + return this.$store.state.mediaProxyCache.bannedUrls + }, + isDesktop() { + return this.$store.state.app.device === 'desktop' + }, + loading() { + return this.$store.state.mediaProxyCache.loading + }, + removeSelectedDisabled() { + return this.selectedUrls.length === 0 + } + }, + mounted() { + this.$store.dispatch('GetNodeInfo') + this.$store.dispatch('NeedReboot') + this.$store.dispatch('ListBannedUrls', 1) + }, + methods: { + evictURL() { + const urls = this.urls.split(',').map(url => url.trim()).filter(el => el.length > 0) + this.$store.dispatch('PurgeUrls', { urls, ban: this.ban }) + this.urls = '' + }, + handleSelectionChange(value) { + this.$data.selectedUrls = value + }, + removeSelected() { + const urls = this.selectedUrls.map(el => el.url) + this.$store.dispatch('RemoveBannedUrls', urls) + this.selectedUrls = [] + }, + removeUrl(url) { + this.$store.dispatch('RemoveBannedUrls', [url]) + } + } +} +</script> + +<style rel='stylesheet/scss' lang='scss' scoped> +h1 { + margin: 0; +} +.expl { + color: #666666; + font-size: 13px; + line-height: 22px; + margin: 5px 0 0 0; + overflow-wrap: break-word; + overflow: hidden; + text-overflow: ellipsis; +} +.banned-urls-table { + margin-top: 15px; + margin-bottom: 15px; +} +.evict-button { + margin-left: 15px; +} +.media-proxy-cache-header { + margin-left: 15px; + margin-top: 22px; + font-weight: 500; +} +.media-proxy-cache-header-container { + display: flex; + align-items: center; + justify-content: space-between; + margin: 10px 15px; +} +.remove-url-button { + width: 150px; +} +.url-input { + margin-right: 15px; +} +.url-input-container { + display: flex; + align-items: baseline; + margin: 15px 15px 5px 15px; +} +.url-input-expl { + margin-left: 15px; +} + +@media only screen and (max-width:480px) { + .url-input { + width: 100%; + margin-bottom: 5px; + } +} +</style> diff --git a/src/views/settings/components/Inputs.vue b/src/views/settings/components/Inputs.vue index 8bbd8121b9544ee954bac9d6ddd04eaaf06f7401..0639994e48fb937844270290a0748cd10f2fa78a 100644 --- a/src/views/settings/components/Inputs.vue +++ b/src/views/settings/components/Inputs.vue @@ -95,15 +95,14 @@ <el-input v-if="setting.type === 'atom'" :value="inputValue" - :placeholder="setting.suggestions[0] ? setting.suggestions[0].substr(1) : ''" + :placeholder="setting.suggestions && setting.suggestions[0] ? setting.suggestions[0].substr(1) : ''" :data-search="setting.key || setting.group" class="input" @input="update($event, settingGroup.group, settingGroup.key, settingParent, setting.key, setting.type, nested)"> <template slot="prepend">:</template> </el-input> <!-- special inputs --> - <crontab-input v-if="setting.key === ':crontab'" :data="data[setting.key]" :setting-group="settingGroup" :setting="setting"/> - <editable-keyword-input v-if="editableKeyword(setting.key, setting.type)" :data="keywordData" :setting-group="settingGroup" :setting="setting"/> + <editable-keyword-input v-if="editableKeyword(setting.key, setting.type)" :data="keywordData" :setting-group="settingGroup" :setting="setting" :parents="settingParent"/> <icons-input v-if="setting.key === ':icons'" :data="iconsData" :setting-group="settingGroup" :setting="setting"/> <link-formatter-input v-if="booleanCombinedInput" :data="data" :setting-group="settingGroup" :setting="setting"/> <mascots-input v-if="setting.key === ':mascots'" :data="keywordData" :setting-group="settingGroup" :setting="setting"/> @@ -129,7 +128,6 @@ <script> import i18n from '@/lang' import { - CrontabInput, EditableKeywordInput, IconsInput, ImageUploadInput, @@ -148,7 +146,6 @@ import marked from 'marked' export default { name: 'Inputs', components: { - CrontabInput, EditableKeywordInput, IconsInput, ImageUploadInput, @@ -225,7 +222,7 @@ export default { this.$store.state.settings.db[group][key].includes(this.setting.key) }, iconsData() { - return Array.isArray(this.data[':icons']) ? this.data[':icons'] : [] + return Array.isArray(this.data) ? this.data : [] }, inputValue() { if ([':esshd', ':cors_plug', ':quack', ':tesla', ':swoosh'].includes(this.settingGroup.group) && @@ -267,6 +264,10 @@ export default { } }, keywordData() { + if (this.settingParent.length > 0 || + (Array.isArray(this.setting.type) && this.setting.type.includes('tuple') && this.setting.type.includes('list'))) { + return Array.isArray(this.data[this.setting.key]) ? this.data[this.setting.key] : [] + } return Array.isArray(this.data) ? this.data : [] }, reducedSelects() { @@ -296,10 +297,14 @@ export default { }, methods: { editableKeyword(key, type) { - return key === ':replace' || - type === 'map' || - (Array.isArray(type) && type.includes('keyword') && type.includes('integer')) || - (Array.isArray(type) && type.includes('keyword') && type.findIndex(el => el.includes('list') && el.includes('string')) !== -1) + return Array.isArray(type) && ( + (type.includes('map') && type.includes('string')) || + (type.includes('map') && type.findIndex(el => el.includes('list') && el.includes('string')) !== -1) || + (type.includes('keyword') && type.includes('integer')) || + (type.includes('keyword') && type.includes('string')) || + (type.includes('tuple') && type.includes('list')) || + (type.includes('keyword') && type.findIndex(el => el.includes('list') && el.includes('string')) !== -1) + ) }, getFormattedDescription(desc) { return marked(desc) @@ -346,7 +351,7 @@ export default { type.includes('module') || (type.includes('list') && type.includes('string')) || (type.includes('list') && type.includes('atom')) || - (type.includes('regex') && type.includes('string')) + (!type.includes('keyword') && type.includes('regex') && type.includes('string')) ) }, renderSingleSelect(type) { diff --git a/src/views/settings/components/MediaProxy.vue b/src/views/settings/components/MediaProxy.vue index 11c9d7931746492f5b002a463d65ca63b9c733fe..d3fa619d75d7e9465b24ec538503ee168110bfc3 100644 --- a/src/views/settings/components/MediaProxy.vue +++ b/src/views/settings/components/MediaProxy.vue @@ -3,6 +3,14 @@ <el-form v-if="!loading" :model="mediaProxyData" :label-position="labelPosition" :label-width="labelWidth"> <setting :setting-group="mediaProxy" :data="mediaProxyData"/> </el-form> + <el-divider v-if="mediaProxy" class="divider thick-line"/> + <el-form v-if="!loading" :model="httpInvalidationData" :label-position="labelPosition" :label-width="labelWidth"> + <setting :setting-group="httpInvalidation" :data="httpInvalidationData"/> + </el-form> + <el-divider v-if="httpInvalidation" class="divider thick-line"/> + <el-form v-if="!loading" :model="scriptInvalidationData" :label-position="labelPosition" :label-width="labelWidth"> + <setting :setting-group="scriptInvalidation" :data="scriptInvalidationData"/> + </el-form> <div class="submit-button-container"> <el-button class="submit-button" type="primary" @click="onSubmit">Submit</el-button> </div> @@ -22,6 +30,12 @@ export default { ...mapGetters([ 'settings' ]), + httpInvalidation() { + return this.settings.description.find(setting => setting.key === 'Pleroma.Web.MediaProxy.Invalidation.Http') + }, + httpInvalidationData() { + return _.get(this.settings.settings, [':pleroma', 'Pleroma.Web.MediaProxy.Invalidation.Http']) || {} + }, isMobile() { return this.$store.state.app.device === 'mobile' }, @@ -51,6 +65,12 @@ export default { }, mediaProxyData() { return _.get(this.settings.settings, [':pleroma', ':media_proxy']) || {} + }, + scriptInvalidation() { + return this.settings.description.find(setting => setting.key === 'Pleroma.Web.MediaProxy.Invalidation.Script') + }, + scriptInvalidationData() { + return _.get(this.settings.settings, [':pleroma', 'Pleroma.Web.MediaProxy.Invalidation.Script']) || {} } }, methods: { diff --git a/src/views/settings/components/Setting.vue b/src/views/settings/components/Setting.vue index 1656db6142af04e21267916868430aa9d7734b8b..7593fb3d2c417065afa1d3510d522959bc21ffe5 100644 --- a/src/views/settings/components/Setting.vue +++ b/src/views/settings/components/Setting.vue @@ -122,7 +122,7 @@ export default { return type === 'keyword' || type === 'map' || type.includes('keyword') || - key === ':replace' + type.includes('map') }, divideSetting(key) { return [':sslopts', ':tlsopts', ':adapter', ':poll_limits', ':queues', ':styling', ':invalidation', ':multi_factor_authentication'].includes(key) diff --git a/src/views/settings/components/inputComponents/CrontabInput.vue b/src/views/settings/components/inputComponents/CrontabInput.vue deleted file mode 100644 index 89a1491549784d8c0295c047d1ac3e21e774089d..0000000000000000000000000000000000000000 --- a/src/views/settings/components/inputComponents/CrontabInput.vue +++ /dev/null @@ -1,86 +0,0 @@ -<template> - <el-form :label-width="labelWidth" :label-position="isMobile ? 'top' : 'right'" class="crontab"> - <el-form-item v-for="worker in workers" :key="worker" :label="worker" :data-search="setting.key" class="crontab-container"> - <el-input - :value="data[worker]" - :placeholder="getSuggestion(worker) || null" - class="input setting-input" - @input="update($event, worker)"/> - </el-form-item> - </el-form> -</template> - -<script> -export default { - name: 'CrontabInput', - props: { - data: { - type: Object, - default: function() { - return {} - } - }, - setting: { - type: Object, - default: function() { - return {} - } - }, - settingGroup: { - type: Object, - default: function() { - return {} - } - } - }, - computed: { - isDesktop() { - return this.$store.state.app.device === 'desktop' - }, - isMobile() { - return this.$store.state.app.device === 'mobile' - }, - isTablet() { - return this.$store.state.app.device === 'tablet' - }, - labelWidth() { - if (this.isMobile) { - return '100%' - } else { - return '380px' - } - }, - workers() { - return this.setting.suggestions.map(worker => worker[1]) - } - }, - methods: { - getSuggestion(worker) { - return this.setting.suggestions.find(suggestion => suggestion[1] === worker)[0] - }, - update(value, worker) { - const currentValue = this.$store.state.settings.settings[this.settingGroup.group][this.settingGroup.key][this.setting.key] - const updatedValue = { ...currentValue, [worker]: value } - const updatedValueWithType = Object.keys(currentValue).reduce((acc, key) => { - if (key === worker) { - return { ...acc, [key]: ['reversed_tuple', value] } - } else { - return { ...acc, [key]: ['reversed_tuple', currentValue[key]] } - } - }, {}) - - this.$store.dispatch('UpdateSettings', - { group: this.settingGroup.group, key: this.settingGroup.key, input: this.setting.key, value: updatedValueWithType, type: this.setting.type } - ) - this.$store.dispatch('UpdateState', - { group: this.settingGroup.group, key: this.settingGroup.key, input: this.setting.key, value: updatedValue } - ) - } - } -} -</script> - -<style rel='stylesheet/scss' lang='scss'> -@import '../../styles/main'; -@include settings -</style> diff --git a/src/views/settings/components/inputComponents/EditableKeywordInput.vue b/src/views/settings/components/inputComponents/EditableKeywordInput.vue index 009b2033bcf750d296a6471e98466e085b89c318..727ff0da61c11f874bf7bfc657d7c17af0e86bf0 100644 --- a/src/views/settings/components/inputComponents/EditableKeywordInput.vue +++ b/src/views/settings/components/inputComponents/EditableKeywordInput.vue @@ -1,22 +1,31 @@ <template> <div class="editable-keyword-container"> - <div v-if="setting.key === ':replace'" :data-search="setting.key || setting.group"> + <div v-if="setting.key === ':crontab'" :data-search="setting.key" class="crontab"> + <el-form-item v-for="worker in data" :key="getId(worker)" :label="getCrontabWorkerLabel(worker)" class="crontab-container"> + <el-input + :value="getValue(worker)" + :placeholder="getSuggestion(worker) || null" + class="input setting-input" + @input="updateCrontab($event, 'value', worker)"/> + </el-form-item> + </div> + <div v-else-if="editableKeywordWithInteger" :data-search="setting.key || setting.group"> <div v-for="element in data" :key="getId(element)" class="setting-input"> - <el-input :value="getKey(element)" placeholder="pattern" class="name-input" @input="parseEditableKeyword($event, 'key', element)"/> : - <el-input :value="getValue(element)" placeholder="replacement" class="value-input" @input="parseEditableKeyword($event, 'value', element)"/> + <el-input :value="getKey(element)" placeholder="key" class="name-input" @input="parseEditableKeyword($event, 'key', element)"/> : + <el-input-number :value="getValue(element)" :min="0" size="large" class="value-input" @change="parseEditableKeyword($event, 'value', element)"/> <el-button :size="isDesktop ? 'medium' : 'mini'" class="icon-minus-button" icon="el-icon-minus" circle @click="deleteEditableKeywordRow(element)"/> </div> <el-button :size="isDesktop ? 'medium' : 'mini'" icon="el-icon-plus" circle @click="addRowToEditableKeyword"/> </div> - <div v-else-if="editableKeywordWithInteger" :data-search="setting.key || setting.group"> + <div v-else-if="editableKeywordWithString" :data-search="setting.key || setting.group"> <div v-for="element in data" :key="getId(element)" class="setting-input"> - <el-input :value="getKey(element)" placeholder="key" class="name-input" @input="parseEditableKeyword($event, 'key', element)"/> : - <el-input-number :value="getValue(element)" :min="0" size="large" class="value-input" @change="parseEditableKeyword($event, 'value', element)"/> + <el-input :value="getKey(element)" :placeholder="keyPlaceholder" class="name-input" @input="parseEditableKeyword($event, 'key', element)"/> : + <el-input :value="getValue(element)" :placeholder="valuePlaceholder" class="value-input" @input="parseEditableKeyword($event, 'value', element)"/> <el-button :size="isDesktop ? 'medium' : 'mini'" class="icon-minus-button" icon="el-icon-minus" circle @click="deleteEditableKeywordRow(element)"/> </div> <el-button :size="isDesktop ? 'medium' : 'mini'" icon="el-icon-plus" circle @click="addRowToEditableKeyword"/> </div> - <div v-else :data-search="setting.key || setting.group"> + <div v-else-if="editableKeywordWithSelect" :data-search="setting.key || setting.group"> <div v-for="element in data" :key="getId(element)" class="setting-input"> <el-input :value="getKey(element)" placeholder="key" class="name-input" @input="parseEditableKeyword($event, 'key', element)"/> : <el-select :value="getValue(element)" multiple filterable allow-create class="value-input" @change="parseEditableKeyword($event, 'value', element)"/> @@ -28,6 +37,8 @@ </template> <script> +import { processNested } from '@/store/modules/normalizers' + export default { name: 'EditableKeywordInput', props: { @@ -37,6 +48,13 @@ export default { return {} } }, + parents: { + type: Array, + default: function() { + return [] + }, + required: false + }, setting: { type: Object, default: function() { @@ -52,10 +70,33 @@ export default { }, computed: { editableKeywordWithInteger() { - return Array.isArray(this.setting.type) && this.setting.type.includes('keyword') && this.setting.type.includes('integer') + return this.setting.type.includes('keyword') && this.setting.type.includes('integer') + }, + editableKeywordWithSelect() { + return (this.setting.type.includes('map') && this.setting.type.findIndex(el => el.includes('list') && el.includes('string')) !== -1) || + (this.setting.type.includes('keyword') && this.setting.type.findIndex(el => el.includes('list') && el.includes('string')) !== -1) + }, + editableKeywordWithString() { + return this.setting.key !== ':crontab' && ( + (this.setting.type.includes('keyword') && this.setting.type.includes('string')) || + (this.setting.type.includes('tuple') && this.setting.type.includes('list')) || + (this.setting.type.includes('map') && this.setting.type.includes('string')) + ) }, isDesktop() { return this.$store.state.app.device === 'desktop' + }, + keyPlaceholder() { + return this.setting.key === ':replace' ? 'pattern' : 'key' + }, + settings() { + return this.$store.state.settings.settings + }, + updatedSettings() { + return this.$store.state.settings.updatedSettings + }, + valuePlaceholder() { + return this.setting.key === ':replace' ? 'replacement' : 'value' } }, methods: { @@ -71,6 +112,10 @@ export default { generateID() { return `f${(~~(Math.random() * 1e8)).toString(16)}` }, + getCrontabWorkerLabel(worker) { + const workerKey = this.getKey(worker) + return workerKey.includes('Pleroma.Workers.Cron.') ? workerKey.replace('Pleroma.Workers.Cron.', '') : workerKey + }, getKey(element) { return Object.keys(element)[0] }, @@ -78,6 +123,9 @@ export default { const { id } = Object.values(element)[0] return id }, + getSuggestion(worker) { + return this.setting.suggestions.find(suggestion => suggestion[1] === this.getKey(worker))[0] + }, getValue(element) { const { value } = Object.values(element)[0] return value @@ -95,10 +143,40 @@ export default { this.updateSetting(updatedValue, this.settingGroup.group, this.settingGroup.key, this.setting.key, this.setting.type) }, + updateCrontab(value, inputType, worker) { + const updatedId = this.getId(worker) + const updatedValue = this.data.map((worker, index) => { + if (Object.values(worker)[0].id === updatedId) { + return { [Object.keys(worker)[0]]: { ...Object.values(this.data[index])[0], value }} + } + return worker + }) + const updatedValueWithType = updatedValue.reduce((acc, worker) => { + return { ...acc, [Object.keys(worker)[0]]: ['reversed_tuple', Object.values(worker)[0].value] } + }, {}) + + this.$store.dispatch('UpdateSettings', + { group: this.settingGroup.group, key: this.settingGroup.key, input: this.setting.key, value: updatedValueWithType, type: this.setting.type } + ) + this.$store.dispatch('UpdateState', + { group: this.settingGroup.group, key: this.settingGroup.key, input: this.setting.key, value: updatedValue } + ) + }, updateSetting(value, group, key, input, type) { - const updatedSettings = this.wrapUpdatedSettings(value, input, type) - this.$store.dispatch('UpdateSettings', { group, key, input, value: updatedSettings, type }) - this.$store.dispatch('UpdateState', { group, key, input, value }) + const wrappedSettings = this.wrapUpdatedSettings(value, input, type) + + if (this.parents.length > 0) { + const { valueForState, + valueForUpdatedSettings, + setting } = processNested(value, wrappedSettings, group, key, this.parents.reverse(), this.settings, this.updatedSettings) + this.$store.dispatch('UpdateSettings', + { group, key, input: setting.key, value: valueForUpdatedSettings, type: setting.type }) + this.$store.dispatch('UpdateState', + { group, key, input: setting.key, value: valueForState }) + } else { + this.$store.dispatch('UpdateSettings', { group, key, input, value: wrappedSettings, type }) + this.$store.dispatch('UpdateState', { group, key, input, value }) + } }, wrapUpdatedSettings(value, input, type) { return type === 'map' diff --git a/src/views/settings/components/inputComponents/RateLimitInput.vue b/src/views/settings/components/inputComponents/RateLimitInput.vue index 431ae83b48d7efcc8e1c54aa07ba67787c8c35b4..60668369a6aa35cfa9579bc3c5655011efd194f6 100644 --- a/src/views/settings/components/inputComponents/RateLimitInput.vue +++ b/src/views/settings/components/inputComponents/RateLimitInput.vue @@ -1,14 +1,16 @@ <template> <div :data-search="setting.key || setting.group" class="rate-limit-container"> <div v-if="!rateLimitAuthUsers"> - <el-input + <el-input-number :value="rateLimitAllUsers[0]" + :controls="false" placeholder="scale" class="scale-input" @input="parseRateLimiter($event, setting.key, 'scale', 'oneLimit', rateLimitAllUsers)"/> <span>:</span> - <el-input + <el-input-number :value="rateLimitAllUsers[1]" + :controls="false" placeholder="limit" class="limit-input" @input="parseRateLimiter($event, setting.key, 'limit', 'oneLimit', rateLimitAllUsers)"/> @@ -25,16 +27,18 @@ </span> </div> <div class="rate-limit-content"> - <el-input + <el-input-number :value="rateLimitUnauthUsers[0]" + :controls="false" placeholder="scale" class="scale-input" @input="parseRateLimiter( $event, setting.key, 'scale', 'unauthUsersLimit', [rateLimitUnauthUsers, rateLimitAuthUsers] )"/> <span>:</span> - <el-input + <el-input-number :value="rateLimitUnauthUsers[1]" + :controls="false" placeholder="limit" class="limit-input" @input="parseRateLimiter( @@ -49,14 +53,16 @@ </span> </div> <div class="rate-limit-content"> - <el-input + <el-input-number :value="rateLimitAuthUsers[0]" + :controls="false" placeholder="scale" class="scale-input" @input="parseRateLimiter($event, setting.key, 'scale', 'authUserslimit', [rateLimitUnauthUsers, rateLimitAuthUsers])"/> <span>:</span> - <el-input + <el-input-number :value="rateLimitAuthUsers[1]" + :controls="false" placeholder="limit" class="limit-input" @input="parseRateLimiter($event, setting.key, 'limit', 'authUserslimit', [rateLimitUnauthUsers, rateLimitAuthUsers])"/> diff --git a/src/views/settings/components/inputComponents/index.js b/src/views/settings/components/inputComponents/index.js index c7ac42bfaa2c618e8e32a1278f1b26f521ff46c9..0ef58841b87988f5b1edf3d10d687b23edac1092 100644 --- a/src/views/settings/components/inputComponents/index.js +++ b/src/views/settings/components/inputComponents/index.js @@ -1,5 +1,4 @@ export { default as EditableKeywordInput } from './EditableKeywordInput' -export { default as CrontabInput } from './CrontabInput' export { default as IconsInput } from './IconsInput' export { default as ImageUploadInput } from './ImageUploadInput' export { default as LinkFormatterInput } from './LinkFormatterInput' diff --git a/src/views/settings/components/tabs.js b/src/views/settings/components/tabs.js index bf3c752f6834abc3d1cc8f7fa5e1424badd8202c..394f0670998c6510ca810ce68a3fa3a4f0fa82bf 100644 --- a/src/views/settings/components/tabs.js +++ b/src/views/settings/components/tabs.js @@ -1,82 +1,84 @@ -export const tabs = { - 'activity-pub': { - label: 'settings.activityPub', - settings: [':activitypub', ':user'] - }, - 'authentication': { - label: 'settings.auth', - settings: [':auth', ':ldap', ':oauth2', 'Pleroma.Web.Auth.Authenticator'] - }, - 'esshd': { - label: 'settings.esshd', - settings: [':esshd'] - }, - 'captcha': { - label: 'settings.captcha', - settings: ['Pleroma.Captcha', 'Pleroma.Captcha.Kocaptcha'] - }, - 'frontend': { - label: 'settings.frontend', - settings: [':assets', ':chat', ':emoji', ':frontend_configurations', ':markup', ':static_fe'] - }, - 'gopher': { - label: 'settings.gopher', - settings: [':gopher'] - }, - 'http': { - label: 'settings.http', - settings: [':cors_plug', ':http', ':http_security', ':web_cache_ttl'] - }, - 'instance': { - label: 'settings.instance', - settings: [':admin_token', ':instance', ':manifest', 'Pleroma.User', 'Pleroma.ScheduledActivity', ':uri_schemes', ':feed', ':streamer'] - }, - 'job-queue': { - label: 'settings.jobQueue', - settings: ['Pleroma.ActivityExpiration', 'Oban', ':workers'] - }, - 'link-formatter': { - label: 'settings.linkFormatter', - settings: ['Pleroma.Formatter'] - }, - 'logger': { - label: 'settings.logger', - settings: [':console', ':ex_syslogger', ':quack', ':logger'] - }, - 'mailer': { - label: 'settings.mailer', - settings: [':email_notifications', 'Pleroma.Emails.Mailer', 'Pleroma.Emails.UserEmail', ':swoosh', 'Pleroma.Emails.NewUsersDigestEmail'] - }, - 'media-proxy': { - label: 'settings.mediaProxy', - settings: [':media_proxy'] - }, - 'metadata': { - label: 'settings.metadata', - settings: ['Pleroma.Web.Metadata', ':rich_media'] - }, - 'mrf': { - label: 'settings.mrf', - settings: [':mrf_simple', ':mrf_rejectnonpublic', ':mrf_hellthread', ':mrf_keyword', ':mrf_subchain', ':mrf_mention', ':mrf_normalize_markup', ':mrf_vocabulary', ':mrf_object_age', ':modules'] - }, - 'rate-limiters': { - label: 'settings.rateLimiters', - settings: [':rate_limit'] - }, - 'relays': { - label: 'settings.relays', - settings: ['relays'] - }, - 'web-push': { - label: 'settings.webPush', - settings: [':vapid_details'] - }, - 'upload': { - label: 'settings.upload', - settings: ['Pleroma.Upload.Filter.AnonymizeFilename', 'Pleroma.Upload.Filter.Mogrify', 'Pleroma.Uploaders.S3', 'Pleroma.Uploaders.Local', 'Pleroma.Upload'] - }, - 'other': { - label: 'settings.other', - settings: [':mime', 'Pleroma.Plugs.RemoteIp'] +export const tabs = description => { + return { + 'activity-pub': { + label: 'settings.activityPub', + settings: [':activitypub', ':user'] + }, + 'authentication': { + label: 'settings.auth', + settings: [':auth', ':ldap', ':oauth2', 'Pleroma.Web.Auth.Authenticator'] + }, + 'auto-linker': { + label: 'settings.autoLinker', + settings: [':opts'] + }, + 'esshd': { + label: 'settings.esshd', + settings: [':esshd'] + }, + 'captcha': { + label: 'settings.captcha', + settings: ['Pleroma.Captcha', 'Pleroma.Captcha.Kocaptcha'] + }, + 'frontend': { + label: 'settings.frontend', + settings: [':assets', ':chat', ':emoji', ':frontend_configurations', ':markup', ':static_fe'] + }, + 'gopher': { + label: 'settings.gopher', + settings: [':gopher'] + }, + 'http': { + label: 'settings.http', + settings: [':cors_plug', ':http', ':http_security', ':web_cache_ttl'] + }, + 'instance': { + label: 'settings.instance', + settings: [':admin_token', ':instance', ':manifest', 'Pleroma.User', 'Pleroma.ScheduledActivity', ':uri_schemes', ':feed', ':streamer'] + }, + 'job-queue': { + label: 'settings.jobQueue', + settings: ['Pleroma.ActivityExpiration', 'Oban', ':workers'] + }, + 'logger': { + label: 'settings.logger', + settings: [':console', ':ex_syslogger', ':quack', ':logger'] + }, + 'mailer': { + label: 'settings.mailer', + settings: [':email_notifications', 'Pleroma.Emails.Mailer', 'Pleroma.Emails.UserEmail', ':swoosh', 'Pleroma.Emails.NewUsersDigestEmail'] + }, + 'media-proxy': { + label: 'settings.mediaProxy', + settings: [':media_proxy', 'Pleroma.Web.MediaProxy.Invalidation.Http', 'Pleroma.Web.MediaProxy.Invalidation.Script'] + }, + 'metadata': { + label: 'settings.metadata', + settings: ['Pleroma.Web.Metadata', ':rich_media'] + }, + 'mrf': { + label: 'settings.mrf', + settings: description.filter(el => el.tab === 'mrf').map(setting => setting.key) + }, + 'rate-limiters': { + label: 'settings.rateLimiters', + settings: [':rate_limit'] + }, + 'relays': { + label: 'settings.relays', + settings: ['relays'] + }, + 'web-push': { + label: 'settings.webPush', + settings: [':vapid_details'] + }, + 'upload': { + label: 'settings.upload', + settings: ['Pleroma.Upload.Filter.AnonymizeFilename', 'Pleroma.Upload.Filter.Mogrify', 'Pleroma.Uploaders.S3', 'Pleroma.Uploaders.Local', 'Pleroma.Upload', ':s3'] + }, + 'other': { + label: 'settings.other', + settings: [':mime', 'Pleroma.Plugs.RemoteIp'] + } } } diff --git a/src/views/settings/index.vue b/src/views/settings/index.vue index 45f20324ec0f2fe73dcba56e84e37b6842d8e7a8..08f0cb64f20abdafac4fce6bfd70c66affcdb303 100644 --- a/src/views/settings/index.vue +++ b/src/views/settings/index.vue @@ -200,7 +200,7 @@ export default { return this.$store.state.settings.searchData }, tabs() { - return tabs + return tabs(this.$store.state.settings.description) } }, mounted: function() { diff --git a/test/modules/normalizers/parseTuples.test.js b/test/modules/normalizers/parseTuples.test.js index 70192e76f00c5b876a96072976bd109bc1d51827..9cbe9d2fea0b390537ac26946c95489bd8f2dca4 100644 --- a/test/modules/normalizers/parseTuples.test.js +++ b/test/modules/normalizers/parseTuples.test.js @@ -198,6 +198,30 @@ describe('Parse tuples', () => { expect(_.isEqual(expectedResult, result)).toBeTruthy() }) + it('parses crontab setting', () => { + const tuples = [{ tuple: [':crontab', [ + { tuple: ['0 0 * * *', 'Pleroma.Workers.Cron.ClearOauthTokenWorker'] }, + { tuple: ['0 * * * *', 'Pleroma.Workers.Cron.StatsWorker'] }, + { tuple: ['* * * * *', 'Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker']} + ]]}] + const expectedResult = { ':crontab': [ + { 'Pleroma.Workers.Cron.ClearOauthTokenWorker': { value: '0 0 * * *'}}, + { 'Pleroma.Workers.Cron.StatsWorker': { value: '0 * * * *'}}, + { 'Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker': { value: '* * * * *'}} + ]} + + const parsed = parseTuples(tuples, 'Oban') + + expect(typeof parsed).toBe('object') + expect(':crontab' in parsed).toBeTruthy() + const result = { ...parsed, ':crontab': parsed[':crontab'].map(el => { + const key = Object.keys(el)[0] + const { id, ...rest } = el[key] + return { [key]: rest } + })} + expect(_.isEqual(expectedResult, result)).toBeTruthy() + }) + it('parses match_actor setting in mrf_subchain group', () => { const tuples = [{ tuple: [":match_actor", { '~r/https:\/\/example.com/s': ["Elixir.Pleroma.Web.ActivityPub.MRF.DropPolicy"]}]}] @@ -216,6 +240,26 @@ describe('Parse tuples', () => { expect(_.isEqual(expectedResult, result)).toBeTruthy() }) + it('parses options setting in MediaProxy.Invalidation.Http group', () => { + const tuples = [{ tuple: [":options", [{ tuple: [":params", { xxx: "zzz", aaa: "bbb" }]}]]}] + const expectedResult = { ':options': { ':params': + [ { xxx: { value: 'zzz' }}, { aaa: { value: 'bbb' }}] + }} + + const parsed = parseTuples(tuples, 'Pleroma.Web.MediaProxy.Invalidation.Http') + + expect(typeof parsed).toBe('object') + expect(':options' in parsed).toBeTruthy() + + const idRemoved = parsed[':options'][':params'].map(el => { + const key = Object.keys(el)[0] + const { id, ...rest } = el[key] + return { [key]: rest } + }) + parsed[':options'][':params'] = idRemoved + expect(_.isEqual(expectedResult, parsed)).toBeTruthy() + }) + it('parses proxy_url', () => { const proxyUrlNull = [{ tuple: [":proxy_url", null] }] const proxyUrlTuple = [{ tuple: [":proxy_url", { tuple: [":socks5", ":localhost", 3090] }]}] diff --git a/test/modules/normalizers/wrapUpdatedSettings.test.js b/test/modules/normalizers/wrapUpdatedSettings.test.js index 6b360f85c8b90f495c2e943b4ca10346a354bc64..dedb5c3460c25a403de2563b6e61160b7503657b 100644 --- a/test/modules/normalizers/wrapUpdatedSettings.test.js +++ b/test/modules/normalizers/wrapUpdatedSettings.test.js @@ -130,7 +130,7 @@ describe('Wrap settings', () => { }] const settings2 = { ':emoji': { ':groups': [ - ['keyword', 'string', ['list', 'string']], + ['keyword', ['list', 'string']], { ':custom': [['list'], ['/emoji/*.png', '/emoji/**/*.png']], ':another_group': ['list', ['/custom_emoji/*.png']]} ]}} @@ -151,7 +151,7 @@ describe('Wrap settings', () => { it('wraps :replace setting', () => { const settings = { ':mrf_keyword': { ':replace': [ - [['tuple', 'string', 'string'], ['tuple', 'regex', 'string']], + ['list', 'tuple'], { 'pattern': ['list', 'replacement'], '/\w+/': ['list', 'test_replacement']} ]}} @@ -296,17 +296,23 @@ describe('Wrap settings', () => { }]}] }] - const settings3 = { ':mrf_subchain': { ':match_actor': ['map', { - '~r/https:\/\/example.com/s': ['Elixir.Pleroma.Web.ActivityPub.MRF.DropPolicy'], - '~r/https:\/\/test.com': ['Elixir.Pleroma.Web.ActivityPub.MRF.TestPolicy'] + expect(_.isEqual(result1, expectedResult1)).toBeTruthy() + expect(_.isEqual(result2, expectedResult2)).toBeTruthy() + }) + + it('wraps settings with type that includes map', () => { + const settings1 = { ':mrf_subchain': { ':match_actor': [['map', ['list', 'string']], { + '~r/https:\/\/example.com/s': ['list', ['Elixir.Pleroma.Web.ActivityPub.MRF.DropPolicy']], + '~r/https:\/\/test.com': ['list', ['Elixir.Pleroma.Web.ActivityPub.MRF.TestPolicy']] }]}} - const state3 = { ':pleroma': { ':mrf_subchain': { ':match_actor': [ - { '~r/https:\/\/example.com/s': ['Elixir.Pleroma.Web.ActivityPub.MRF.DropPolicy'] }, - { '~r/https:\/\/test.com': ['Elixir.Pleroma.Web.ActivityPub.MRF.TestPolicy'] } + const state1 = { ':pleroma': { ':mrf_subchain': { ':match_actor': [ + { '~r/https:\/\/example.com/s': { value: ['Elixir.Pleroma.Web.ActivityPub.MRF.DropPolicy'], id: '1234' }}, + { '~r/https:\/\/test.com': { value: ['Elixir.Pleroma.Web.ActivityPub.MRF.TestPolicy'], id: '5678' } } ] }}} - const result3 = wrapUpdatedSettings(':pleroma', settings3, state3) - const expectedResult3 = [{ + + const result1 = wrapUpdatedSettings(':pleroma', settings1, state1) + const expectedResult1 = [{ group: ':pleroma', key: ':mrf_subchain', value: [{ tuple: [':match_actor', { @@ -315,9 +321,24 @@ describe('Wrap settings', () => { }]}] }] + const settings2 = { 'Pleroma.Web.MediaProxy.Invalidation.Http': { + ':options': ['keyword', { ':params': [['map', 'string'], { aaa: ['list', 'bbb'], xxx: ['list', 'zzz'] }]}] + }} + const state2 = { ':pleroma': { 'Pleroma.Web.MediaProxy.Invalidation.Http': { + ':options': { ':params': [{ aaa: { value: 'bbb', id: '1' }, xxx: { value: 'zzz', id: '2' }}] } + }}} + + const result2 = wrapUpdatedSettings(':pleroma', settings2, state2) + const expectedResult2 = [{ + group: ':pleroma', + key: 'Pleroma.Web.MediaProxy.Invalidation.Http', + value: [{ tuple: [':options', [ + { tuple: [':params', { aaa: 'bbb', xxx: 'zzz' }]} + ]]}] + }] + expect(_.isEqual(result1, expectedResult1)).toBeTruthy() expect(_.isEqual(result2, expectedResult2)).toBeTruthy() - expect(_.isEqual(result3, expectedResult3)).toBeTruthy() }) it('wraps IP setting', () => { @@ -351,10 +372,10 @@ describe('Wrap settings', () => { it('wraps regular settings', () => { const settings = { ':http_security': { - ':report_uri': ["string", "https://test.com"], - ':ct_max_age': ["integer", 150000], - ':sts': ["boolean", true], - ':methods': [["list", "string"], ["POST", "PUT", "PATCH"]] + ':report_uri': ['string', 'https://test.com'], + ':ct_max_age': ['integer', 150000], + ':sts': ['boolean', true], + ':methods': [['list', 'string'], ['POST', 'PUT', 'PATCH']] }} const state = { ':pleroma': { ':http_security': {}}} const result = wrapUpdatedSettings(':pleroma', settings, state) @@ -362,10 +383,10 @@ describe('Wrap settings', () => { group: ':pleroma', key: ':http_security', value: [ - { tuple: [":report_uri", "https://test.com"] }, - { tuple: [":ct_max_age", 150000] }, - { tuple: [":sts", true] }, - { tuple: [":methods", ["POST", "PUT", "PATCH"]] } + { tuple: [':report_uri', 'https://test.com'] }, + { tuple: [':ct_max_age', 150000] }, + { tuple: [':sts', true] }, + { tuple: [':methods', ['POST', 'PUT', 'PATCH']] } ] }]