From c5b7496c5d4bd6a0b1cff8111f431891620a4e19 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 5 Sep 2026 14:47:29 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9B=85=20Sync=202026-09-05=2014:47:29?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- filebrowser/files/config.yaml | 159 +----------------- filebrowser/files/filebrowser.init | 4 +- .../luci-static/resources/view/filebrowser.js | 27 +-- luci-app-filetransfer/Makefile | 5 - .../luasrc/controller/filetransfer.lua | 4 +- .../luasrc/model/cbi/filetransfer.lua | 15 +- luci-app-filetransfer/po/zh-cn | 1 + luci-app-filetransfer/po/zh_Hans | 1 - .../po/{zh-cn => zh_Hans}/filetransfer.po | 20 +-- .../root/etc/uci-defaults/luci-filetransfer | 6 - .../rpcd/acl.d/luci-app-filetransfer.json | 11 ++ luci-theme-footstrap/Makefile | 2 +- .../htdocs/luci-static/resources/fs-assets.js | 49 ++++-- .../htdocs/luci-static/resources/fs-axes.js | 12 +- .../etc/uci-defaults/30_luci-theme-footstrap | 21 ++- .../www/cgi-bin/luci-theme-footstrap-pattern | 43 +++++ .../themes/footstrap/partials/head.ut | 6 +- package_kernel_tcp-brutal/Makefile | 4 +- pgyvpn/Makefile | 59 ++++++- 19 files changed, 219 insertions(+), 230 deletions(-) create mode 120000 luci-app-filetransfer/po/zh-cn delete mode 120000 luci-app-filetransfer/po/zh_Hans rename luci-app-filetransfer/po/{zh-cn => zh_Hans}/filetransfer.po (65%) delete mode 100755 luci-app-filetransfer/root/etc/uci-defaults/luci-filetransfer create mode 100644 luci-app-filetransfer/root/usr/share/rpcd/acl.d/luci-app-filetransfer.json create mode 100755 luci-theme-footstrap/root/www/cgi-bin/luci-theme-footstrap-pattern diff --git a/filebrowser/files/config.yaml b/filebrowser/files/config.yaml index d5b46ba7e..b558ae19d 100644 --- a/filebrowser/files/config.yaml +++ b/filebrowser/files/config.yaml @@ -1,156 +1,13 @@ server: - minSearchLength: 3 # minimum length of search query to begin searching (default: 3) - disableUpdateCheck: false # disables backend update check service - numImageProcessors: 4 # number of concurrent image processing jobs used to create previews, default is number of cpu cores available. - socket: "" # socket to listen on - tlsKey: "" # path to TLS key - tlsCert: "" # path to TLS cert - disablePreviews: false # disable all previews thumbnails, simple icons will be used - disablePreviewResize: false # disable resizing of previews for faster loading over slow connections - disableTypeDetectionByHeader: false # disable type detection by header, useful if filesystem is slow. port: 8989 - baseURL: "/" - logging: - - levels: "" # separated list of log levels to enable. (eg. "info|warning|error|debug") - apiLevels: "" # separated list of log levels to enable for the API. (eg. "info|warning|error") - output: "stdout" # output location. (eg. "stdout" or "path/to/file.log") - noColors: false # disable colors in the output - json: false # output in json format - utc: false # use UTC time in the output instead of local time - debugMedia: false # output ffmpeg stdout for media integration -- careful can produces lots of output! - database: "/etc/filebrowser/database.db" # path to the database file - sources: # validate:required,dive - - path: "/" - name: "Root" # display name + database: "/etc/filebrowser/database.db" + sources: + - path: "/" # Do not use a root "/" directory or include the "/var" folder config: - denyByDefault: false # deny access unless an "allow" access rule was specifically created. - private: false # designate as source as private -- currently just means no sharing permitted. - disabled: false # disable the source, this is useful so you don't need to remove it from the config file - indexingIntervalMinutes: 0 # optional manual overide interval in minutes to re-index the source - disableIndexing: false # disable the indexing of this source - maxWatchers: 0 # number of concurrent watchers to use for this source, currently not supported - neverWatchPaths: [] - exclude: # exclude files and folders from indexing, if include is not set - hidden: false # exclude hidden files and folders. - ignoreZeroSizeFolders: false # ignore folders with 0 size - filePaths: [] - folderPaths: [] - fileNames: [] - folderNames: [] - fileEndsWith: [] - folderEndsWith: [] - fileStartsWith: [] - folderStartsWith: [] - include: # include files and folders from indexing, if exclude is not set - rootFolders: [] - rootFiles: [] - defaultUserScope: "/" # default "/" should match folders under path - defaultEnabled: false # should be added as a default source for new users? - createUserDir: false # create a user directory for each user - externalUrl: "" # used by share links if set (eg. http://mydomain.com) - internalUrl: "" # used by integrations if set, this is the base domain that an integration service will use to communicate with filebrowser (eg. http://localhost:8080) - cacheDir: "tmp" # path to the cache directory, used for thumbnails and other cached files - maxArchiveSize: 50 # max pre-archive combined size of files/folder that are allowed to be archived (in GB) + defaultEnabled: true auth: - tokenExpirationHours: 2 # time in hours each web UI session token is valid for. Default is 2 hours. - methods: - proxy: # validate:omitempty - enabled: false - createUser: false # create user if not exists - header: "" # required header to use for authentication. Security Warning: FileBrowser blindly accepts the header value as username. - logoutRedirectUrl: "" # if provider logout url is provided, filebrowser will also redirect to logout url. Custom logout query params are respected. - noauth: false # if set to true, overrides all other auth methods and disables authentication - password: # validate:omitempty - enabled: true - minLength: 3 # minimum pasword length required, default is 5. validate:omitempty - signup: false # allow signups on login page if enabled -- not secure. validate:omitempty - recaptcha: # recaptcha config, only used if signup is enabled validate:omitempty - host: "" # validate:required - key: "" # validate:required - secret: "" # validate:required - enforcedOtp: false # if set to true, TOTP is enforced for all password users users. Otherwise, users can choose to enable TOTP. - oidc: # validate:omitempty - enabled: false # whether to enable OIDC authentication - clientId: "**hidden**" # secret: client id of the OIDC application - clientSecret: "**hidden**" # secret: client secret of the OIDC application - issuerUrl: "" # authorization URL of the OIDC provider - scopes: "openid email profile" # scopes to request from the OIDC provider - userIdentifier: "preferred_username" # the field value to use as the username. Default is "preferred_username", can also be "email" or "username", or "phone" - disableVerifyTLS: false # disable TLS verification for the OIDC provider. This is insecure and should only be used for testing. - logoutRedirectUrl: "" # if provider logout url is provided, filebrowser will also redirect to logout url. Custom logout query params are respected. - createUser: false # create user if not exists - adminGroup: "" # if set, users in this group will be granted admin privileges. - groupsClaim: "groups" # the JSON field name to read groups from. Default is "groups" - key: "**hidden**" # secret: the key used to sign the JWT tokens. If not set, a random key will be generated. - adminUsername: "admin" # secret: the username of the admin user. If not set, the default is "admin". - adminPassword: "admin" # secret: the password of the admin user. If not set, the default is "admin". - totpSecret: "**hidden**" # secret: secret used to encrypt TOTP secrets -frontend: - name: "FileBrowser Quantum" # display name - disableDefaultLinks: false # disable default links in the sidebar - disableUsedPercentage: false # disable used percentage for the sources in the sidebar - externalLinks: - - text: "Help" # the text to display on the link validate:required - title: "" # the title to display on hover - url: "https://github.com/gtsteffaniak/filebrowser" # the url to link to validate:required - disableNavButtons: false # disable the nav buttons in the sidebar - styling: - customCSS: "" # if a valid path to a css file is provided, it will be applied for all users. (eg. "reduce-rounded-corners.css") - lightBackground: "#f5f5f5" # specify a valid CSS color property value to use as the background color in light mode - darkBackground: "#141D24" # Specify a valid CSS color property value to use as the background color in dark mode - customThemes: # A list of custom css files that each user can select to override the default styling. if "default" is key name then it will be the default option. - default: - description: "The default theme" # The description of the theme to display in the UI. - css: "" # The css file path and filename to use for the theme. - alternative: - description: "Reduce rounded corners" # The description of the theme to display in the UI. - css: "reduce-rounded-corners.css" # The css file path and filename to use for the theme. - favicon: "" # path to a favicon to use for the frontend - description: "FileBrowser Quantum is a file manager for the web which can be used to manage files on your server" # description that shows up in html head meta description + adminUsername: admin + adminPassword: "admin" userDefaults: - editorQuickSave: false # show quick save button in editor - hideSidebarFileActions: false # hide the file actions in the sidebar - disableQuickToggles: false # disable the quick toggles in the sidebar - disableSearchOptions: false # disable the search options in the search bar - stickySidebar: true # keep sidebar open when navigating - darkMode: true # should dark mode be enabled - locale: "zhCN" # language to use: eg. de, en, or fr - viewMode: "normal" # view mode to use: eg. normal, list, grid, or compact - singleClick: false # open directory on single click, also enables middle click to open in new tab - showHidden: false # show hidden files in the UI. On windows this includes files starting with a dot and windows hidden files - dateFormat: false # when false, the date is relative, when true, the date is an exact timestamp - gallerySize: 3 # 0-9 - the size of the gallery thumbnails - themeColor: "var(--blue)" # theme color to use: eg. #ff0000, or var(--red), var(--purple), etc - quickDownload: false # show icon to download in one click - disablePreviewExt: "" # space separated list of file extensions to disable preview for - disableViewingExt: "" # space separated list of file extensions to disable viewing for - lockPassword: false # disable the user from changing their password - disableSettings: false # disable the user from viewing the settings page - preview: - disableHideSidebar: false - highQuality: false - image: false - video: false - motionVideoPreview: false - office: false - popup: false - autoplayMedia: false - defaultMediaPlayer: false - folder: false - permissions: - api: false - admin: false - modify: false - share: false - realtime: false - loginMethod: "password" # login method to use: eg. password, proxy, oidc - disableUpdateNotifications: false # disable update notifications banner for admin users - deleteWithoutConfirming: false # delete files without confirmation - fileLoading: # upload and download settings - maxConcurrentUpload: 10 - uploadChunkSizeMb: 10 - clearAll: false - disableOnlyOfficeExt: "" # list of file extensions to disable onlyoffice editor for - customTheme: "" # Name of theme to use chosen from custom themes config. - showSelectMultiple: false # show select multiple files on desktop - debugOffice: false # debug onlyoffice editor + ui: + locale: "cn" diff --git a/filebrowser/files/filebrowser.init b/filebrowser/files/filebrowser.init index 230401d53..86152ca39 100644 --- a/filebrowser/files/filebrowser.init +++ b/filebrowser/files/filebrowser.init @@ -18,9 +18,11 @@ start() { local listen_port root_path base_url config_get listen_port "config" "listen_port" "8989" config_get root_path "config" "root_path" "/" - config_get base_url "config" "base_url" "/" echo "Starting filebrowser..." + sed -e "s/ port:.*/ port:$listen_port/" \ + -e "s, - path:.*, - path:\"$root_path\"," \ + -i "$CONF_PATH" start-stop-daemon -S -q -b -m -p "$PID_FILE" -x "$PROG" -- -c "$CONF_PATH" } diff --git a/luci-app-filebrowser/htdocs/luci-static/resources/view/filebrowser.js b/luci-app-filebrowser/htdocs/luci-static/resources/view/filebrowser.js index 473361bcb..d18e3ab8f 100644 --- a/luci-app-filebrowser/htdocs/luci-static/resources/view/filebrowser.js +++ b/luci-app-filebrowser/htdocs/luci-static/resources/view/filebrowser.js @@ -49,17 +49,24 @@ return view.extend({ s = m.section(form.TypedSection); s.anonymous = true; s.render = function() { - poll.add(function() { - return Promise.resolve().then(function() { - let view = document.getElementById('service_status'); - view.innerHTML = renderStatus(data.isRunning, webport); - }); - }); + poll.add(function() { + return fs.stat('/var/run/filebrowser.pid').then(function(stat) { + let view = document.getElementById('service_status'); + if (view) { + view.innerHTML = renderStatus(stat, webport); + } + }).catch(function() { + let view = document.getElementById('service_status'); + if (view) { + view.innerHTML = renderStatus(null, webport); + } + }); + }); - return E('div', { class: 'cbi-section', id: 'status_bar' }, [ - E('p', { id: 'service_status' }, _('Collecting data...')) - ]); - } + return E('div', { class: 'cbi-section', id: 'status_bar' }, [ + E('p', { id: 'service_status' }, _('Collecting data...')) + ]); +}; s = m.section(form.NamedSection, 'config', 'filebrowser'); diff --git a/luci-app-filetransfer/Makefile b/luci-app-filetransfer/Makefile index 4826f1ccd..d049f0897 100644 --- a/luci-app-filetransfer/Makefile +++ b/luci-app-filetransfer/Makefile @@ -8,11 +8,6 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=LuCI page for IPK upload LUCI_DEPENDS:=+luci-lib-fs -LUCI_PKGARCH:=all - -PKG_NAME:=luci-app-filetransfer -PKG_VERSION:=1 -PKG_RELEASE:=1 include $(TOPDIR)/feeds/luci/luci.mk diff --git a/luci-app-filetransfer/luasrc/controller/filetransfer.lua b/luci-app-filetransfer/luasrc/controller/filetransfer.lua index ac1f1f616..c67939059 100644 --- a/luci-app-filetransfer/luasrc/controller/filetransfer.lua +++ b/luci-app-filetransfer/luasrc/controller/filetransfer.lua @@ -9,5 +9,7 @@ module("luci.controller.filetransfer", package.seeall) function index() - entry({"admin", "system", "filetransfer"}, form("filetransfer"), _("FileTransfer"), 89).dependent = true + local page = entry({"admin", "system", "filetransfer"}, form("filetransfer"), _("FileTransfer"), 89) + page.dependent = true + page.acl_depends = { "luci-app-filetransfer" } end diff --git a/luci-app-filetransfer/luasrc/model/cbi/filetransfer.lua b/luci-app-filetransfer/luasrc/model/cbi/filetransfer.lua index 91da2bd9a..bb7d970f4 100644 --- a/luci-app-filetransfer/luasrc/model/cbi/filetransfer.lua +++ b/luci-app-filetransfer/luasrc/model/cbi/filetransfer.lua @@ -91,16 +91,6 @@ elseif luci.http.formvalue("download") then Download() end -local function getSizeStr(size) - local i = 0 - local byteUnits = {' kB', ' MB', ' GB', ' TB'} - repeat - size = size / 1024 - i = i + 1 - until(size <= 1024) - return string.format("%.1f", size) .. byteUnits[i] -end - local inits, attr = {} for i, f in ipairs(fs.glob("/tmp/upload/*")) do attr = fs.stat(f) @@ -109,7 +99,7 @@ for i, f in ipairs(fs.glob("/tmp/upload/*")) do inits[i].name = fs.basename(f) inits[i].mtime = os.date("%Y-%m-%d %H:%M:%S", attr.mtime) inits[i].modestr = attr.modestr - inits[i].size = getSizeStr(attr.size) + inits[i].size = tostring(attr.size) inits[i].remove = 0 inits[i].install = false end @@ -122,9 +112,8 @@ form.submit = false tb = form:section(Table, inits) nm = tb:option(DummyValue, "name", translate("File name")) mt = tb:option(DummyValue, "mtime", translate("Modify time")) -ms = tb:option(DummyValue, "modestr", translate("Attributes")) +ms = tb:option(DummyValue, "modestr", translate("Mode string")) sz = tb:option(DummyValue, "size", translate("Size")) - btnrm = tb:option(Button, "remove", translate("Remove")) btnrm.render = function(self, section, scope) self.inputstyle = "remove" diff --git a/luci-app-filetransfer/po/zh-cn b/luci-app-filetransfer/po/zh-cn new file mode 120000 index 000000000..8d69574dd --- /dev/null +++ b/luci-app-filetransfer/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-filetransfer/po/zh_Hans b/luci-app-filetransfer/po/zh_Hans deleted file mode 120000 index 41451e4a1..000000000 --- a/luci-app-filetransfer/po/zh_Hans +++ /dev/null @@ -1 +0,0 @@ -zh-cn \ No newline at end of file diff --git a/luci-app-filetransfer/po/zh-cn/filetransfer.po b/luci-app-filetransfer/po/zh_Hans/filetransfer.po similarity index 65% rename from luci-app-filetransfer/po/zh-cn/filetransfer.po rename to luci-app-filetransfer/po/zh_Hans/filetransfer.po index 925a47aab..829c7d285 100644 --- a/luci-app-filetransfer/po/zh-cn/filetransfer.po +++ b/luci-app-filetransfer/po/zh_Hans/filetransfer.po @@ -1,15 +1,5 @@ msgid "" -msgstr "" -"Content-Type: text/plain; charset=UTF-8\n" -"Project-Id-Version: \n" -"POT-Creation-Date: \n" -"PO-Revision-Date: \n" -"Last-Translator: dingpengyu \n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Transfer-Encoding: 8bit\n" -"Language: zh_CN\n" -"X-Generator: Poedit 2.3.1\n" +msgstr "Content-Type: text/plain; charset=UTF-8" msgid "Choose local file:" msgstr "选择本地文件:" @@ -38,17 +28,17 @@ msgstr "文件传输" msgid "Install" msgstr "安装" -msgid "Attributes" -msgstr "属性" +msgid "Mode string" +msgstr "模式字符串" msgid "Modify time" msgstr "修改时间" msgid "No specify upload file." -msgstr "未指定上传文件。" +msgstr "未指定上传文件" msgid "Path on Route:" -msgstr "路由根目录:" +msgstr "路由上目录" msgid "Remove" msgstr "移除" diff --git a/luci-app-filetransfer/root/etc/uci-defaults/luci-filetransfer b/luci-app-filetransfer/root/etc/uci-defaults/luci-filetransfer deleted file mode 100755 index 935d7c8be..000000000 --- a/luci-app-filetransfer/root/etc/uci-defaults/luci-filetransfer +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -sed -i 's/cbi.submit\"] = true/cbi.submit\"] = \"1\"/g' /usr/lib/lua/luci/dispatcher.lua - -rm -f /tmp/luci-indexcache -exit 0 diff --git a/luci-app-filetransfer/root/usr/share/rpcd/acl.d/luci-app-filetransfer.json b/luci-app-filetransfer/root/usr/share/rpcd/acl.d/luci-app-filetransfer.json new file mode 100644 index 000000000..a8a6bdcd8 --- /dev/null +++ b/luci-app-filetransfer/root/usr/share/rpcd/acl.d/luci-app-filetransfer.json @@ -0,0 +1,11 @@ +{ + "luci-app-filetransfer": { + "description": "Grant UCI access for luci-app-filetransfer", + "read": { + "uci": [ "filetransfer" ] + }, + "write": { + "uci": [ "filetransfer" ] + } + } +} diff --git a/luci-theme-footstrap/Makefile b/luci-theme-footstrap/Makefile index 64a887690..c4766a1cf 100644 --- a/luci-theme-footstrap/Makefile +++ b/luci-theme-footstrap/Makefile @@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap FOOTSTRAP_VERSION?= ifneq ($(FOOTSTRAP_VERSION),) PKG_VERSION:=$(FOOTSTRAP_VERSION) -PKG_RELEASE:=57 +PKG_RELEASE:=58 endif LUCI_TITLE:=Footstrap Theme diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-assets.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-assets.js index 0d17efb1f..a8f5b52a9 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-assets.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-assets.js @@ -43,17 +43,22 @@ const _uciCommit = rpc.declare({ object: 'uci', method: 'commit', params: [ 'con * Router-side, like the login photo and for the same reason — a file cannot live in localStorage, * and a pattern is something a router wears. The path is a fixed server-side constant matched * exactly by the rpcd ACL, so nothing user-controlled reaches a path. It lives under /etc so a - * package upgrade cannot delete it (keep.d carries it across a sysupgrade), and the served name - * ends in .svg because uhttpd types a file by extension. + * package upgrade cannot delete it (keep.d carries it across a sysupgrade). It keeps the .svg name + * for its own sake — the CGI handler that now serves it (root/www/cgi-bin) sets Content-Type + * itself and does not rely on uhttpd's by-extension typing the way the login photo's /www symlink + * still does. * * How it is made to fit is 15-wallpaper.css's mask, not anything done to the bytes: the file * supplies the alpha and the theme the colour, so one upload reads correctly in both modes and * under every palette. * * What is refused: an SVG is a document, not a picture, and while a masked or background image - * never executes script, the same file fetched from its own URL would. Uploading already needs an - * authenticated admin session with uci write rights, so this is defence in depth — but the check is - * cheap and the failure mode is somebody else's browser. */ + * never executes script, the same file fetched from its own URL would (OpenWrt forum thread + * 251930) — which is what the server-side handler (root/www/cgi-bin) now closes with response + * headers a browser will not execute. THIS check runs in the uploading admin's OWN browser and is + * not a defence at all: a `curl` POST to the same cgi-io path skips it entirely, and the rpcd ACL + * authorises that POST regardless of what wrote it. It exists only so an admin who uploads a bad + * file by mistake is told so before it lands, and the cost of running it is cheap. */ const PAT_PATH = '/etc/footstrap/pattern.svg'; /* cgi-upload target; the ACL grants exactly this */ const PAT_MAX = 512 * 1024; /* a tile that has to reach a router's flash and then every page load */ /* What makes an uploaded SVG unacceptable, decided on the PARSED document and not on its text: a @@ -65,19 +70,30 @@ const PAT_MAX = 512 * 1024; /* a tile that has to reach a router's flash * no subresource is fetched, no handler is bound. So the questions are exact ones about nodes: * * - is it an SVG at all (a parsererror, or a root that is not , is not an image) - * - does it carry an element that executes or embeds (script, foreignObject, iframe, …) + * - does it carry an element that executes or embeds (script, foreignObject, iframe, style, …) * - does it carry a real event-handler attribute — `^on[a-z]+$` * - does any value start a `javascript:` url - * - does any href point off this router; `#fragment` and `data:` stay allowed, being how a tile - * refers to its own and embeds a bitmap + * - does any href point off this router — a leading `//`, or a `data:`/`blob:`/`about:` payload + * - does any element carry a `style` attribute * * The check is for the way the file can be reached that a mask does not cover: its own URL, opened * directly, same-origin with the session. * * `animate`/`set` are listed for a second reason as well: they can retarget an attribute at run * time (``), and a tile that animates repaints a - * full-viewport layer behind every page. */ -const PAT_BAD_TAGS = [ 'script', 'foreignobject', 'iframe', 'embed', 'object', 'audio', 'video', 'animate', 'set' ]; + * full-viewport layer behind every page. + * + * `style` is refused both as an ELEMENT and as an ATTRIBUTE: a `` reaches off-router with no script and no href at all, and a + * `style="…url(…)…"` attribute does the same per-element. `data:`/`blob:`/`about:` were allowed + * through `href`/`xlink:href` before this pass, on the theory that a tile only ever uses `data:` to + * embed its own bitmap — closed anyway, because the same scheme admits `data:text/html` and + * `data:image/svg+xml`, and telling those apart from a `data:image/png` bitmap means parsing the + * URI's own MIME token, which is exactly the kind of grammar-guessing this file avoids everywhere + * else. The cost is real: an admin embedding a bitmap inside the tile now needs a second uploaded + * asset referenced by a same-origin URL instead of a `data:` href — the tile itself is unaffected, + * since 15-wallpaper.css never used one. */ +const PAT_BAD_TAGS = [ 'script', 'foreignobject', 'iframe', 'embed', 'object', 'audio', 'video', 'animate', 'set', 'style' ]; const SVG_NS = 'http://www.w3.org/2000/svg'; /* null if the parsed document is fine, otherwise the sentence to show. */ @@ -118,8 +134,17 @@ function _svgObjection(text) { * refuse files that do nothing. */ if ((/^on[a-z]+$/).test(n)) return refused; if ((/^javascript:/i).test(v)) return refused; - /* off-router reference. A leading `//` is protocol-relative and just as external. */ - if ((/(?:^|:)href$/).test(n) && (/^(?:[a-z][a-z0-9+.-]*:)?\/\//i).test(v)) return refused; + /* a `style=""` attribute reaches the same `url()` grammar as a `