diff --git a/README.rst b/README.rst index 3a7fc47..7442d24 100644 --- a/README.rst +++ b/README.rst @@ -72,6 +72,14 @@ What is Remarkbox? ------------------ Remarkbox is a standalone question and answer site (forum) or an embedded comments/product reviews service that works anywhere HTML is supported. +Features +-------- +- **Dark Mode Support:** User-configurable theme preferences with automatic theme detection for embedded contexts +- **Passwordless Authentication:** One-time-password codes via email for secure registration and login +- **Multi-tenant Architecture:** Host multiple forums and comment systems on a single installation +- **Customizable Themes:** Plugin-based theme system supporting custom branding and styling +- **Embed Anywhere:** Works with static sites, WordPress, or any platform that supports HTML + Project Goals ============================================== diff --git a/index.html b/index.html index d413a4b..564dcab 100644 --- a/index.html +++ b/index.html @@ -57,11 +57,11 @@ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem var thread_title = window.document.title; var thread_fragment = window.location.hash; - var rb_src = "http://127.0.0.1:6543/embed" + "?rb_owner_key=" + rb_owner_key + "&thread_title=" + escape(thread_title) + "&thread_uri=" + encodeURIComponent(thread_uri) + + "&mode=light" + thread_fragment; function create_remarkbox_iframe() { diff --git a/index2.html b/index2.html index 5299e4b..0b60e1e 100644 --- a/index2.html +++ b/index2.html @@ -60,7 +60,7 @@ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem var thread_fragment = window.location.hash; var email = "russell.ballestrini@gmail.com"; function create_remarkbox_iframe() { - var src = "http://127.0.0.1:6543/embed?rb_owner_key=" + rb_owner_key + "&email=" + email + "&thread_uri=" + thread_uri; + var src = "http://127.0.0.1:6543/embed?rb_owner_key=" + rb_owner_key + "&email=" + email + "&thread_uri=" + thread_uri + "&mode=light"; var ifrm = document.createElement("iframe"); ifrm.setAttribute("id", "remarkbox-iframe"); ifrm.setAttribute("scrolling", "no"); @@ -86,5 +86,4 @@ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem - diff --git a/remarkbox/__init__.py b/remarkbox/__init__.py index ab9811b..bb4002a 100644 --- a/remarkbox/__init__.py +++ b/remarkbox/__init__.py @@ -28,9 +28,6 @@ import re # needed to load themes. from pkg_resources import iter_entry_points -# needed to support expanding ENV vars from ini. -from os.path import expandvars - import logging log = logging.getLogger(__name__) @@ -43,18 +40,47 @@ def get_int_or_bool_or_none_or_str(value): Given a string value pulled from a configuration file, this function attempts to return the value with the proper type. """ + # Handle non-string values + if not isinstance(value, str): + return value + + # Handle string values try: return int(value) except ValueError: - if value.lower() in {"yes", "y", "true", "y"}: + value_lower = value.lower() + if value_lower in {"yes", "y", "true", "t", "1"}: return True - elif value.lower() in {"no", "n", "false", "f"}: + elif value_lower in {"no", "n", "false", "f", "0"}: return False - elif value.lower() == "none": + elif value_lower in {"none", "null"}: return None return str(value) +def expand_env_vars(value): + """Expand environment variables including ${VAR:-default} syntax.""" + if not isinstance(value, str): + return value + + import os + import re + + # Handle ${VAR:-default} syntax + # Use a non-greedy match to stop at the first closing brace + pattern = r"\$\{([^:}]*)(?::-([^}]*?))?\}" + + def replacer(match): + var_name = match.group(1) + # Handle empty variable name case ${:-default} + if not var_name: + return match.group(2) if match.group(2) is not None else match.group(0) + default_value = match.group(2) if match.group(2) is not None else "" + return os.environ.get(var_name, default_value) + + return re.sub(pattern, replacer, value) + + def get_children_settings(settings, parent_key): """ Accept a settings dict and parent key, return dict of children @@ -73,18 +99,14 @@ def get_children_settings(settings, parent_key): {'hashalg': 'md5'} """ - # needed to support expanding ENV vars from ini. - from os.path import expandvars - # the +1 is the . between parent and child settings. parent_len = len(parent_key) + 1 children = {} for key, value in settings.items(): if parent_key in key: - # expandvars replaces template with ENV vars. - children[key[parent_len:]] = get_int_or_bool_or_none_or_str( - expandvars(value) - ) + # Expand environment variables with support for defaults + expanded_value = expand_env_vars(value) + children[key[parent_len:]] = get_int_or_bool_or_none_or_str(expanded_value) return children @@ -155,6 +177,11 @@ def maybe_root_domain(string): def main(global_config, **settings): """This function returns a Pyramid WSGI application.""" + # Expand environment variables in all settings using our custom function + for key, value in list(settings.items()): + if isinstance(value, str): + settings[key] = expand_env_vars(value) + app_settings = get_children_settings(settings, "app") session_settings = get_children_settings(settings, "session") @@ -175,7 +202,18 @@ def main(global_config, **settings): session_settings["domain"] = root_domain factory = SignedCookieSessionFactory(**session_settings) - return factory(request) + session = factory(request) + + # Log session contents and size on every request + import pickle + session_dict = dict(session) + serialized = pickle.dumps(session_dict) + log.info(f"Session total size: {len(serialized)} bytes, keys: {list(session_dict.keys())}") + for key, value in session_dict.items(): + item_size = len(pickle.dumps({key: value})) + log.info(f" Session['{key}'] = {item_size} bytes (type: {type(value).__name__})") + + return session # setup session factory to use unencrypted but signed cookies. # session_factory = SignedCookieSessionFactory(**session_settings) diff --git a/remarkbox/lib/mail.py b/remarkbox/lib/mail.py index fe033ec..1589cf4 100644 --- a/remarkbox/lib/mail.py +++ b/remarkbox/lib/mail.py @@ -187,12 +187,12 @@ def send_verification_digits_to_email(request, to_email, raw_digits): message_text = WELCOME_1_TEXT.format(raw_digits) message_html = WELCOME_1_HTML.format(subject, raw_digits) - if not request.user.verified: - message_text = WELCOME_1_TEXT.format(raw_digits) - message_html = WELCOME_1_HTML.format(subject, raw_digits) - else: + if request.user and request.user.verified: message_text = WELCOME_2_TEXT.format(raw_digits) message_html = WELCOME_2_HTML.format(subject, raw_digits) + else: + message_text = WELCOME_1_TEXT.format(raw_digits) + message_html = WELCOME_1_HTML.format(subject, raw_digits) send_pyramid_email(request, to_email, subject, message_text, message_html) diff --git a/remarkbox/models/meta.py b/remarkbox/models/meta.py index 843a92c..5a067a6 100644 --- a/remarkbox/models/meta.py +++ b/remarkbox/models/meta.py @@ -7,6 +7,7 @@ from sqlalchemy import ForeignKey from time import time +import base64 import uuid from sqlalchemy_utils import UUIDType as TempUUIDType @@ -75,10 +76,10 @@ def short_id_to_bytes(short_id): """Accept a short_id (sanitized url safe base64 string) and return a byte string. >>> short_id_to_bytes('dbHeSEFLEeeuz5xONpxxWA') - 'u\xb1\xdeHAK\x11\xe7\xae\xcf\x9cN6\x9cqX' + b'u\xb1\xdeHAK\x11\xe7\xae\xcf\x9cN6\x9cqX' """ - return (short_id + "===").replace("_", "/").replace("-", "+").decode("base64") + return base64.b64decode((short_id + "===").replace("_", "/").replace("-", "+")) def id_to_uuid(the_id): diff --git a/remarkbox/views/__init__.py b/remarkbox/views/__init__.py index 63cf930..c2ae93d 100644 --- a/remarkbox/views/__init__.py +++ b/remarkbox/views/__init__.py @@ -113,7 +113,11 @@ def nodes_pending_verify(request): def set_node_to_pending_in_session(request, node): """Update session to make node go into pending_verify state.""" - nodes_pending_verify(request).append(str(node.id_without_dashes)) + pending = nodes_pending_verify(request) + node_id = str(node.id_without_dashes) + # Only add if not already present and list isn't too large + if node_id not in pending and len(pending) < 50: + pending.append(node_id) def verify_pending_nodes_in_session(request, user):