Skip to content

Commit

Permalink
1.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
ovx committed Jul 15, 2024
1 parent 6310bac commit 671f351
Show file tree
Hide file tree
Showing 9 changed files with 80 additions and 65 deletions.
Binary file added .wordpress-org/screenshot-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .wordpress-org/screenshot-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .wordpress-org/screenshot-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@ ALTCHA offers a free, open-source Captcha alternative, ensuring robust spam prot
Read more about ALTCHA: https://github.com/altcha-org/altcha

Website: https://altcha.org
WordPress Plugin Directory: https://wordpress.org/plugins/altcha-spam-protection/

Having troubles? Please report in [Issues](https://github.com/altcha-org/wordpress-plugin/issues).

## WordPress Plugin Directory Status

The plugin has been submitted to the directory, it's currently under review.

## Supported Integrations

Expand All @@ -35,6 +33,8 @@ Currently the Floating UI does not work with:

## Installation

In your WordPress installation, search for "altcha" in the plugin directory and click Install. Alternatively, install the plugin manually:

1. Download the `.zip` from the [Releases](https://github.com/altcha-org/wordpress-plugin/releases).
2. Upload `altcha` folder to the `/wp-content/plugins/` directory
3. Activate the plugin through the 'Plugins' menu in WordPress
Expand Down
22 changes: 18 additions & 4 deletions admin/options.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,16 @@ function altcha_options_page_html()

<div>
<div style="margin-bottom: 0.3rem;"><b>Do you like ALTCHA?</b></div>
<div>
<a href="https://github.com/altcha-org/altcha" target="_blank" style="display: inline-flex; gap: 0.3rem;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#FFCC00" width="18" height="18"><path d="M12.0006 18.26L4.94715 22.2082L6.52248 14.2799L0.587891 8.7918L8.61493 7.84006L12.0006 0.5L15.3862 7.84006L23.4132 8.7918L17.4787 14.2799L19.054 22.2082L12.0006 18.26Z"></path></svg>
<span>Star it on GitHub!</span>
<div style="display:flex;gap: 0.5rem;">
<a href="https://wordpress.org/support/plugin/altcha-spam-protection/reviews/?filter=5#new-post" target="_blank" style="display: inline-flex; gap: 0.5rem;">
<span style="display: inline-flex; gap: 0.1rem;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#FFCC00" width="18" height="18"><path d="M12.0006 18.26L4.94715 22.2082L6.52248 14.2799L0.587891 8.7918L8.61493 7.84006L12.0006 0.5L15.3862 7.84006L23.4132 8.7918L17.4787 14.2799L19.054 22.2082L12.0006 18.26Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#FFCC00" width="18" height="18"><path d="M12.0006 18.26L4.94715 22.2082L6.52248 14.2799L0.587891 8.7918L8.61493 7.84006L12.0006 0.5L15.3862 7.84006L23.4132 8.7918L17.4787 14.2799L19.054 22.2082L12.0006 18.26Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#FFCC00" width="18" height="18"><path d="M12.0006 18.26L4.94715 22.2082L6.52248 14.2799L0.587891 8.7918L8.61493 7.84006L12.0006 0.5L15.3862 7.84006L23.4132 8.7918L17.4787 14.2799L19.054 22.2082L12.0006 18.26Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#FFCC00" width="18" height="18"><path d="M12.0006 18.26L4.94715 22.2082L6.52248 14.2799L0.587891 8.7918L8.61493 7.84006L12.0006 0.5L15.3862 7.84006L23.4132 8.7918L17.4787 14.2799L19.054 22.2082L12.0006 18.26Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#FFCC00" width="18" height="18"><path d="M12.0006 18.26L4.94715 22.2082L6.52248 14.2799L0.587891 8.7918L8.61493 7.84006L12.0006 0.5L15.3862 7.84006L23.4132 8.7918L17.4787 14.2799L19.054 22.2082L12.0006 18.26Z"></path></svg>
</span>
<span>Review it!</span>
</a>
</div>
</div>
Expand All @@ -60,6 +66,14 @@ function altcha_options_page_html()

<div style="opacity: 0.8;">
<p>ALTCHA Spam Protection for WordPress, plugin version <?php echo esc_html(AltchaPlugin::$version) ?>, widget version <?php echo esc_html(AltchaPlugin::$widget_version) ?></p>
<p>
Please give ALTCHA a <a href="https://wordpress.org/support/plugin/altcha-spam-protection/reviews/?filter=5#new-post" target="_blank">★★★★★ rating</a> on WordPress.org to help us get the word out.
</p>
<p>
<a href="https://github.com/altcha-org/altcha" target="_blank" style="display: inline-flex; gap: 0.3rem;">
<span>Star ALTCHA on GitHub!</span>
</a>
</p>
</div>
</div>
<?php
Expand Down
8 changes: 4 additions & 4 deletions altcha.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@
* Description: ALTCHA is a free, open-source CAPTCHA alternative that offers robust protection without using cookies, ensuring full GDPR compliance by design. It also provides invisible anti-spam and anti-bot protection through ALTCHA's API.
* Author: Altcha.org
* Author URI: https://altcha.org
* Version: 0.3.0
* Stable tag: 0.3.0
* Version: 1.0.0
* Stable tag: 1.0.0
* Requires at least: 5.0
* Requires PHP: 7.3
* Tested up to: 6.5
* License: GPLv2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
*/

define('ALTCHA_VERSION', '0.3.0');
define('ALTCHA_VERSION', '1.0.0');
define('ALTCHA_WEBSITE', 'https://altcha.org/');
define('ALTCHA_WIDGET_VERSION', '0.6.2');
define('ALTCHA_WIDGET_VERSION', '0.6.3');
define('ALTCHA_LANGUAGES', [
"bg" => "Bulgarian",
"ca" => "Catalan",
Expand Down
91 changes: 46 additions & 45 deletions public/altcha.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function Rt(r, e) {
function $(r, e, t) {
r.insertBefore(e, t || null);
}
function T(r) {
function Z(r) {
r.parentNode && r.parentNode.removeChild(r);
}
function A(r) {
Expand Down Expand Up @@ -252,7 +252,7 @@ function Wt(r, e, t, i, o, l, s = null, u = [-1]) {
}) : [], d.update(), U = !0, ae(d.before_update), d.fragment = i ? i(d.ctx) : !1, e.target) {
if (e.hydrate) {
const S = It(e.target);
d.fragment && d.fragment.l(S), S.forEach(T);
d.fragment && d.fragment.l(S), S.forEach(Z);
} else
d.fragment && d.fragment.c();
e.intro && Dt(r.$$.fragment), Ut(r, e.target, e.anchor), v();
Expand Down Expand Up @@ -313,7 +313,7 @@ typeof HTMLElement == "function" && (ot = class extends HTMLElement {
$(d, s, U);
},
d: function(d) {
d && T(s);
d && Z(s);
}
};
};
Expand Down Expand Up @@ -543,7 +543,7 @@ function Oe(r) {
$(o, e, l), _(e, t), _(e, i);
},
d(o) {
o && T(e);
o && Z(e);
}
};
}
Expand All @@ -568,7 +568,7 @@ function qt(r) {
o[4] + "_checkbox") && c(e, "for", i);
},
d(o) {
o && T(e);
o && Z(e);
}
};
}
Expand All @@ -590,7 +590,7 @@ function Qt(r) {
i[11].verifying + "") && (e.innerHTML = t);
},
d(i) {
i && T(e);
i && Z(e);
}
};
}
Expand Down Expand Up @@ -626,7 +626,7 @@ function er(r) {
l[5]);
},
d(l) {
l && (T(e), T(i), T(o));
l && (Z(e), Z(i), Z(o));
}
};
}
Expand All @@ -646,7 +646,7 @@ function Ye(r) {
f[11].ariaLinkLabel) && c(t, "aria-label", u);
},
d(f) {
f && T(e);
f && Z(e);
}
};
}
Expand All @@ -670,7 +670,7 @@ function Pe(r) {
s === (s = l(f)) && u ? u.p(f, d) : (u.d(1), u = s(f), u && (u.c(), u.m(e, null)));
},
d(f) {
f && T(e), u.d();
f && Z(e), u.d();
}
};
}
Expand Down Expand Up @@ -703,7 +703,7 @@ function tr(r) {
);
},
d(i) {
i && T(e);
i && Z(e);
}
};
}
Expand Down Expand Up @@ -736,7 +736,7 @@ function rr(r) {
);
},
d(i) {
i && T(e);
i && Z(e);
}
};
}
Expand All @@ -758,7 +758,7 @@ function Ke(r) {
o[11].footer + "") && (t.innerHTML = i);
},
d(o) {
o && T(e);
o && Z(e);
}
};
}
Expand All @@ -773,7 +773,7 @@ function Je(r) {
},
p: ne,
d(t) {
t && T(e), r[36](null);
t && Z(e), r[36](null);
}
};
}
Expand Down Expand Up @@ -887,7 +887,7 @@ function nr(r) {
i: ne,
o: ne,
d(h) {
h && T(e), N && N.d(), j.d(), L && L.d(), R && R.d(), w && w.d(), z && z.d(), r[37](null), F = !1, ae(P);
h && Z(e), N && N.d(), j.d(), L && L.d(), R && R.d(), w && w.d(), z && z.d(), r[37](null), F = !1, ae(P);
}
};
}
Expand All @@ -905,22 +905,22 @@ function or(r, e, t) {
var Fe, Ge;
let i, o, l, { auto: s = void 0 } = e, { blockspam: u = void 0 } = e, { challengeurl: f = void 0 } = e, { challengejson: d = void 0 } = e, { debug: U = !1 } = e, { delay: S = 0 } = e, { expire: D = void 0 } = e, { floating: k = void 0 } = e, { floatinganchor: F = void 0 } = e, { floatingoffset: P = void 0 } = e, { hidefooter: N = !1 } = e, { hidelogo: Q = !1 } = e, { name: H = "altcha" } = e, { maxnumber: j = 1e6 } = e, { mockerror: L = !1 } = e, { refetchonexpire: R = !0 } = e, { spamfilter: w = !1 } = e, { strings: z = void 0 } = e, { test: h = !1 } = e, { verifyurl: y = void 0 } = e, { workers: ie = Math.min(16, navigator.hardwareConcurrency || 8) } = e, { workerurl: ve = void 0 } = e;
const we = Tt(), Re = ["SHA-256", "SHA-384", "SHA-512"], ze = (Ge = (Fe = document.documentElement.lang) == null ? void 0 : Fe.split("-")) == null ? void 0 : Ge[0];
let O = !1, x, K = null, fe = null, m = null, ye = null, J = null, Z = b.UNVERIFIED, W = null;
let O = !1, x, K = null, fe = null, m = null, ye = null, J = null, M = b.UNVERIFIED, W = null;
jt(() => {
m && (m.removeEventListener("submit", Ae), m.removeEventListener("reset", Ne), m.removeEventListener("focusin", Ie), m = null), W && (clearTimeout(W), W = null), document.removeEventListener("click", Ze), document.removeEventListener("scroll", Me), window.removeEventListener("resize", $e);
}), St(() => {
E("mounted", "0.6.2"), E("workers", ie), h && E("using test mode"), D && ue(D), s !== void 0 && E("auto", s), k !== void 0 && De(k), m = x.closest("form"), m && (m.addEventListener("submit", Ae, { capture: !0 }), m.addEventListener("reset", Ne), s === "onfocus" && m.addEventListener("focusin", Ie)), s === "onload" && q();
E("mounted", "0.6.3"), E("workers", ie), h && E("using test mode"), D && ue(D), s !== void 0 && E("auto", s), k !== void 0 && De(k), m = x.closest("form"), m && (m.addEventListener("submit", Ae, { capture: !0 }), m.addEventListener("reset", Ne), s === "onfocus" && m.addEventListener("focusin", Ie)), s === "onload" && q();
});
function E(...n) {
(U || n.some((a) => a instanceof Error)) && console[n[0] instanceof Error ? "error" : "log"]("ALTCHA", ...n);
}
function Ie(n) {
Z === b.UNVERIFIED && q();
M === b.UNVERIFIED && q();
}
function Ae(n) {
m && s === "onsubmit" && (Z === b.UNVERIFIED ? (n.preventDefault(), n.stopPropagation(), q().then(() => {
m && s === "onsubmit" && (M === b.UNVERIFIED ? (n.preventDefault(), n.stopPropagation(), q().then(() => {
m == null || m.requestSubmit();
})) : Z !== b.VERIFIED && (n.preventDefault(), n.stopPropagation(), Z === b.VERIFYING && Ve()));
})) : M !== b.VERIFIED && (n.preventDefault(), n.stopPropagation(), M === b.VERIFYING && Ve()));
}
function Ne() {
de();
Expand Down Expand Up @@ -967,7 +967,7 @@ function or(r, e, t) {
});
if (a.status !== 200)
throw new Error(`Server responded with ${a.status}.`);
const g = a.headers.get("Expires"), I = a.headers.get("X-Altcha-Config"), M = await a.json(), V = new URLSearchParams((n = M.salt.split("?")) == null ? void 0 : n[1]), X = V.get("expires") || V.get("expire");
const g = a.headers.get("Expires"), I = a.headers.get("X-Altcha-Config"), V = await a.json(), T = new URLSearchParams((n = V.salt.split("?")) == null ? void 0 : n[1]), X = T.get("expires") || T.get("expire");
if (X) {
const p = new Date(+X * 1e3), C = isNaN(p.getTime()) ? 0 : p.getTime() - Date.now();
C > 0 && ue(C);
Expand All @@ -986,11 +986,11 @@ function or(r, e, t) {
C > 0 && ue(C);
}
}
return M;
return V;
}
}
function Te() {
f && R && Z === b.VERIFIED ? q() : de(b.EXPIRED, l.expired);
f && R && M === b.VERIFIED ? q() : de(b.EXPIRED, l.expired);
}
async function at(n) {
let a = null;
Expand All @@ -1008,20 +1008,20 @@ function or(r, e, t) {
solution: await Kt(n.challenge, n.salt, n.algorithm, n.maxnumber || j).promise
};
}
async function ft(n, a, g, I = typeof h == "number" ? h : j, M = Math.ceil(ie)) {
const V = [];
if (M < 1)
async function ft(n, a, g, I = typeof h == "number" ? h : j, V = Math.ceil(ie)) {
const T = [];
if (V < 1)
throw new Error("Wrong number of workers configured.");
if (M > 16)
if (V > 16)
throw new Error("Too many workers. Max. 16 allowed workers.");
for (let C = 0; C < M; C++)
V.push(createAltchaWorker(ve));
const X = Math.ceil(I / M), p = await Promise.all(V.map((C, _e) => {
for (let C = 0; C < V; C++)
T.push(createAltchaWorker(ve));
const X = Math.ceil(I / V), p = await Promise.all(T.map((C, _e) => {
const oe = _e * X;
return new Promise((he) => {
C.addEventListener("message", (ge) => {
if (ge.data)
for (const le of V)
for (const le of T)
le !== C && le.postMessage({ type: "abort" });
he(ge.data);
}), C.postMessage({
Expand All @@ -1036,25 +1036,25 @@ function or(r, e, t) {
});
});
}));
for (const C of V)
for (const C of T)
C.terminate();
return p.find((C) => !!C) || null;
}
function ut() {
[b.UNVERIFIED, b.ERROR, b.EXPIRED].includes(Z) ? w && (m == null ? void 0 : m.reportValidity()) === !1 ? t(7, O = !1) : q() : t(7, O = !0);
[b.UNVERIFIED, b.ERROR, b.EXPIRED].includes(M) ? w && (m == null ? void 0 : m.reportValidity()) === !1 ? t(7, O = !1) : q() : t(7, O = !0);
}
function Ze(n) {
const a = n.target;
k && a && !x.contains(a) && Z === b.VERIFIED && t(8, x.style.display = "none", x);
k && a && !x.contains(a) && M === b.VERIFIED && t(8, x.style.display = "none", x);
}
function Me() {
k && pe();
}
function Ve() {
Z === b.VERIFYING && l.waitAlert && alert(l.waitAlert);
M === b.VERIFYING && l.waitAlert && alert(l.waitAlert);
}
function dt(n) {
k && Z !== b.UNVERIFIED && requestAnimationFrame(() => {
k && M !== b.UNVERIFIED && requestAnimationFrame(() => {
pe();
});
}
Expand All @@ -1077,8 +1077,9 @@ function or(r, e, t) {
...(m == null ? void 0 : m.querySelectorAll(n != null && n.length ? n.map((g) => `input[name="${g}"]`).join(", ") : 'input[type="text"]:not([data-no-spamfilter]), textarea:not([data-no-spamfilter])')) || []
].reduce(
(g, I) => {
const M = I.name, V = I.value.trim();
return M && V && (g[M] = V), g;
const V = I.name, T = I.value;
return V && T && (g[V] = /\n/.test(T) ? T.replace(new RegExp("(?<!\\r)\\n", "g"), `\r
`) : T), g;
},
{}
);
Expand All @@ -1089,7 +1090,7 @@ function or(r, e, t) {
E("requesting server verification from", y);
const a = { payload: n };
if (w) {
const { blockedCountries: M, classifier: V, disableRules: X, email: p, expectedLanguages: C, expectedCountries: _e, fields: oe, ipAddress: he, text: ge, timeZone: le } = typeof w == "object" ? w : {
const { blockedCountries: V, classifier: T, disableRules: X, email: p, expectedLanguages: C, expectedCountries: _e, fields: oe, ipAddress: he, text: ge, timeZone: le } = typeof w == "object" ? w : {
blockedCountries: void 0,
classifier: void 0,
disableRules: void 0,
Expand All @@ -1101,7 +1102,7 @@ function or(r, e, t) {
text: void 0,
timeZone: void 0
};
a.blockedCountries = M, a.classifier = V, a.disableRules = X, a.email = p === !1 ? void 0 : ht(p), a.expectedCountries = _e, a.expectedLanguages = C || (ze ? [ze] : void 0), a.fields = oe === !1 ? void 0 : gt(oe), a.ipAddress = he === !1 ? void 0 : he || "auto", a.text = ge, a.timeZone = le === !1 ? void 0 : le || ir();
a.blockedCountries = V, a.classifier = T, a.disableRules = X, a.email = p === !1 ? void 0 : ht(p), a.expectedCountries = _e, a.expectedLanguages = C || (ze ? [ze] : void 0), a.fields = oe === !1 ? void 0 : gt(oe), a.ipAddress = he === !1 ? void 0 : he || "auto", a.text = ge, a.timeZone = le === !1 ? void 0 : le || ir();
}
const g = await fetch(y, {
body: JSON.stringify(a),
Expand All @@ -1117,7 +1118,7 @@ function or(r, e, t) {
function pe(n = 20) {
if (x)
if (fe || (fe = (F ? document.querySelector(F) : m == null ? void 0 : m.querySelector('input[type="submit"], button[type="submit"], button:not([type="button"]):not([type="reset"])')) || m), fe) {
const a = parseInt(P, 10) || 12, g = fe.getBoundingClientRect(), I = x.getBoundingClientRect(), M = document.documentElement.clientHeight, V = document.documentElement.clientWidth, X = k === "auto" ? g.bottom + I.height + a + n > M : k === "top", p = Math.max(n, Math.min(V - n - I.width, g.left + g.width / 2 - I.width / 2));
const a = parseInt(P, 10) || 12, g = fe.getBoundingClientRect(), I = x.getBoundingClientRect(), V = document.documentElement.clientHeight, T = document.documentElement.clientWidth, X = k === "auto" ? g.bottom + I.height + a + n > V : k === "top", p = Math.max(n, Math.min(T - n - I.width, g.left + g.width / 2 - I.width / 2));
if (X ? t(8, x.style.top = `${g.top - (I.height + a)}px`, x) : t(8, x.style.top = `${g.bottom + a}px`, x), t(8, x.style.left = `${p}px`, x), x.setAttribute("data-floating", X ? "top" : "bottom"), K) {
const C = K.getBoundingClientRect();
t(9, K.style.left = g.left - p + g.width / 2 - C.width / 2 + "px", K);
Expand All @@ -1129,7 +1130,7 @@ function or(r, e, t) {
n.auto !== void 0 && (t(0, s = n.auto), s === "onload" && q()), n.floatinganchor !== void 0 && t(18, F = n.floatinganchor), n.delay !== void 0 && t(16, S = n.delay), n.floatingoffset !== void 0 && t(19, P = n.floatingoffset), n.floating !== void 0 && De(n.floating), n.expire !== void 0 && (ue(n.expire), t(17, D = n.expire)), n.challenge && (je(n.challenge), i = n.challenge), n.challengeurl !== void 0 && t(14, f = n.challengeurl), n.debug !== void 0 && t(15, U = !!n.debug), n.hidefooter !== void 0 && t(2, N = !!n.hidefooter), n.hidelogo !== void 0 && t(3, Q = !!n.hidelogo), n.maxnumber !== void 0 && t(20, j = +n.maxnumber), n.mockerror !== void 0 && t(21, L = !!n.mockerror), n.name !== void 0 && t(4, H = n.name), n.refetchonexpire !== void 0 && t(22, R = !!n.refetchonexpire), n.spamfilter !== void 0 && t(23, w = typeof n.spamfilter == "object" ? n.spamfilter : !!n.spamfilter), n.strings && t(34, o = n.strings), n.test !== void 0 && t(24, h = typeof n.test == "number" ? n.test : !!n.test), n.verifyurl !== void 0 && t(25, y = n.verifyurl), n.workers !== void 0 && t(26, ie = +n.workers);
}
function de(n = b.UNVERIFIED, a = null) {
W && (clearTimeout(W), W = null), t(7, O = !1), t(10, ye = a), t(5, J = null), t(6, Z = n);
W && (clearTimeout(W), W = null), t(7, O = !1), t(10, ye = a), t(5, J = null), t(6, M = n);
}
async function q() {
return de(b.VERIFYING), await new Promise((n) => setTimeout(n, S || 0)), ct().then((n) => (je(n), E("challenge", n), at(n))).then(({ data: n, solution: a }) => {
Expand All @@ -1141,10 +1142,10 @@ function or(r, e, t) {
throw E("Unable to find a solution. Ensure that the 'maxnumber' attribute is greater than the randomly generated number."), new Error("Unexpected result returned.");
}).then(() => {
Zt().then(() => {
t(6, Z = b.VERIFIED), t(7, O = !0), E("verified"), we("verified", { payload: J });
t(6, M = b.VERIFIED), t(7, O = !0), E("verified"), we("verified", { payload: J });
});
}).catch((n) => {
E(n), t(6, Z = b.ERROR), t(7, O = !1), t(10, ye = n.message);
E(n), t(6, M = b.ERROR), t(7, O = !1), t(10, ye = n.message);
});
}
function bt() {
Expand Down Expand Up @@ -1177,7 +1178,7 @@ function or(r, e, t) {
waitAlert: "Verifying... please wait.",
...o
}), r.$$.dirty[0] & /*payload, state*/
96 && we("statechange", { payload: J, state: Z }), r.$$.dirty[0] & /*state*/
96 && we("statechange", { payload: J, state: M }), r.$$.dirty[0] & /*state*/
64 && dt();
}, [
s,
Expand All @@ -1186,7 +1187,7 @@ function or(r, e, t) {
Q,
H,
J,
Z,
M,
O,
x,
K,
Expand Down
Loading

0 comments on commit 671f351

Please sign in to comment.