349 lines
8.1 KiB
Python
349 lines
8.1 KiB
Python
import importlib
|
|
import pkgutil
|
|
import re
|
|
from engine.plugin import Plugin
|
|
from plugins.embed.providers.base import EmbedProvider
|
|
|
|
class EmbedConsentManager(Plugin):
|
|
name = "embed"
|
|
priority = 45
|
|
|
|
def __init__(self, config):
|
|
super().__init__(config)
|
|
self.providers = []
|
|
self.load_providers()
|
|
|
|
def load_providers(self):
|
|
package = "plugins.embed.providers"
|
|
pkg = importlib.import_module(package)
|
|
|
|
for _, name, _ in pkgutil.iter_modules(pkg.__path__):
|
|
module = importlib.import_module(f"{package}.{name}")
|
|
for attr in dir(module):
|
|
cls = getattr(module, attr)
|
|
if isinstance(cls, type) and issubclass(cls, EmbedProvider) and cls is not EmbedProvider:
|
|
if hasattr(cls, "pattern") and hasattr(cls, "name"):
|
|
provider_config = self.config.get(cls.name, {})
|
|
self.providers.append(cls(provider_config))
|
|
|
|
def after_markdown(self, builder, html):
|
|
for provider in self.providers:
|
|
html = provider.match(html)
|
|
return html
|
|
|
|
return super().after_markdown(builder, html)
|
|
def after_page_render(self, builder, page, content):
|
|
pattern = r"<\/body>"
|
|
content = re.sub(pattern, self.inject_assets() + "BLAAA</body>", content, 0, re.MULTILINE)
|
|
return content
|
|
|
|
def after_build(self, builder):
|
|
settings_path = builder.config.output_dir / "privacy-settings.html"
|
|
settings_path.write_text(self.generate_settings_page(), encoding="utf-8")
|
|
|
|
def inject_assets(self):
|
|
return self.css() + self.script()
|
|
|
|
def generate_settings_page(self):
|
|
|
|
cards = ""
|
|
|
|
for p in self.providers:
|
|
cards += f"""
|
|
<div class="provider-card">
|
|
<div>
|
|
<strong>{p.name.title()}</strong>
|
|
<div style="font-size:0.9rem;color:var(--muted);">
|
|
External content provider
|
|
</div>
|
|
</div>
|
|
|
|
<label class="switch">
|
|
<input type="checkbox"
|
|
onchange="toggleProvider('{p.name}', this)"
|
|
id="toggle-{p.name}">
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
"""
|
|
|
|
return f"""
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Privacy Settings</title>
|
|
{self.css()}
|
|
</head>
|
|
<body>
|
|
|
|
<div class="settings-container">
|
|
<h1>Privacy Settings</h1>
|
|
<p>Control which external providers may load content on this site.</p>
|
|
|
|
{cards}
|
|
|
|
<button class="btn-danger" onclick="revokeAll()">
|
|
Revoke All Permissions
|
|
</button>
|
|
|
|
</div>
|
|
|
|
{self.script()}
|
|
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function() {{
|
|
{''.join([f"""
|
|
if(localStorage.getItem("embed-provider-{p.name}") === "granted") {{
|
|
document.getElementById("toggle-{p.name}").checked = true;
|
|
}}
|
|
""" for p in self.providers])}
|
|
}});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
def css(self):
|
|
return """
|
|
<style>
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
|
}
|
|
|
|
/* ===== Embed Card ===== */
|
|
.embed-consent {
|
|
position: relative;
|
|
margin: 30px 0;
|
|
border-radius: 18px;
|
|
overflow: hidden;
|
|
backdrop-filter: blur(20px);
|
|
background: var(--card);
|
|
border: 1px solid rgba(255,255,255,0.2);
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.embed-overlay {
|
|
padding: 60px 25px;
|
|
text-align: center;
|
|
}
|
|
|
|
.embed-box h3 {
|
|
margin-bottom: 10px;
|
|
font-size: 1.2rem;
|
|
}
|
|
|
|
.embed-box p {
|
|
color: var(--muted);
|
|
}
|
|
|
|
.embed-accept {
|
|
margin-top: 20px;
|
|
padding: 12px 28px;
|
|
border-radius: 10px;
|
|
border: none;
|
|
background: var(--accent);
|
|
color: white;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.embed-accept:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 20px rgba(0,0,0,0.2);
|
|
}
|
|
|
|
/* ===== Spinner ===== */
|
|
.embed-spinner {
|
|
width: 50px;
|
|
height: 50px;
|
|
border-radius: 50%;
|
|
border: 5px solid rgba(255,255,255,0.2);
|
|
border-top: 5px solid var(--accent);
|
|
animation: spin 0.8s linear infinite;
|
|
margin: 80px auto;
|
|
}
|
|
|
|
@keyframes spin {
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* ===== Settings Page ===== */
|
|
.settings-container {
|
|
max-width: 800px;
|
|
margin: 80px auto;
|
|
padding: 40px;
|
|
}
|
|
|
|
.settings-container h1 {
|
|
font-size: 2rem;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.settings-container p {
|
|
color: var(--muted);
|
|
margin-bottom: 40px;
|
|
}
|
|
|
|
/* Provider Card */
|
|
.provider-card {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
border-radius: 16px;
|
|
backdrop-filter: blur(20px);
|
|
background: var(--card);
|
|
border: 1px solid rgba(255,255,255,0.2);
|
|
transition: 0.2s ease;
|
|
}
|
|
|
|
.provider-card:hover {
|
|
transform: translateY(-3px);
|
|
}
|
|
|
|
/* Toggle Switch */
|
|
.switch {
|
|
position: relative;
|
|
width: 52px;
|
|
height: 28px;
|
|
}
|
|
|
|
.switch input {
|
|
display: none;
|
|
}
|
|
|
|
.slider {
|
|
position: absolute;
|
|
cursor: pointer;
|
|
background: #ccc;
|
|
border-radius: 50px;
|
|
inset: 0;
|
|
transition: 0.3s;
|
|
}
|
|
|
|
.slider:before {
|
|
content: "";
|
|
position: absolute;
|
|
height: 22px;
|
|
width: 22px;
|
|
left: 3px;
|
|
bottom: 3px;
|
|
background: white;
|
|
border-radius: 50%;
|
|
transition: 0.3s;
|
|
}
|
|
|
|
input:checked + .slider {
|
|
background: var(--accent);
|
|
}
|
|
|
|
input:checked + .slider:before {
|
|
transform: translateX(24px);
|
|
}
|
|
|
|
/* Buttons */
|
|
.btn-danger {
|
|
margin-top: 30px;
|
|
padding: 12px 25px;
|
|
border-radius: 10px;
|
|
border: none;
|
|
background: var(--danger);
|
|
color: white;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
opacity: 0.9;
|
|
}
|
|
|
|
</style>
|
|
"""
|
|
|
|
|
|
# ---------------------------------
|
|
# JavaScript
|
|
# ---------------------------------
|
|
def script(self):
|
|
return """
|
|
<script>
|
|
|
|
function getState(provider) {
|
|
return localStorage.getItem("embed-provider-" + provider);
|
|
}
|
|
|
|
function setState(provider, state) {
|
|
localStorage.setItem("embed-provider-" + provider, state);
|
|
}
|
|
|
|
function acceptProvider(provider) {
|
|
setState(provider, "granted");
|
|
document.querySelectorAll('.embed-consent[data-provider="' + provider + '"]')
|
|
.forEach(loadEmbed);
|
|
}
|
|
|
|
function loadEmbed(container) {
|
|
const template = container.querySelector(".embed-template");
|
|
if (!template) return;
|
|
|
|
container.innerHTML = '<div class="embed-spinner"></div>';
|
|
|
|
setTimeout(() => {
|
|
const clone = template.content.cloneNode(true);
|
|
container.innerHTML = "";
|
|
container.appendChild(clone);
|
|
container.style.opacity = "0";
|
|
setTimeout(()=> container.style.opacity="1",50);
|
|
}, 600);
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", function(){
|
|
document.querySelectorAll(".embed-consent").forEach(container => {
|
|
const provider = container.dataset.provider;
|
|
if (getState(provider) === "granted") {
|
|
loadEmbed(container);
|
|
}
|
|
});
|
|
});
|
|
|
|
function toggleProvider(provider, checkbox) {
|
|
if (checkbox.checked) {
|
|
setState(provider, "granted");
|
|
} else {
|
|
setState(provider, "denied");
|
|
}
|
|
}
|
|
|
|
function revokeAll() {
|
|
Object.keys(localStorage).forEach(k=>{
|
|
if(k.startsWith("embed-provider-")) localStorage.removeItem(k);
|
|
});
|
|
alert("All provider consent revoked.");
|
|
}
|
|
</script>
|
|
"""
|
|
|
|
def revocation_script(self):
|
|
return '''
|
|
<script>
|
|
function revokeProvider(provider){
|
|
localStorage.removeItem("embed-consent-" + provider);
|
|
alert(provider + " consent revoked. Reloading.");
|
|
location.reload();
|
|
}
|
|
|
|
function revokeAll(){
|
|
Object.keys(localStorage).forEach(k=>{
|
|
if(k.startsWith("embed-consent-")) localStorage.removeItem(k);
|
|
});
|
|
alert("All embed consent revoked. Reloading.");
|
|
location.reload();
|
|
}
|
|
</script>
|
|
''' |