hfs 3.4.0-alpha1 → 3.4.0-alpha2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/assets/AccountsPage-CoiPofEv.js +4 -0
- package/admin/assets/{ConfigFilePage-D5TZZyMR.js → ConfigFilePage-jUH2feqi.js} +1 -1
- package/admin/assets/{CustomHtmlPage-DV2-ufWZ.js → CustomHtmlPage-Pl_AJZrl.js} +1 -1
- package/admin/assets/{InternetPage-DT3wSQYQ.js → InternetPage-DIcAwhFk.js} +1 -1
- package/admin/assets/{LangPage-DbXCv7wR.js → LangPage-BwVY8QRD.js} +1 -1
- package/admin/assets/{LogsPage-DmAxUZpu.js → LogsPage-CcjS12cP.js} +1 -1
- package/admin/assets/{MonitorPage-D7ydOeq_.js → MonitorPage-DMVFXyMe.js} +1 -1
- package/admin/assets/OptionsPage-BB40ax9G.js +3 -0
- package/admin/assets/{PluginsPage-BYzfwuvg.js → PluginsPage-CC2dINcb.js} +1 -1
- package/admin/assets/{icons-CfBG9vvB.js → icons-C_ux1zLA.js} +1 -1
- package/admin/assets/{index-CVCnYMoB.js → index-BSSQOMG_.js} +4 -4
- package/admin/assets/{routing-C4l8rnk_.js → routing-Be72yFmd.js} +1 -1
- package/admin/index.html +1 -1
- package/frontend/assets/{dist-legacy-CKczq_fx.js → dist-legacy-D7Dg5w3L.js} +1 -1
- package/frontend/assets/index-legacy-CnazC3Dj.js +1 -0
- package/frontend/assets/{lib-legacy-D2tyILlj.js → lib-legacy-BxFHpdER.js} +1 -1
- package/frontend/assets/{sha512-legacy-CFcDeiiP.js → sha512-legacy-C8xoQBnP.js} +1 -1
- package/frontend/index.html +1 -1
- package/npm-shrinkwrap.json +66 -66
- package/package.json +1 -1
- package/plugins/antibrute/plugin.js +72 -18
- package/src/admin-langs/embedded.js +2 -2
- package/src/admin-langs/hfs-admin-lang-ar.json +34 -26
- package/src/admin-langs/hfs-admin-lang-bg.json +35 -27
- package/src/admin-langs/hfs-admin-lang-de.json +37 -29
- package/src/admin-langs/hfs-admin-lang-el.json +38 -30
- package/src/admin-langs/hfs-admin-lang-en.json +8 -0
- package/src/admin-langs/hfs-admin-lang-es.json +37 -29
- package/src/admin-langs/hfs-admin-lang-fi.json +100 -92
- package/src/admin-langs/hfs-admin-lang-fr.json +39 -31
- package/src/admin-langs/hfs-admin-lang-hu.json +37 -29
- package/src/admin-langs/hfs-admin-lang-it.json +35 -27
- package/src/admin-langs/hfs-admin-lang-ja.json +36 -28
- package/src/admin-langs/hfs-admin-lang-ko.json +39 -31
- package/src/admin-langs/hfs-admin-lang-lt.json +35 -27
- package/src/admin-langs/hfs-admin-lang-ms.json +36 -28
- package/src/admin-langs/hfs-admin-lang-nl.json +36 -28
- package/src/admin-langs/hfs-admin-lang-pl.json +36 -28
- package/src/admin-langs/hfs-admin-lang-pt-br.json +36 -28
- package/src/admin-langs/hfs-admin-lang-ro.json +37 -29
- package/src/admin-langs/hfs-admin-lang-ru.json +35 -27
- package/src/admin-langs/hfs-admin-lang-sr-latn.json +35 -27
- package/src/admin-langs/hfs-admin-lang-sr.json +39 -31
- package/src/admin-langs/hfs-admin-lang-th.json +35 -27
- package/src/admin-langs/hfs-admin-lang-tr.json +38 -30
- package/src/admin-langs/hfs-admin-lang-uk.json +35 -27
- package/src/admin-langs/hfs-admin-lang-vi.json +35 -27
- package/src/admin-langs/hfs-admin-lang-zh-hant.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-zh.json +728 -720
- package/src/adminApis.js +2 -1
- package/src/adminLang.js +8 -7
- package/src/api.accounts.js +9 -4
- package/src/api.auth.js +11 -3
- package/src/api.vfs.js +3 -2
- package/src/auth.js +25 -14
- package/src/consoleLog.js +4 -0
- package/src/const.js +1 -1
- package/src/cross-const.js +1 -1
- package/src/cross.js +1 -1
- package/src/customHtml.js +3 -2
- package/src/deletePermission.js +120 -0
- package/src/events.js +6 -0
- package/src/index.js +3 -2
- package/src/lang.js +6 -1
- package/src/langs/embedded.js +2 -2
- package/src/langs/hfs-lang-ar.json +2 -1
- package/src/langs/hfs-lang-bg.json +2 -1
- package/src/langs/hfs-lang-de.json +2 -1
- package/src/langs/hfs-lang-el.json +2 -1
- package/src/langs/hfs-lang-en.json +2 -1
- package/src/langs/hfs-lang-es.json +2 -1
- package/src/langs/hfs-lang-fi.json +2 -1
- package/src/langs/hfs-lang-fr.json +2 -1
- package/src/langs/hfs-lang-hu.json +2 -1
- package/src/langs/hfs-lang-it.json +2 -1
- package/src/langs/hfs-lang-ja.json +2 -1
- package/src/langs/hfs-lang-ko.json +2 -1
- package/src/langs/hfs-lang-lt.json +2 -1
- package/src/langs/hfs-lang-ms.json +2 -1
- package/src/langs/hfs-lang-nl.json +2 -1
- package/src/langs/hfs-lang-pl.json +2 -1
- package/src/langs/hfs-lang-pt-br.json +2 -1
- package/src/langs/hfs-lang-ro.json +2 -1
- package/src/langs/hfs-lang-ru.json +2 -1
- package/src/langs/hfs-lang-sr-latn.json +2 -1
- package/src/langs/hfs-lang-sr.json +2 -1
- package/src/langs/hfs-lang-th.json +2 -1
- package/src/langs/hfs-lang-tr.json +2 -1
- package/src/langs/hfs-lang-uk.json +2 -1
- package/src/langs/hfs-lang-vi.json +2 -1
- package/src/langs/hfs-lang-zh-hant.json +189 -0
- package/src/langs/hfs-lang-zh.json +96 -98
- package/src/listen.js +8 -4
- package/src/middlewares.js +31 -28
- package/src/perm.js +26 -4
- package/src/plugins.js +2 -2
- package/src/serveFile.js +5 -9
- package/src/serveGuiAndSharedFiles.js +9 -8
- package/src/serveGuiFiles.js +2 -2
- package/src/sessionStamp.js +53 -0
- package/src/upload.js +1 -1
- package/src/uploadOwners.js +4 -2
- package/src/vfs.js +80 -33
- package/src/webdav.js +2 -2
- package/admin/assets/AccountsPage-C6J2hRcY.js +0 -4
- package/admin/assets/OptionsPage-DpN3ajRm.js +0 -3
- package/frontend/assets/index-legacy-ZYae30AY.js +0 -1
- package/src/admin-langs/hfs-admin-lang-zh-tw.json +0 -1141
- package/src/langs/hfs-lang-zh-tw.json +0 -187
package/src/adminApis.js
CHANGED
|
@@ -38,6 +38,7 @@ const ddns_1 = require("./ddns");
|
|
|
38
38
|
const block_1 = require("./block");
|
|
39
39
|
const github_1 = require("./github");
|
|
40
40
|
const acme_1 = require("./acme");
|
|
41
|
+
const first_1 = require("./first");
|
|
41
42
|
exports.adminApis = {
|
|
42
43
|
...api_vfs_1.default,
|
|
43
44
|
...api_accounts_1.default,
|
|
@@ -133,7 +134,7 @@ exports.adminApis = {
|
|
|
133
134
|
return {};
|
|
134
135
|
},
|
|
135
136
|
quit() {
|
|
136
|
-
setTimeout(
|
|
137
|
+
setTimeout(first_1.quit); // let the API response finish before starting ordered cleanup
|
|
137
138
|
return {};
|
|
138
139
|
},
|
|
139
140
|
async get_status(_params, ctx) {
|
package/src/adminLang.js
CHANGED
|
@@ -22,7 +22,7 @@ async function getAdminLangs() {
|
|
|
22
22
|
return lodash_1.default.uniq([...Object.keys(embedded_1.default), ...files.map(name => (0, lang_1.file2code)(name, true))
|
|
23
23
|
.filter(code => (0, lang_1.normalizeLangCode)(code) === code)]);
|
|
24
24
|
}
|
|
25
|
-
exports.adminLang = (0, config_1.defineConfig)(misc_1.CFG.admin_lang, '', lang_1.normalizeLangCode);
|
|
25
|
+
exports.adminLang = (0, config_1.defineConfig)(misc_1.CFG.admin_lang, '', v => (0, lang_1.langCodeForLookup)((0, lang_1.normalizeLangCode)(v)));
|
|
26
26
|
const cache = (0, expiringCache_1.expiringCache)(3_000);
|
|
27
27
|
function getAdminLangData(ctx, langs) {
|
|
28
28
|
const configured = exports.adminLang.compiled();
|
|
@@ -39,12 +39,13 @@ function invalidateAdminLang(code) {
|
|
|
39
39
|
function browserAdminLang(ctx, langs) {
|
|
40
40
|
const accepted = ctx?.get('Accept-Language') || '';
|
|
41
41
|
for (const raw of accepted.split(',')) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
let code = (0, lang_1.normalizeLangCode)(raw);
|
|
43
|
+
while (code) {
|
|
44
|
+
const candidate = (0, lang_1.langCodeForLookup)(code);
|
|
45
|
+
if (langs.includes(candidate))
|
|
46
|
+
return candidate;
|
|
47
|
+
code = code.substring(0, code.lastIndexOf('-'));
|
|
48
|
+
}
|
|
48
49
|
}
|
|
49
50
|
return const_1.EMBEDDED_LANGUAGE;
|
|
50
51
|
}
|
package/src/api.accounts.js
CHANGED
|
@@ -9,6 +9,7 @@ const perm_1 = require("./perm");
|
|
|
9
9
|
const lodash_1 = __importDefault(require("lodash"));
|
|
10
10
|
const const_1 = require("./const");
|
|
11
11
|
const auth_1 = require("./auth");
|
|
12
|
+
const sessionStamp_1 = require("./sessionStamp");
|
|
12
13
|
const misc_1 = require("./misc");
|
|
13
14
|
const api_vfs_1 = require("./api.vfs");
|
|
14
15
|
function serializeAccount(ac) {
|
|
@@ -23,7 +24,7 @@ function serializeAccount(ac) {
|
|
|
23
24
|
adminActualAccess: (0, perm_1.accountCanLoginAdmin)(ac),
|
|
24
25
|
canLogin: hasLogin ? (0, perm_1.accountCanLogin)(ac) : undefined,
|
|
25
26
|
canChangePassword: (0, perm_1.accountCanChangePassword)(ac),
|
|
26
|
-
invalidated:
|
|
27
|
+
invalidated: Number((0, sessionStamp_1.getSessionStamp)(ac.username)?.split('.')[0]) || undefined,
|
|
27
28
|
directMembers: Object.values(perm_1.accounts.get()).filter(a => a.belongs?.includes(ac.username)).map(x => x.username),
|
|
28
29
|
members: (0, misc_1.with_)(Object.values(perm_1.accounts.get()), accounts => {
|
|
29
30
|
const ret = [];
|
|
@@ -60,11 +61,13 @@ exports.default = {
|
|
|
60
61
|
if (!acc)
|
|
61
62
|
return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST);
|
|
62
63
|
await (0, perm_1.updateAccount)(acc, (0, api_vfs_1.pickProps)(changes, ALLOWED_KEYS));
|
|
63
|
-
if (
|
|
64
|
+
if (ctx.session?.username === (0, perm_1.normalizeUsername)(username)) { // preserve only the session making its own account change
|
|
64
65
|
ctx.session.username = acc.username;
|
|
66
|
+
ctx.session.stamp = (0, sessionStamp_1.getSessionStamp)(acc.username);
|
|
67
|
+
}
|
|
65
68
|
return lodash_1.default.pick(acc, 'username');
|
|
66
69
|
},
|
|
67
|
-
async add_account({ overwrite, username, ...rest }) {
|
|
70
|
+
async add_account({ overwrite, username, ...rest }, ctx) {
|
|
68
71
|
(0, misc_1.apiAssertTypes)({ string: { username } });
|
|
69
72
|
const existing = (0, perm_1.getAccount)(username);
|
|
70
73
|
rest = (0, api_vfs_1.pickProps)(rest, ALLOWED_KEYS);
|
|
@@ -72,6 +75,8 @@ exports.default = {
|
|
|
72
75
|
if (!overwrite)
|
|
73
76
|
return new apiMiddleware_1.ApiError(const_1.HTTP_CONFLICT);
|
|
74
77
|
await (0, perm_1.updateAccount)(existing, rest);
|
|
78
|
+
if (ctx.session?.username === existing.username)
|
|
79
|
+
ctx.session.stamp = (0, sessionStamp_1.getSessionStamp)(existing.username);
|
|
75
80
|
return lodash_1.default.pick(existing, 'username');
|
|
76
81
|
}
|
|
77
82
|
const acc = await (0, perm_1.addAccount)(username, rest);
|
|
@@ -87,7 +92,7 @@ exports.default = {
|
|
|
87
92
|
},
|
|
88
93
|
invalidate_sessions({ username }) {
|
|
89
94
|
(0, misc_1.apiAssertTypes)({ string: { username } });
|
|
90
|
-
|
|
95
|
+
(0, perm_1.invalidateAccountSessions)(username);
|
|
91
96
|
return {};
|
|
92
97
|
},
|
|
93
98
|
};
|
package/src/api.auth.js
CHANGED
|
@@ -16,6 +16,7 @@ const events_1 = __importDefault(require("./events"));
|
|
|
16
16
|
const misc_1 = require("./misc");
|
|
17
17
|
const uploadOwners_1 = require("./uploadOwners");
|
|
18
18
|
const node_crypto_1 = require("node:crypto");
|
|
19
|
+
const sessionStamp_1 = require("./sessionStamp");
|
|
19
20
|
const ongoingLogins = {}; // store data that doesn't fit session object
|
|
20
21
|
const keepSessionAlive = (0, config_1.defineConfig)(misc_1.CFG.keep_session_alive, true);
|
|
21
22
|
const fakeSrpSecret = (0, node_crypto_1.randomBytes)(32);
|
|
@@ -66,7 +67,7 @@ exports.authApis = {
|
|
|
66
67
|
return new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR);
|
|
67
68
|
if (account?.plugin?.auth) // tell client to do clear-text login, before firing attemptingLogin, before triggering anti-brute
|
|
68
69
|
return new apiMiddleware_1.ApiError(const_1.HTTP_METHOD_NOT_ALLOWED);
|
|
69
|
-
if ((await events_1.default.emitAsync('attemptingLogin', { ctx, username }))?.isDefaultPrevented())
|
|
70
|
+
if ((await events_1.default.emitAsync('attemptingLogin', { ctx, username, via: 'srp' }))?.isDefaultPrevented())
|
|
70
71
|
return;
|
|
71
72
|
if (account && !(0, perm_1.accountCanLogin)(account)) {
|
|
72
73
|
ctx.logExtra({ u: username });
|
|
@@ -76,13 +77,14 @@ exports.authApis = {
|
|
|
76
77
|
if (account && (0, middlewares_1.failAllowNet)(ctx, account))
|
|
77
78
|
return unauthorized();
|
|
78
79
|
try { // unknown users complete step 1 so only a full failed login can reveal and penalize the attempt
|
|
80
|
+
const stamp = account && (0, sessionStamp_1.getSessionStamp)(account.username);
|
|
79
81
|
const { srpServer, ...rest } = await (0, auth_1.srpServerStep1)(account || fakeSrpAccount(username));
|
|
80
82
|
// keep the public handshake identifier independent of predictable application PRNG state
|
|
81
83
|
const sid = (0, node_crypto_1.randomUUID)();
|
|
82
84
|
ongoingLogins[sid] = srpServer;
|
|
83
85
|
setTimeout(() => delete ongoingLogins[sid], 60_000); // client must complete api sequence (loginSrp2) within 1 minute or will be discarded to avoid memory leaks
|
|
84
86
|
// 60-second expiry bounded 5k challenges to ~37 MB in a local saturation test, so an additional cap isn't justified
|
|
85
|
-
ctx.session.loggingIn = { username, sid }; // temporarily store until process is complete
|
|
87
|
+
ctx.session.loggingIn = { username, sid, stamp }; // temporarily store until process is complete
|
|
86
88
|
return rest;
|
|
87
89
|
}
|
|
88
90
|
catch (code) {
|
|
@@ -109,7 +111,7 @@ exports.authApis = {
|
|
|
109
111
|
return new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR);
|
|
110
112
|
if (!ctx.session.loggingIn)
|
|
111
113
|
return new apiMiddleware_1.ApiError(const_1.HTTP_CONFLICT);
|
|
112
|
-
const { username, sid } = ctx.session.loggingIn;
|
|
114
|
+
const { username, sid, stamp } = ctx.session.loggingIn;
|
|
113
115
|
delete ctx.session.loggingIn;
|
|
114
116
|
const step1 = ongoingLogins[sid];
|
|
115
117
|
if (!step1)
|
|
@@ -117,6 +119,10 @@ exports.authApis = {
|
|
|
117
119
|
try {
|
|
118
120
|
const M2 = await step1.step2(BigInt(pubKey), BigInt(proof))
|
|
119
121
|
.catch(() => { throw ''; }); // falsy value for later
|
|
122
|
+
// an old-password handshake must not mint a session after credentials have been revoked
|
|
123
|
+
const account = (0, perm_1.getAccount)(username);
|
|
124
|
+
if (!account || !stamp || stamp !== (0, sessionStamp_1.getSessionStamp)(account.username))
|
|
125
|
+
throw '';
|
|
120
126
|
await (0, auth_1.setLoggedIn)(ctx, username, 'srp');
|
|
121
127
|
return {
|
|
122
128
|
proof: String(M2),
|
|
@@ -150,6 +156,8 @@ exports.authApis = {
|
|
|
150
156
|
if (!salt || !verifier)
|
|
151
157
|
return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'missing parameters');
|
|
152
158
|
await (0, perm_1.updateAccount)(a, a => (0, perm_1.saveSrpInfo)(a, salt, verifier));
|
|
159
|
+
if (ctx.session?.username === a.username)
|
|
160
|
+
ctx.session.stamp = (0, sessionStamp_1.getSessionStamp)(a.username);
|
|
153
161
|
delete a.require_password_change;
|
|
154
162
|
return {};
|
|
155
163
|
}
|
package/src/api.vfs.js
CHANGED
|
@@ -36,8 +36,9 @@ exports.default = {
|
|
|
36
36
|
delete copyStats.mtime;
|
|
37
37
|
const inherited = node.parent && (0, vfs_1.permsFromParent)(node.parent, {});
|
|
38
38
|
const byMasks = node.original && lodash_1.default.pickBy(node, (v, k) => v !== node.original[k] // something is changing me...
|
|
39
|
-
&&
|
|
40
|
-
|
|
39
|
+
&& (k === 'gui_asset'
|
|
40
|
+
? v !== node.parent?.gui_asset // gui_asset inherits outside the permission helpers
|
|
41
|
+
: !(inherited && k in inherited) && misc_1.PERM_KEYS.includes(k))); // ...must be masks
|
|
41
42
|
return {
|
|
42
43
|
...copyStats,
|
|
43
44
|
...node.original || node,
|
package/src/auth.js
CHANGED
|
@@ -36,7 +36,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.invalidateSessionBefore = void 0;
|
|
40
39
|
exports.srpServerStep1 = srpServerStep1;
|
|
41
40
|
exports.srpCheck = srpCheck;
|
|
42
41
|
exports.getCurrentUsername = getCurrentUsername;
|
|
@@ -51,6 +50,7 @@ const cross_1 = require("./cross");
|
|
|
51
50
|
const expiringCache_1 = require("./expiringCache");
|
|
52
51
|
const node_crypto_1 = require("node:crypto");
|
|
53
52
|
const events_1 = __importDefault(require("./events"));
|
|
53
|
+
const sessionStamp_1 = require("./sessionStamp");
|
|
54
54
|
const srp6aNimbusRoutines = new srp.SRPRoutines(new srp.SRPParameters());
|
|
55
55
|
async function srpServerStep1(account) {
|
|
56
56
|
if (!account.srp)
|
|
@@ -67,13 +67,17 @@ async function srpCheck(username, password) {
|
|
|
67
67
|
const account = (0, perm_1.getAccount)(username);
|
|
68
68
|
if (!account?.srp || !password)
|
|
69
69
|
return;
|
|
70
|
-
const
|
|
70
|
+
const verifier = account.srp;
|
|
71
|
+
const sessionStamp = (0, sessionStamp_1.getSessionStamp)(account.username);
|
|
72
|
+
const k = (0, node_crypto_1.createHash)('sha256').update(username + password + verifier).digest("hex");
|
|
71
73
|
const good = await cache.try(k, async () => {
|
|
72
74
|
const { srpServer, salt, pubKey } = await srpServerStep1(account);
|
|
73
75
|
const client = await (0, srp_1.srpClientPart)(srp, username, password, salt, pubKey);
|
|
74
76
|
return srpServer.step2(client.A, client.M1).then(() => true, () => false);
|
|
75
77
|
});
|
|
76
|
-
|
|
78
|
+
// verification can finish after a password change or account replacement
|
|
79
|
+
return good && (0, perm_1.getAccount)(username) === account && account.srp === verifier && (0, sessionStamp_1.getSessionStamp)(account.username) === sessionStamp
|
|
80
|
+
? account : undefined;
|
|
77
81
|
}
|
|
78
82
|
function getCurrentUsername(ctx) {
|
|
79
83
|
return ctx.state.account?.username || '';
|
|
@@ -81,13 +85,19 @@ function getCurrentUsername(ctx) {
|
|
|
81
85
|
async function clearTextLogin(ctx, u, p, via) {
|
|
82
86
|
if (!p)
|
|
83
87
|
return;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
let account;
|
|
89
|
+
try {
|
|
90
|
+
if ((await events_1.default.emitAsync('attemptingLogin', { ctx, username: u, via }))?.isDefaultPrevented())
|
|
91
|
+
return;
|
|
92
|
+
const plugins = await events_1.default.emitAsync('clearTextLogin', { ctx, username: u, password: p, via }); // provide clear password to plugins
|
|
93
|
+
account = plugins?.some(x => x === true) ? (0, perm_1.getAccount)(u) : await srpCheck(u, p);
|
|
94
|
+
if (!account && u)
|
|
95
|
+
events_1.default.emit('failedLogin', { ctx, username: u, via });
|
|
96
|
+
return account;
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
events_1.default.emit('loginAttemptFinished', { ctx, username: u, via, success: Boolean(account) });
|
|
100
|
+
}
|
|
91
101
|
}
|
|
92
102
|
// centralized log-in state
|
|
93
103
|
async function setLoggedIn(ctx, username, via) {
|
|
@@ -106,8 +116,11 @@ async function setLoggedIn(ctx, username, via) {
|
|
|
106
116
|
const a = ctx.state.account = (0, perm_1.getAccount)(username);
|
|
107
117
|
if (!a)
|
|
108
118
|
return;
|
|
119
|
+
const sessionStamp = (0, sessionStamp_1.getSessionStamp)(a.username);
|
|
109
120
|
const result = await events_1.default.emitAsync('finalizingLogin', { ctx, username: a.username, via, inputs: { ...ctx.state.params, ...ctx.query } });
|
|
110
|
-
|
|
121
|
+
// async plugin checks must not let a revoked login acquire the new session stamp
|
|
122
|
+
const error = result?.find(x => x && lodash_1.default.isString(x)) || (result?.isDefaultPrevented()
|
|
123
|
+
|| !sessionStamp || (0, perm_1.getAccount)(username) !== a || (0, sessionStamp_1.getSessionStamp)(a.username) !== sessionStamp) && "Login denied";
|
|
111
124
|
if (error) {
|
|
112
125
|
// restore the session identity: the candidate account was exposed only for plugin checks
|
|
113
126
|
ctx.state.account = (0, perm_1.getAccount)(s.username, false);
|
|
@@ -116,13 +129,11 @@ async function setLoggedIn(ctx, username, via) {
|
|
|
116
129
|
}
|
|
117
130
|
const normalized = (0, perm_1.normalizeUsername)(username);
|
|
118
131
|
s.username = normalized;
|
|
119
|
-
s.ts = Date.now();
|
|
120
132
|
s.ip = ctx.ip;
|
|
133
|
+
s.stamp = sessionStamp;
|
|
121
134
|
const k = cross_const_1.ALLOW_SESSION_IP_CHANGE;
|
|
122
135
|
s[k] = k in ctx.query || Boolean(ctx.state.params?.[k]) || undefined; // login APIs will get ctx.state.params, others can rely on ctx.query
|
|
123
136
|
if (!a.expire && a.days_to_live)
|
|
124
137
|
(0, perm_1.updateAccount)(a, { expire: new Date(Date.now() + a.days_to_live * cross_1.DAY) });
|
|
125
138
|
await events_1.default.emitAsync('login', ctx);
|
|
126
139
|
}
|
|
127
|
-
// since session are currently stored in cookies, we need to store this information
|
|
128
|
-
exports.invalidateSessionBefore = new Map();
|
package/src/consoleLog.js
CHANGED
|
@@ -17,6 +17,7 @@ f?.on('error', err => {
|
|
|
17
17
|
console.error("Cannot write console file", argv_1.argv.consoleFile, String(err));
|
|
18
18
|
});
|
|
19
19
|
let terminalOutputBroken = false;
|
|
20
|
+
let lastTerminalDate = '';
|
|
20
21
|
for (const stream of [process.stdout, process.stderr])
|
|
21
22
|
stream.on('error', err => {
|
|
22
23
|
if (!isBrokenTerminalOutput(err))
|
|
@@ -40,6 +41,9 @@ for (const k of ['log', 'warn', 'error', 'debug']) {
|
|
|
40
41
|
events_1.default.emit('console', rec);
|
|
41
42
|
f?.write(`${(0, cross_1.formatTimestamp)(ts)} [${k}] ${msg}\n`);
|
|
42
43
|
if (!terminalOutputBroken) {
|
|
44
|
+
const date = (0, cross_1.formatDate)(ts);
|
|
45
|
+
if (date !== lastTerminalDate)
|
|
46
|
+
console.log.original(lastTerminalDate = date);
|
|
43
47
|
try {
|
|
44
48
|
return original((0, cross_1.formatTime)(ts), ...args);
|
|
45
49
|
} // bundled nodejs doesn't have locales (and apparently uses en-US)
|
package/src/const.js
CHANGED
|
@@ -49,7 +49,7 @@ const path_1 = require("path");
|
|
|
49
49
|
const cross_1 = require("./cross");
|
|
50
50
|
const argv_1 = require("./argv");
|
|
51
51
|
__exportStar(require("./cross-const"), exports);
|
|
52
|
-
exports.API_VERSION = 13.
|
|
52
|
+
exports.API_VERSION = 13.71;
|
|
53
53
|
exports.COMPATIBLE_API_VERSION = 1; // the day we break with the past, we'll update this
|
|
54
54
|
// you can add arguments with this file, currently used for the update process on mac/linux.
|
|
55
55
|
// we are using homedir because it's the only stable path both the old and new process can agree on (open() doesn't preserve cwd)
|
package/src/cross-const.js
CHANGED
|
@@ -43,7 +43,7 @@ exports.CFG = constMap([
|
|
|
43
43
|
'smart_unc_folder_detection', 'sort_by', 'sort_numerics', 'split_uploads',
|
|
44
44
|
'suspend_plugins', 'theme', 'tile_size', 'title', 'title_with_path', 'track_ips',
|
|
45
45
|
'update_to_beta', 'upnp_enabled', 'version', 'vfs', 'webdav_initial_auth',
|
|
46
|
-
'zip_calculate_size_for_seconds', 'https_port'
|
|
46
|
+
'zip_calculate_size_for_seconds', 'https_port', 'require_upload_approval_for_active_content',
|
|
47
47
|
]);
|
|
48
48
|
function constMap(a) {
|
|
49
49
|
return Object.fromEntries(a.map(x => [x, x]));
|
package/src/cross.js
CHANGED
|
@@ -143,7 +143,7 @@ exports.defaultPerms = {
|
|
|
143
143
|
};
|
|
144
144
|
exports.PERM_KEYS = typedKeys(exports.defaultPerms);
|
|
145
145
|
exports.VFS_STORED_KEYS = ['name', 'source', 'masks', 'default', 'accept', 'rename',
|
|
146
|
-
'mime', 'url', 'target', 'comment', 'icon', 'order', 'see_without_probing', 'children', ...exports.PERM_KEYS];
|
|
146
|
+
'mime', 'url', 'target', 'comment', 'icon', 'order', 'gui_asset', 'see_without_probing', 'children', ...exports.PERM_KEYS];
|
|
147
147
|
function isWhoObject(v) {
|
|
148
148
|
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
149
149
|
}
|
package/src/customHtml.js
CHANGED
|
@@ -14,8 +14,9 @@ const config_1 = require("./config");
|
|
|
14
14
|
const plugins_1 = require("./plugins");
|
|
15
15
|
const lodash_1 = __importDefault(require("lodash"));
|
|
16
16
|
const FILE = 'custom.html';
|
|
17
|
-
exports.customHtmlSections = ['style', 'script', 'beforeHeader', 'afterHeader', 'afterMenuBar',
|
|
18
|
-
'footer', 'top', 'bottom', 'afterEntryName', 'beforeLogin', 'unauthorized', 'htmlHead', 'userPanelAfterInfo'
|
|
17
|
+
exports.customHtmlSections = ['style', 'script', 'beforeHeader', 'afterHeader', 'afterMenuBar',
|
|
18
|
+
'footer', 'top', 'bottom', 'afterEntryName', 'beforeLogin', 'unauthorized', 'htmlHead', 'userPanelAfterInfo',
|
|
19
|
+
'afterList', 'beforeOptions', 'afterOptions'];
|
|
19
20
|
exports.customHtml = watchLoadCustomHtml();
|
|
20
21
|
exports.disableCustomHtml = (0, config_1.defineConfig)(misc_1.CFG.disable_custom_html, false);
|
|
21
22
|
function watchLoadCustomHtml(folder = '') {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.checkDeletePermission = checkDeletePermission;
|
|
7
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
8
|
+
const node_path_1 = require("node:path");
|
|
9
|
+
const cross_1 = require("./cross");
|
|
10
|
+
const const_1 = require("./const");
|
|
11
|
+
const events_1 = __importDefault(require("./events"));
|
|
12
|
+
const uploadOwners_1 = require("./uploadOwners");
|
|
13
|
+
const vfs_1 = require("./vfs");
|
|
14
|
+
// unlike a listing, deletion affects hidden entries and must fail if any directory cannot be read
|
|
15
|
+
async function checkDeletePermission(node, ctx) {
|
|
16
|
+
if ((0, vfs_1.statusCodeForMissingPerm)(node, 'can_delete', ctx))
|
|
17
|
+
return false;
|
|
18
|
+
if (!node.source || !(await promises_1.default.lstat(node.source)).isDirectory())
|
|
19
|
+
return true;
|
|
20
|
+
return recur(node, []);
|
|
21
|
+
async function recur(parent, scopes) {
|
|
22
|
+
// explicit VFS entries are not deletable, even when their source is several levels below this directory
|
|
23
|
+
for (const child of explicitNodes(parent)) {
|
|
24
|
+
if (!child.source)
|
|
25
|
+
continue;
|
|
26
|
+
const path = (0, node_path_1.relative)((0, node_path_1.resolve)(parent.source), (0, node_path_1.resolve)(child.source));
|
|
27
|
+
if (path === '..' || path.startsWith('..' + node_path_1.sep) || (0, node_path_1.isAbsolute)(path))
|
|
28
|
+
continue;
|
|
29
|
+
try {
|
|
30
|
+
await promises_1.default.lstat(child.source);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error.code === 'ENOENT')
|
|
34
|
+
continue;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
ctx.status = const_1.HTTP_FORBIDDEN;
|
|
38
|
+
await reportBlocked(child);
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const inherited = (0, vfs_1.permsFromParent)(parent, {}) || {};
|
|
42
|
+
const keys = new Set(['can_delete']);
|
|
43
|
+
for (const key of keys) {
|
|
44
|
+
addReferences(parent[key] ?? cross_1.defaultPerms[key]);
|
|
45
|
+
addReferences(inherited[key] ?? cross_1.defaultPerms[key]);
|
|
46
|
+
}
|
|
47
|
+
// skip scanning descendants only when inherited permissions guarantee deletion and no deeper rule or plugin can override them
|
|
48
|
+
if (!scopes.some(scope => hasRules(scope.node, keys)) && !hasRules(parent, keys)
|
|
49
|
+
&& !cross_1.PERM_KEYS.some(key => (0, cross_1.isWhoObject)(inherited[key]))
|
|
50
|
+
&& !events_1.default.hasOtherListeners('checkVfsPermission', uploadOwners_1.grantUploadOwnerDelete)
|
|
51
|
+
&& !(0, vfs_1.statusCodeForMissingPerm)(inherited, 'can_delete', ctx, false, false))
|
|
52
|
+
return true;
|
|
53
|
+
// only literal path prefixes can be exhausted by inheritMasks; other patterns may match again deeper down
|
|
54
|
+
const persistentMasks = Object.fromEntries(Object.entries(parent.masks || {}).filter(([mask, mods]) => mods && (cross_1.PERM_KEYS.some(key => mods[key] !== undefined) || hasRules(mods, new Set(cross_1.PERM_KEYS)))
|
|
55
|
+
&& (!mask.includes('/') || /[!*?\[\]{}()|\\]/.test(mask.split('/')[0]))));
|
|
56
|
+
if (Object.keys(persistentMasks).length)
|
|
57
|
+
scopes = [...scopes, { node: { masks: persistentMasks }, path: '' }];
|
|
58
|
+
for await (const entry of await promises_1.default.opendir(parent.source)) {
|
|
59
|
+
if (ctx.isAborted())
|
|
60
|
+
throw Error("Deletion cancelled");
|
|
61
|
+
const source = (0, node_path_1.join)(parent.source, entry.name);
|
|
62
|
+
const name = (0, vfs_1.getVirtualName)(entry.name, parent, source);
|
|
63
|
+
const explicit = (0, vfs_1.getChildBySource)(parent, source);
|
|
64
|
+
// permission masks use the target type, but deletion must never recurse through the link
|
|
65
|
+
const isFolder = entry.isSymbolicLink()
|
|
66
|
+
? await promises_1.default.stat(source).then(stats => stats.isDirectory(), error => {
|
|
67
|
+
if (error.code === 'ENOENT')
|
|
68
|
+
return false;
|
|
69
|
+
throw error;
|
|
70
|
+
}) : entry.isDirectory();
|
|
71
|
+
const child = {
|
|
72
|
+
...explicit, original: explicit, source, isFolder,
|
|
73
|
+
rename: explicit?.rename || (0, vfs_1.renameUnderPath)(parent.rename, entry.name),
|
|
74
|
+
};
|
|
75
|
+
const nextScopes = scopes.map(scope => ({ node: scope.node, path: scope.path + name }));
|
|
76
|
+
for (const scope of [...nextScopes].reverse()) {
|
|
77
|
+
(0, vfs_1.parentMaskApplier)(scope.node, true)(child, scope.path);
|
|
78
|
+
(0, vfs_1.parentMaskApplier)(scope.node)(child, name);
|
|
79
|
+
}
|
|
80
|
+
const evaluated = (0, vfs_1.applyParentToChild)(child, parent, name);
|
|
81
|
+
if ((0, vfs_1.statusCodeForMissingPerm)(evaluated, 'can_delete', ctx)) {
|
|
82
|
+
await reportBlocked(evaluated);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
// dirents do not follow symlinks, including dangling links
|
|
86
|
+
if (entry.isDirectory() && !await recur(evaluated, nextScopes.map(scope => ({ ...scope, path: scope.path + '/' }))))
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
function addReferences(who) {
|
|
91
|
+
if ((0, cross_1.isWhoObject)(who)) {
|
|
92
|
+
if (who.this !== undefined)
|
|
93
|
+
addReferences(who.this);
|
|
94
|
+
if (who.children !== undefined)
|
|
95
|
+
addReferences(who.children);
|
|
96
|
+
}
|
|
97
|
+
else if (typeof who === 'string' && cross_1.PERM_KEYS.includes(who))
|
|
98
|
+
keys.add(who);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function reportBlocked(blocked) {
|
|
102
|
+
// a deletion error must not disclose names hidden by access permissions
|
|
103
|
+
let visible = blocked;
|
|
104
|
+
for (let ancestor = blocked; ancestor && ancestor !== node; ancestor = ancestor.parent)
|
|
105
|
+
if (!(0, vfs_1.hasPermission)(ancestor, 'can_see', ctx) || !await (0, vfs_1.urlToNode)(ancestor.vfsPath, ctx)
|
|
106
|
+
|| ancestor.parent && !(0, vfs_1.hasPermission)(ancestor.parent, 'can_list', ctx))
|
|
107
|
+
visible = ancestor.parent || node;
|
|
108
|
+
ctx.body = { uri: '/' + visible.vfsPath };
|
|
109
|
+
}
|
|
110
|
+
function* explicitNodes(parent) {
|
|
111
|
+
for (const child of parent.children || []) {
|
|
112
|
+
const evaluated = (0, vfs_1.applyParentToChild)(child, parent);
|
|
113
|
+
yield evaluated;
|
|
114
|
+
yield* explicitNodes(evaluated);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function hasRules(node, keys) {
|
|
119
|
+
return Boolean(node.children?.length || Object.values(node.masks || {}).some(mods => mods && (Array.from(keys).some(key => mods[key] !== undefined) || hasRules(mods, keys))));
|
|
120
|
+
}
|
package/src/events.js
CHANGED
|
@@ -56,6 +56,12 @@ class BetterEventEmitter {
|
|
|
56
56
|
anyListener(event) {
|
|
57
57
|
return Boolean(this.listeners.get(event)?.size);
|
|
58
58
|
}
|
|
59
|
+
hasOtherListeners(event, mine) {
|
|
60
|
+
for (const x of this.listeners.get(event) || [])
|
|
61
|
+
if (x !== mine)
|
|
62
|
+
return true;
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
59
65
|
emit(event, ...args) {
|
|
60
66
|
let cbs = this.listeners.get(event);
|
|
61
67
|
if (!cbs?.size)
|
package/src/index.js
CHANGED
|
@@ -48,15 +48,16 @@ exports.app = new koa_1.default({ keys });
|
|
|
48
48
|
exports.app.use(middlewares_1.sessionMiddleware)
|
|
49
49
|
.use(selfCheck_1.selfCheckMiddleware)
|
|
50
50
|
.use(acme_1.acmeMiddleware)
|
|
51
|
-
.use(middlewares_1.
|
|
51
|
+
.use(middlewares_1.prepareConnection)
|
|
52
|
+
.use(middlewares_1.blockFilter) // denied clients must not reach credential verification or its session/account side effects
|
|
52
53
|
.use(geo_1.geoFilter)
|
|
54
|
+
.use(middlewares_1.prepareState)
|
|
53
55
|
.use(ips_1.trackIpsMw)
|
|
54
56
|
.use(middlewares_1.gzipper)
|
|
55
57
|
.use(roots_1.rootsHostGuard)
|
|
56
58
|
.use(log_1.logMw)
|
|
57
59
|
.use(middlewares_1.someSecurity)
|
|
58
60
|
.use(middlewares_1.paramsDecoder) // must be done before plugins, so they can manipulate params
|
|
59
|
-
.use(middlewares_1.headRequests)
|
|
60
61
|
.use(roots_1.rootsMiddleware)
|
|
61
62
|
.use(throttler_1.throttler)
|
|
62
63
|
.use(plugins_1.pluginsMiddleware)
|
package/src/lang.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.normalizeLangCode = normalizeLangCode;
|
|
7
|
+
exports.langCodeForLookup = langCodeForLookup;
|
|
7
8
|
exports.code2file = code2file;
|
|
8
9
|
exports.file2code = file2code;
|
|
9
10
|
exports.getLangData = getLangData;
|
|
@@ -22,6 +23,9 @@ function normalizeLangCode(code) {
|
|
|
22
23
|
code = code.toLowerCase().split(';', 1)[0].trim();
|
|
23
24
|
return /^[a-z]{2,3}(-[a-z0-9]{2,8})*$/.test(code) ? code : '';
|
|
24
25
|
}
|
|
26
|
+
function langCodeForLookup(code) {
|
|
27
|
+
return ['zh-tw', 'zh-hk', 'zh-mo'].includes(code) ? 'zh-hant' : code;
|
|
28
|
+
}
|
|
25
29
|
function code2file(code, admin = false) {
|
|
26
30
|
return (admin ? 'hfs-admin-lang-' : 'hfs-lang-') + code.toLowerCase() + SUFFIX;
|
|
27
31
|
}
|
|
@@ -39,7 +43,7 @@ async function getLangData(ctxOrLangCsv) {
|
|
|
39
43
|
}
|
|
40
44
|
const csv = ctxOrLangCsv.toLowerCase();
|
|
41
45
|
return cache.try(csv, async () => {
|
|
42
|
-
const langs = csv.split(',').map(normalizeLangCode).filter(Boolean);
|
|
46
|
+
const langs = csv.split(',').map(normalizeLangCode).map(langCodeForLookup).filter(Boolean);
|
|
43
47
|
if (!langs.includes(const_1.EMBEDDED_LANGUAGE))
|
|
44
48
|
langs.push(const_1.EMBEDDED_LANGUAGE);
|
|
45
49
|
const ret = {};
|
|
@@ -76,6 +80,7 @@ async function getLangData(ctxOrLangCsv) {
|
|
|
76
80
|
let forceLangData;
|
|
77
81
|
let undo;
|
|
78
82
|
(0, config_1.defineConfig)(misc_1.CFG.force_lang, '', v => {
|
|
83
|
+
v = langCodeForLookup(v);
|
|
79
84
|
undo?.();
|
|
80
85
|
if (!v)
|
|
81
86
|
return forceLangData = undefined;
|
package/src/langs/embedded.js
CHANGED
|
@@ -12,7 +12,7 @@ const hfs_lang_sr_json_1 = __importDefault(require("./hfs-lang-sr.json"));
|
|
|
12
12
|
const hfs_lang_sr_latn_json_1 = __importDefault(require("./hfs-lang-sr-latn.json"));
|
|
13
13
|
const hfs_lang_ko_json_1 = __importDefault(require("./hfs-lang-ko.json"));
|
|
14
14
|
const hfs_lang_ms_json_1 = __importDefault(require("./hfs-lang-ms.json"));
|
|
15
|
-
const
|
|
15
|
+
const hfs_lang_zh_hant_json_1 = __importDefault(require("./hfs-lang-zh-hant.json"));
|
|
16
16
|
const hfs_lang_fr_json_1 = __importDefault(require("./hfs-lang-fr.json"));
|
|
17
17
|
const hfs_lang_pl_json_1 = __importDefault(require("./hfs-lang-pl.json"));
|
|
18
18
|
const hfs_lang_pt_br_json_1 = __importDefault(require("./hfs-lang-pt-br.json"));
|
|
@@ -30,4 +30,4 @@ const hfs_lang_uk_json_1 = __importDefault(require("./hfs-lang-uk.json"));
|
|
|
30
30
|
const hfs_lang_ar_json_1 = __importDefault(require("./hfs-lang-ar.json"));
|
|
31
31
|
const hfs_lang_lt_json_1 = __importDefault(require("./hfs-lang-lt.json"));
|
|
32
32
|
const hfs_lang_bg_json_1 = __importDefault(require("./hfs-lang-bg.json"));
|
|
33
|
-
exports.default = { en: hfs_lang_en_json_1.default, it: hfs_lang_it_json_1.default, zh: hfs_lang_zh_json_1.default, ru: hfs_lang_ru_json_1.default, sr: hfs_lang_sr_json_1.default, 'sr-latn': hfs_lang_sr_latn_json_1.default, ko: hfs_lang_ko_json_1.default, ms: hfs_lang_ms_json_1.default, 'zh-
|
|
33
|
+
exports.default = { en: hfs_lang_en_json_1.default, it: hfs_lang_it_json_1.default, zh: hfs_lang_zh_json_1.default, ru: hfs_lang_ru_json_1.default, sr: hfs_lang_sr_json_1.default, 'sr-latn': hfs_lang_sr_latn_json_1.default, ko: hfs_lang_ko_json_1.default, ms: hfs_lang_ms_json_1.default, 'zh-hant': hfs_lang_zh_hant_json_1.default, fr: hfs_lang_fr_json_1.default, pl: hfs_lang_pl_json_1.default, 'pt-br': hfs_lang_pt_br_json_1.default, vi: hfs_lang_vi_json_1.default, es: hfs_lang_es_json_1.default, nl: hfs_lang_nl_json_1.default, el: hfs_lang_el_json_1.default, de: hfs_lang_de_json_1.default, fi: hfs_lang_fi_json_1.default, hu: hfs_lang_hu_json_1.default, ja: hfs_lang_ja_json_1.default, ro: hfs_lang_ro_json_1.default, tr: hfs_lang_tr_json_1.default, th: hfs_lang_th_json_1.default, uk: hfs_lang_uk_json_1.default, ar: hfs_lang_ar_json_1.default, lt: hfs_lang_lt_json_1.default, bg: hfs_lang_bg_json_1.default };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"author": "Hussein M. Alghazal (hmg3099)",
|
|
3
3
|
"version": 1.0,
|
|
4
|
-
"hfs_version": "0.57.5",
|
|
5
4
|
"translate": {
|
|
6
5
|
"Select": "اختر",
|
|
7
6
|
"n_files": "{n,plural,one{# file} other{# files}}",
|
|
@@ -111,6 +110,8 @@
|
|
|
111
110
|
"Timestamp": "الطابع الزمني",
|
|
112
111
|
"Show": "عرض",
|
|
113
112
|
"Loading failed": "فشل التحميل",
|
|
113
|
+
"Download limit reached": "تم بلوغ حد التنزيلات",
|
|
114
|
+
"Retry": "إعادة المحاولة",
|
|
114
115
|
"Rename": "إعادة تسمية",
|
|
115
116
|
"Tiles mode:": "وضع الشبكة:",
|
|
116
117
|
"off": "إيقاف",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"author": "Ангел Трънгалов",
|
|
3
3
|
"version": 1.0,
|
|
4
|
-
"hfs_version": "0.57.5",
|
|
5
4
|
"translate": {
|
|
6
5
|
"Select": "Избери",
|
|
7
6
|
"n_files": "{n,plural,one{# файл} other{# файлове}}",
|
|
@@ -109,6 +108,8 @@
|
|
|
109
108
|
"Timestamp": "Печат на време",
|
|
110
109
|
"Show": "Покажи",
|
|
111
110
|
"Loading failed": "Зареждането неуспешно",
|
|
111
|
+
"Download limit reached": "Достигнат е лимитът за изтегляния",
|
|
112
|
+
"Retry": "Опитай отново",
|
|
112
113
|
"Rename": "Преименувай",
|
|
113
114
|
"Tiles mode:": "Режим плочки:",
|
|
114
115
|
"off": "изкл.",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"author": "Uwe Wennmann",
|
|
3
3
|
"version": 0.1,
|
|
4
|
-
"hfs_version": "0.57.5",
|
|
5
4
|
"translate": {
|
|
6
5
|
"Select": "Auswählen",
|
|
7
6
|
"n_files": "{n,plural,one{# Datei} other{# Dateien}}",
|
|
@@ -111,6 +110,8 @@
|
|
|
111
110
|
"Timestamp": "Zeitstempel",
|
|
112
111
|
"Show": "Anzeigen",
|
|
113
112
|
"Loading failed": "Laden fehlgeschlagen",
|
|
113
|
+
"Download limit reached": "Download-Limit erreicht",
|
|
114
|
+
"Retry": "Erneut versuchen",
|
|
114
115
|
"Rename": "Umbenennen",
|
|
115
116
|
"Tiles mode:": "Kachel-Modus:",
|
|
116
117
|
"off": "aus",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"author": "Konstantinos (konmaz)",
|
|
3
3
|
"version": 1.0,
|
|
4
|
-
"hfs_version": "0.57.5",
|
|
5
4
|
"translate": {
|
|
6
5
|
"Select": "Επιλογή",
|
|
7
6
|
"n_files": "{n,plural,one{# αρχείο} other{# αρχεία}}",
|
|
@@ -111,6 +110,8 @@
|
|
|
111
110
|
"Timestamp": "Χρονική σήμανση",
|
|
112
111
|
"Show": "Εμφάνιση",
|
|
113
112
|
"Loading failed": "Η φόρτωση απέτυχε",
|
|
113
|
+
"Download limit reached": "Συμπληρώθηκε το όριο λήψεων",
|
|
114
|
+
"Retry": "Δοκιμή ξανά",
|
|
114
115
|
"Rename": "Μετονομασία",
|
|
115
116
|
"Tiles mode:": "Λειτουργία πλακιδίων:",
|
|
116
117
|
"off": "ανενεργό",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"author": "NAME OF THE AUTHOR(S) OF THIS TRANSLATION",
|
|
3
3
|
"version": 1.0,
|
|
4
|
-
"hfs_version": "0.57.5",
|
|
5
4
|
"translate": {
|
|
6
5
|
"Select": "Select",
|
|
7
6
|
"n_files": "{n,plural,one{# file} other{# files}}",
|
|
@@ -112,6 +111,8 @@
|
|
|
112
111
|
"Timestamp": "Timestamp",
|
|
113
112
|
"Show": "Show",
|
|
114
113
|
"Loading failed": "Loading failed",
|
|
114
|
+
"Download limit reached": "Download limit reached",
|
|
115
|
+
"Retry": "Retry",
|
|
115
116
|
"Rename": "Rename",
|
|
116
117
|
"Tiles mode:": "Tiles mode:",
|
|
117
118
|
"off": "off",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"author": "Eidy Estupiñan Varona, Arturo Fernández Rivas",
|
|
3
3
|
"version": 1.3,
|
|
4
|
-
"hfs_version": "0.57.5",
|
|
5
4
|
"translate": {
|
|
6
5
|
"Select": "Seleccionar",
|
|
7
6
|
"n_files": "{n,plural,one{# archivo} other {# archivos}}",
|
|
@@ -111,6 +110,8 @@
|
|
|
111
110
|
"Timestamp": "Fecha - Hora",
|
|
112
111
|
"Show": "Visualizar",
|
|
113
112
|
"Loading failed": "Carga fallida",
|
|
113
|
+
"Download limit reached": "Límite de descargas alcanzado",
|
|
114
|
+
"Retry": "Reintentar",
|
|
114
115
|
"Rename": "Renombrar",
|
|
115
116
|
"Tiles mode:": "Cantidad elementos",
|
|
116
117
|
"off": "Apagado",
|