Bitcoin
+Send BTC directly to the project wallet.
+{{ donations.bitcoin_address }}
+
+ Always verify the full address in your wallet before sending.
+diff --git a/src/cmdforge/web/app.py b/src/cmdforge/web/app.py index ff0817f..5b258f1 100644 --- a/src/cmdforge/web/app.py +++ b/src/cmdforge/web/app.py @@ -29,6 +29,7 @@ from cmdforge.registry import app as registry_app from . import web_bp from .auth import login, register, logout +from .donations import load_donation_options from .filters import register_filters from .forum import forum_bp from .sessions import SQLiteSessionInterface, cleanup_expired_sessions @@ -48,6 +49,7 @@ def create_web_app() -> Flask: app.config["SESSION_COOKIE_NAME"] = "cmdforge_session" app.config["SESSION_COOKIE_SECURE"] = os.environ.get("CMDFORGE_ENV") == "production" app.config["SHOW_ADS"] = os.environ.get("CMDFORGE_SHOW_ADS", "").lower() == "true" + app.config["DONATION_OPTIONS"] = load_donation_options(os.environ) # CSRF token generator app.config["CSRF_GENERATOR"] = lambda: secrets.token_urlsafe(32) diff --git a/src/cmdforge/web/donations.py b/src/cmdforge/web/donations.py new file mode 100644 index 0000000..9812f9a --- /dev/null +++ b/src/cmdforge/web/donations.py @@ -0,0 +1,69 @@ +"""Validation and presentation helpers for public donation settings.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from typing import Mapping +from urllib.parse import urlencode, urlparse + + +_BASE58_BITCOIN_ADDRESS = re.compile(r"^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$") +_BECH32_BITCOIN_ADDRESS = re.compile(r"^bc1[ac-hj-np-z02-9]{11,71}$") +_PAYPAL_HOSTS = {"paypal.com", "www.paypal.com", "paypal.me", "www.paypal.me"} + + +@dataclass(frozen=True) +class DonationOptions: + """Public donation methods safe to render as links.""" + + bitcoin_address: str = "" + bitcoin_uri: str = "" + paypal_url: str = "" + + @property + def enabled(self) -> bool: + return bool(self.bitcoin_address or self.paypal_url) + + +def _valid_bitcoin_address(value: str) -> bool: + if _BASE58_BITCOIN_ADDRESS.fullmatch(value): + return True + # Bech32 addresses may be all lower- or uppercase, but never mixed case. + if value.lower() != value and value.upper() != value: + return False + return bool(_BECH32_BITCOIN_ADDRESS.fullmatch(value.lower())) + + +def _valid_paypal_url(value: str) -> bool: + parsed = urlparse(value) + return ( + parsed.scheme == "https" + and parsed.hostname is not None + and parsed.hostname.lower() in _PAYPAL_HOSTS + and not parsed.username + and not parsed.password + ) + + +def load_donation_options(environment: Mapping[str, str]) -> DonationOptions: + """Load public donation details, ignoring malformed environment values.""" + + bitcoin_address = environment.get("CMDFORGE_DONATION_BITCOIN_ADDRESS", "").strip() + if not _valid_bitcoin_address(bitcoin_address): + bitcoin_address = "" + + paypal_url = environment.get("CMDFORGE_DONATION_PAYPAL_URL", "").strip() + if not _valid_paypal_url(paypal_url): + paypal_url = "" + + bitcoin_uri = "" + if bitcoin_address: + query = urlencode({"label": "CmdForge", "message": "Support CmdForge development"}) + bitcoin_uri = f"bitcoin:{bitcoin_address}?{query}" + + return DonationOptions( + bitcoin_address=bitcoin_address, + bitcoin_uri=bitcoin_uri, + paypal_url=paypal_url, + ) diff --git a/src/cmdforge/web/routes.py b/src/cmdforge/web/routes.py index 4225c8a..ab47ea5 100644 --- a/src/cmdforge/web/routes.py +++ b/src/cmdforge/web/routes.py @@ -876,7 +876,10 @@ def philosophy(): @web_bp.route("/donate", endpoint="donate") def donate(): - return render_template("pages/donate.html") + return render_template( + "pages/donate.html", + donations=current_app.config["DONATION_OPTIONS"], + ) @web_bp.route("/privacy", endpoint="privacy") diff --git a/src/cmdforge/web/templates/pages/donate.html b/src/cmdforge/web/templates/pages/donate.html index c0fdd16..d418077 100644 --- a/src/cmdforge/web/templates/pages/donate.html +++ b/src/cmdforge/web/templates/pages/donate.html @@ -2,7 +2,7 @@ {% block title %}Support CmdForge{% endblock %} -{% block meta_description %}Support CmdForge by contributing code, tools, or feedback.{% endblock %} +{% block meta_description %}Support the open-source CmdForge project with Bitcoin, PayPal, tools, code, or feedback.{% endblock %} {% block content %}
- CmdForge is a passion project. The best way to support it is to use it and share what you build. + Help keep an independent, open-source toolkit growing. Every contribution supports hosting, + maintenance, documentation, and the next generation of tools.
+ Choose the method that works for you. Donations are voluntary and do not unlock product features. +
+Send BTC directly to the project wallet.
+{{ donations.bitcoin_address }}
+
+ Always verify the full address in your wallet before sending.
+Contribute with PayPal or a supported card.
++ PayPal handles the payment securely on its own website. CmdForge never receives your payment credentials. +
+ + Donate with PayPal + +Money is never the only way to make a meaningful contribution.
+