# Perl to Python Module Map
This document provides a cross-reference for developers familiar with the original Perl codebase, mapping old module locations to their new equivalents in the Python framework.
| Old Perl Module | New Python Location(s) |
| :--- | :--- |
| `Slash::Apache::Log` | `slash/auditing.py`<br>`slash/middleware/auditing.py` |
| `Slash::Apache::TemplatePages` | `slash/template_pages.py`<br>`main.py` (route registration) |
| `Slash::Apache::User` | `slash/middleware/user_auth.py`<br>`slash/cookies.py`<br>`slash/user_urls.py`<br>`slash/auth.py` |
| `Slash::Apache::User::PasswordSalt` | `slash/security/passwords.py`<br>`config/settings.py` |
| `Slash::Constants` | `slash/constants.py` |
| `Slash::Custom::ApacheCompress` | `slash/middleware/compression.py` |
| `Slash::Display` | `slash/rendering.py`<br>`slash/templating/loader.py`<br>`slash/display.py` |
| `Slash::Display::Provider` | `slash/templating/loader.py`<br>`slash/templating/context.py`<br>`temp_templates/` (template storage) |
| `Slash::Display::Plugin` | `slash/templating/slash_plugin.py`<br>`slash/rendering.py` |
| `Slash::Hook` | `slash/hooks.py` |
| `Slash::Hook::Sample` | `slash/sample_hooks.py` |
| `Slash::Plugin` | `slash/plugins.py` |
| `Slash::XML` | `slash/xml.py` |
| `Slash::Utility::Data` | `slash/sanitizer.py`<br>`slash/urls.py`<br>`slash/formatting.py`<br>`slash/validation.py`<br>`slash/text_utils.py`<br>`slash/xml.py`<br>`slash/security/tokens.py`<br>`slash/security/passwords.py`<br>`slash/templating/filters.py` |
| `Slash::Utility::Environment` | `slash/context.py`<br>`slash/database.py`<br>`slash/skin_detection.py`<br>`slash/middleware/user_auth.py` |
| `Slash::Utility::Environment::determineCurrentSkin` | `slash/skin_detection.py::determine_current_skin` |
| `Slash::Utility::Environment::getCurrentSkin` | `slash/context.py::get_current_skin` |
| `Slash::Utility::Environment::getCurrentStatic` | `slash/context.py::get_current_static` |
| `Slash::Utility::Skin` | `slash/skins.py` |
| `Slash::Utility::User` (conceptual) | `slash/auth.py` |
| `Slash::Utility::Anchor` | `slash/responses.py`<br>`slash/http.py`<br>`slash/ads.py`<br>`slash/cms.py`<br>`slash/skins.py` |
| `Slash::Utility::Comments` | `slash/comments/manager.py`<br>`slash/comments/ui.py`<br>`slash/comments/scoring.py`<br>`slash/comments/validation.py`<br>`slash/comments/utils.py` |
| `Slash` | `slash/stories/presentation.py`<br>`slash/data_snippets.py` |
| `Slash::DB` | `slash/db.py`<br>`slash/database.py` |
| `Slash::DB::MySQL` | `slash.db_abstraction`<br>`slash.sqlite_implementation` (and future backend implementations) |
| `Slash::DB::MySQL::getBlock` | `slash/sqlite_implementation.py` (SQLiteManager.getBlock method) |
| `Slash::DB::MySQL::getSlashConf` | `slash/sqlite_implementation.py::get_slash_conf` |
| `Slash::DB::MySQL::getTemplateByName` | `slash/templating/loader.py::DatabaseTemplateLoader.get_source` |
| `Slash::DB::Static::MySQL` | `slash/db/admin.py`<br>`slash/stories/tasks.py`<br>`slash/tasks/cleanup.py`<br>`slash/auth/cleanup.py`<br>`slash/ip/cleanup.py`<br>`slash/tasks/daemon.py`<br>`slash/site_content.py`<br>`slash/polls.py`<br>`slash/tasks/freshen.py`<br>`slash/auth/tasks.py`<br>`slash/formkeys/cleanup.py`<br>`slash/topics.py`<br>`slash/analytics.py`<br>`slash/skins.py`<br>`slash/tasks/url_processing.py`<br>`slash/tasks/file_queue.py`<br>`slash/moderation.py`<br>`slash/comments/tasks.py`<br>`slash/emailing.py` |
| `sbin/slashd` | `scripts/daemon_runner.py` (conceptual task runner using a library like `APScheduler`) |
| `sbin/portald` | `scripts/portal_content_builder.py` (conceptual content fetching script) |
| `Slash::Install` | `slash/setup.py`<br>`slash/config.py` |
| `Slash::Slashboxes` | `slash/slashboxes.py`<br>`slash/portals.py` |
| `Slash::DB::Upgrade` | `slash/db_upgrade.py` |
| `themes/default/htdocs/about.pl` | `slash/pages.py::about_handler`<br>`main.py` (route registration) |
## Template System Components
| Perl Component | Python Location | Notes |
| :--- | :--- | :--- |
| `template.pl` files | `temp_templates/` directory | Templates stored as `{name};{page};{skin}` files |
| Template Toolkit `.html` files | Jinja2 templates in database | Converted syntax, database-driven |
| Template fallback logic | `slash/templating/loader.py` | Implements skin→theme→default fallback |
| Template import process | `scripts/import_templates.py` | Converts flat files to database entries |
| Template debugging | `uvicorn.log` | Search for `[TEMPLATE_DEBUG]` entries |
## Key Files Modified for /about Endpoint
| Issue Fixed | File Modified | Change Made |
| :--- | :--- | :--- |
| Theme fallback logic | `slash/templating/loader.py` | Fixed skin.name→skin.theme fallback to default_skin |
| Skin detection accuracy | `slash/skin_detection.py` | Added IP address check, port stripping, key sorting |
| Template context | `slash/display.py` | Added missing template context variables |
| Template syntax conversion | `temp_templates/sidebox;misc;default` | Converted Template Toolkit to Jinja2 |
| Gzip compression bug | `slash/middleware/compression.py` | Fixed Content-Length header duplication |
| Server management | `manage_server.py` | Added automatic template import on restart |
When you're porting a Perl file into this Python project, the main idea is to first analyze its core responsibilities rather than doing a direct line-for-line translation. You'll often find that a single Perl "god module" is doing several different jobs, so the first step is to break that functionality apart into smaller, more focused Python files like urls.py, sanitizer.py, or email.py. This deconstruction is key to creating a clean, modern structure.
Once you have the logic grouped, replace any dated Perl patterns with modern Python equivalents. For example, instead of functions that procedurally build HTML strings, create helpers that prepare a context dictionary for a Jinja2 template to render. Swap out custom security or logging code for standard libraries like passlib and logging. Finally, integrate your new modules into the framework and be sure to update the module_map.md and porting_guide.md so everyone knows where the old logic lives now and how to use the new version.
# Porting Guide: From Slash Perl to Python
This guide documents how to translate common patterns from the old Perl codebase to the new Python framework.
---
## **Password Hashing (`PasswordSalt`)**
The `Slash::Apache::User::PasswordSalt` module, which used a manually managed salt file, is replaced by the `slash.security.passwords` module. The new module uses the industry-standard **`passlib`** library, which automatically handles secure salt generation per password. The site-wide "salt" concept is modernized as a **`SECRET_KEY`** (or "pepper") in `config/settings.py`.
* **Old Perl:**
```perl
# Manual hashing with system salt
my $salt = getCurrentPwSalt($vuser);
my $hashed = md5_hex($password . $salt);
```
* **New Python:**
```python
from slash.security.passwords import hash_password, verify_password
# Hashing is handled by a single function
hashed_pw = hash_password("some_user_password")
# Verification is just as simple
is_valid = verify_password("some_user_password", hashed_pw)
```
---
## **Template System & Theme Management**
The `Slash::Display::Provider` for Template Toolkit is replaced by a comprehensive **Jinja2** system in `slash.templating`. This section covers the complete template architecture based on debugging the `/about` endpoint.
### **Critical Insight: Skin vs Theme Distinction**
**SKIN** = Site section/functionality (mainpage, politics, etc.)
**THEME** = Visual appearance/template set (soylentnews, default, etc.)
```python
# Example from database:
# SKIN: skid=1, name='mainpage', hostname='soylentnews.org', theme=''
# THEME: Falls back to constants['default_skin'] = 'soylentnews'
```
### **Template Resolution Logic**
**CRITICAL**: The template loader must implement proper theme fallback logic, NOT just use `skin.name`:
* **Old Perl (from `rehash/Slash/DB/MySQL/MySQL.pm`):**
```perl
$skin ||= getCurrentSkin()->{theme};
$skin ||= $constants->{'default_skin'};
$skin ||= 'default';
```
* **New Python (in `slash/templating/loader.py`):**
```python
# WRONG: Using skin name
skin_name = skin.name # "mainpage" - no templates exist!
# CORRECT: Using theme fallback logic
skin_name = 'default' # final fallback
if skin and hasattr(skin, 'theme') and skin.theme:
skin_name = skin.theme # Use skin's theme field
else:
# Fall back to default_skin constant
constants = get_current_static()
default_skin = constants.get('default_skin')
if default_skin:
skin_name = default_skin # "soylentnews"
```
### **Template Naming Convention**
Templates use semicolon-separated naming: `{template_name};{page};{theme}`
```
html-header;misc;soylentnews ← Theme-specific header
html-header;misc;default ← Default theme header
sidebox;misc;soylentnews ← Theme-specific sidebar
```
### **Template Fallback Order**
The `DatabaseTemplateLoader` tries templates in this order:
1. `{template};{page};{theme}` - Exact match
2. `{template};{page};default` - Default theme for page
3. `{template};misc;{theme}` - Theme template, misc page
4. `{template};misc;default` - Final fallback
### **Context Injection Requirements**
Templates expect these global variables (set in middleware):
```python
# In slash/middleware/user_auth.py
env.globals.update({
'user': get_current_user(),
'skin': get_current_skin(),
'constants': get_current_static(),
'form': get_current_form(),
})
```
### **Template Import Workflow**
1. **Create templates** in `temp_templates/` with semicolon naming
2. **Import to database**: `python scripts/import_templates.py`
3. **Restart server**: `python manage_server.py restart`
4. **Test resolution**: Check logs for `[TEMPLATE_DEBUG]` output
### **Debugging Template Issues**
Enable template debugging in `slash/templating/loader.py`:
```python
logging.info(f"[TEMPLATE_DEBUG] Looking up template '{template_name}'")
logging.info(f"[TEMPLATE_DEBUG] Final lookup params: skin='{skin_name}'")
```
Common problems:
- **Empty theme field**: Skin has `theme=''` - must fall back to `default_skin`
- **Missing templates**: Theme templates don't exist - falls back to default
- **Wrong skin detection**: URL/hostname doesn't match - uses mainpage_skid
* **Old Perl (in a script):**
```perl
# Provider fetches from DB and injects globals implicitly
my $output = $tt->process($template_name, $vars);
```
* **New Python (complete setup):**
```python
from jinja2 import Environment
from slash.templating.loader import DatabaseTemplateLoader
from slash.templating.context import get_global_template_variables
# 1. Setup Jinja2 environment with database loader
env = Environment(loader=DatabaseTemplateLoader())
# 2. Add global variables (done in middleware)
env.globals.update(get_global_template_variables())
# 3. Render template (with proper fallback)
template = env.get_template("html-header") # Resolves to html-header;misc;soylentnews
output = template.render(local_vars)
```
---
## **Template Pages (`Apache::TemplatePages`)**
The `Slash::Apache::TemplatePages` module, which handled Apache requests for `.tmpl` URLs by looking up templates in the database, is replaced by the `slash.template_pages` module with FastAPI route integration.
* **Old Perl (Apache handler):**
```perl
# In httpd.conf or .htaccess
<Files "*.tmpl">
SetHandler perl-script
PerlHandler Slash::Apache::TemplatePages
</Files>
# Handler code
sub handler {
my($r) = @_;
my $page = $r->uri;
$page =~ s|^/(.*)\.tmpl$|$1|;
my $title = $slashdb->getTemplateByName('body', {
page => $page, skin => $skin
});
if ($title) {
header($title, $skin);
print slashDisplay('body', '', { Page => $page });
footer();
} else {
return NOT_FOUND;
}
return OK;
}
```
* **New Python (FastAPI route):**
```python
from slash.template_pages import add_template_pages_routes
# In main.py - register the routes
app = FastAPI()
add_template_pages_routes(app)
# The handler automatically processes .tmpl URLs:
# GET /about.tmpl -> looks up 'body' template with page='about'
# Returns rendered template or 404 if not found
```
The new implementation provides:
- **URL Pattern Matching**: Automatic handling of `/{page}.tmpl` URLs
- **Database Integration**: Looks up templates with page and skin context
- **Template Rendering**: Uses Jinja2 instead of Template Toolkit
- **Error Handling**: Returns proper 404 responses for missing templates
- **Caching**: Includes appropriate cache headers
---
## **Hook System (`Hook`)**
The `Slash::Hook` module, which looked up and ran hooks from the database on every call, is replaced by the more performant `slash.hooks` module. The new system uses an in-memory registry where hooks are registered once when the application starts.
* **Old Perl:**
```perl
# Executes hooks defined in the 'hooks' database table
slashHook('new_comment_hook', { cid => $cid });
```
* **New Python:**
```python
from slash.hooks import run_hook, register_hook
# In a plugin or module's setup code (runs once at startup):
def my_new_comment_handler(cid):
print(f"A new comment was made: {cid}")
register_hook("new_comment", my_new_comment_handler)
# Elsewhere in the code, to trigger the event:
run_hook("new_comment", cid=12345)
```
---
## **Plugin Management (`Plugin`)**
The `Slash::Plugin::isInstalled` check, which queried the database, is replaced by `slash.plugins.is_installed`. The new function checks against a simple list of `ACTIVE_PLUGINS` in the central `config/settings.py` file, which is much faster and easier to manage.
* **Old Perl:**
```perl
use Slash::Polls;
if (Slash::Polls->isInstalled) {
# Do poll stuff...
}
```
* **New Python:**
```python
from slash.plugins import is_installed
if is_installed("polls"):
# Do poll stuff...
pass
```
---
## **Constants**
The `Slash::Constants` module is replaced by Python's `Enum` in `slash/constants.py`.
* **Old Perl:**
```perl
use Slash::Constants ':all';
if ($mode == MSG_MODE_EMAIL) { ... }
```
* **New Python:**
```python
from slash.constants import MessageMode
if mode == MessageMode.EMAIL:
...
```
---
## **Request State (`getCurrent*`)**
The numerous `getCurrentUser()`, `getCurrentForm()`, `getCurrentDB()`, etc., functions are obsolete. All request-scoped state is now held in the `RequestContext` object, accessible via helper functions in `slash.context`.
* **Old Perl:**
```perl
my $user = getCurrentUser();
my $form = getCurrentForm();
```
* **New Python:**
```python
from slash.context import get_current_user, get_current_form
# Access the context within a request's lifecycle
user = get_current_user()
form = get_current_form()
uid = user.uid
```
---
## **Database & Object Handling (`getObject`)**
The `getObject` factory is replaced by **dependency injection**. Your web framework should be configured to provide a database session to each request handler that needs one.
* **Old Perl:**
```perl
my $slashdb = getObject('Slash::DB', { db_type => 'writer' });
$slashdb->updateStory(...);
```
* **New Python (with FastAPI `Depends`):**
```python
from fastapi import Depends
from slash.models import DatabaseSession
from slash.database import get_db_session
@app.post("/story")
def update_story(db: DatabaseSession = Depends(get_db_session)):
db.update_story(...)
```
## **Utilities (`Utility::Data`)**
The monolithic `Slash::Utility::Data` module has been broken down into several smaller, focused modules, each with a clear responsibility.
* **HTML Sanitization**: `strip_html`, `balanceTags`, and related functions are now in `slash.sanitizer`. Use `sanitizer.strip_html()` for general-purpose cleaning.
* **URL Handling**: Functions like `fudgeurl`, `url2abs`, and `cleanRedirectUrl` are in `slash.urls`.
* **Data Formatting**: `formatDate` and `commify` are in `slash.formatting`.
* **Validation**: `emailValid` and `submitDomainAllowed` are in `slash.validation`.
* **Security**: `createLogToken` and `changePassword` are in `slash.security.tokens`.
* **Old Perl:**
```perl
use Slash::Utility::Data;
my $clean_html = strip_html($dirty_html);
my $absolute_url = url2abs('/some/path');
```
* **New Python:**
```python
from slash.sanitizer import strip_html
from slash.urls import url_to_absolute
clean_html = strip_html(dirty_html)
absolute_url = url_to_absolute('/some/path')
```
## **Template Rendering (`Display`)**
The main `slashDisplay` function is replaced by `slash.rendering.render_template`. This function, combined with the `DatabaseTemplateLoader`, emulates the original's behavior of constructing a full template identifier (`name;page;skin`) and performing fallbacks.
1. **Name Construction**: `render_template` uses the provided `template_name`, the `Page` from the context, and the global `skin` to create the ideal template name (e.g., `about;index2;default`).
2. **Database Fallback**: The `DatabaseTemplateLoader` and the underlying `db.get_template` method automatically handle fallbacks. If `about;index2;default` is not found, it will try `about;index2;default`, then `about;misc;default`, and finally `about;misc;default`, just like the original system.
* **Old Perl:**
```perl
# Looks for 'story' template, using current page and skin context
# with fallbacks to 'misc' page and 'default' skin.
slashDisplay('story', { story => $story_data }, { Return => 1 });
```
* **New Python:**
```python
from slash.rendering import render_template
# In a web request handler:
# The skin is taken from global context. 'Page' can be in the local context.
html_output = render_template('story', {'story': story_data, 'Page': 'article'})
```
---
## **Global Template Object (`g`)**
In the original Perl templates, a global object `g` was available, which contained request-level data. The most common use was to access the current skin via `g.Skin`. This is now handled by the `RequestContext` and the `get_current_skin()` helper.
To ensure templates have access to this data, the `g` object is constructed in the page handler and added to the `page_context`.
* **Old Perl (in a template):**
```perl
# Access the skin's name
[% g.Skin.name %]
```
* **New Python (in `slash/pages.py`):**
```python
from slash.context import get_current_skin
# ... in a page handler ...
page_context = {
'g': {
'Skin': get_current_skin(),
},
# ... other context variables ...
}
```
## **System Interactions (`Utility::System`)**
Functionality from `Slash::Utility::System` has been split into dedicated modules.
* **Email**: `sendEmail` is replaced by `slash.email.send_email`. The `bulkEmail` function is replaced by `slash.email.send_bulk_email`, which is a convenient wrapper around the more powerful `slash.custom.bulk_mail.BulkMailer` class.
* **Logging**: The custom `doLog` functions are replaced by Python's standard `logging` module, configured in `slash.logging_config`.
* **Files & Processes**: `save2file` and `prog2file` are replaced by functions in `slash.system`, which use Python's modern `os` and `subprocess` modules.
* **Old Perl:**
```perl
# Single Email
sendEmail($addr, $subj, $body);
# Bulk Email
bulkEmail(\@addrs, $subj, $body);
```
* **New Python:**
```python
import logging
from slash.email import send_email, send_bulk_email
# Single Email
send_email(to_addr=addr, subject=subj, body=body)
# Bulk Email
send_bulk_email(recipients=addrs, subject=subj, body=body)
# For direct, advanced control:
# from slash.custom.bulk_mail import BulkMailer
# mailer = BulkMailer(...)
# await mailer.bulkmail()
```
## **Access Control & Formkeys (`Utility::Access`)**
The broad responsibilities of `Slash::Utility::Access` have been split into focused modules.
* **Form Submission Security**: The "formkey" system for preventing CSRF and abuse is now in `slash.access_control`. Use `check_form_submission()` in request handlers.
* **Content Filtering**: The `filterOk` and `compressOk` functions for checking submitted content are now in `slash.validation`.
* **User Status**: User expiration logic is now part of user management in `slash.auth`.
* **Business Logic**: Feature-specific logic like `isDiscussionOpen` is now in its own module, `slash.discussions`.
* **Old Perl:**
```perl
my $error_flag = formkeyHandler('valid_check', 'comments');
die if $error_flag;
```
* **New Python:**
```python
from slash.access_control import check_form_submission
error_msg = check_form_submission('comments')
if error_msg:
raise HTTPException(status_code=400, detail=error_msg)
```
## **UI Components (`Utility::Display`)**
The procedural `createSelect`, `fancybox`, etc., functions from `Slash::Utility::Display` are replaced by a more modern pattern in `slash.ui_components`. These new functions prepare a **context dictionary**, which is then passed to a dedicated Jinja2 template for rendering. This separates logic from presentation.
The custom `<SLASH-..>` tag processing is now a Jinja2 filter in `slash.templating.filters`.
* **Old Perl:**
```perl
# Procedurally prints HTML
fancybox('100%', 'My Title', 'My Content');
```
* **New Python:**
```python
# In a request handler, you'd pass the context to your main template.
# The main template then includes the widget template.
# In story.html:
# {% include '_fancybox.html' with {'title': 'My Title', 'contents': 'My Content'} %}
# Alternatively, using a helper function:
from slash.ui_components import fancybox
html_output = fancybox(title='My Title', contents='My Content')
```
---
## **Web Responses & Page Structure (`Utility::Anchor`)**
The `Slash::Utility::Anchor` module was a classic "god module" that handled nearly all aspects of generating a web page, from HTTP headers to a page's footer. This functionality has been broken apart into several focused modules that align with modern web framework practices.
* **HTTP Responses**: Functions like `http_send`, `redirect`, and `emit404` are now handled by helpers in `slash.responses`. These functions return `fastapi.Response` objects.
* **ETag Generation**: The `get_etag` logic has been moved to `slash.http.generate_etag`.
* **Header/Footer**: The monolithic `header` and `footer` functions are replaced entirely by Jinja2 templates. The logic for setting cache headers has been moved into the response helpers in `slash.responses`.
* **Ad-serving**: `getAd` and `prepAds` are now in `slash.ads`.
* **Content Blocks**: `getSectionBlock` is now `slash.cms.get_section_block`.
* **Skin Colors**: `getSkinColors` is located in `slash.skins.get_skin_colors`.
* **Old Perl:**
```perl
use Slash::Utility::Anchor;
redirect('http://example.com/');
```
* **New Python:**
```python
from slash.responses import redirect
# In a request handler, you simply return the response object
def some_request_handler():
return redirect('http://example.com/')
```
---
## **Comment System (`Utility::Comments`)**
The monolithic `Slash::Utility::Comments` module has been deconstructed into a dedicated `slash.comments` sub-package. This refactoring separates the core logic from UI rendering, scoring, and validation, making the system more modular and maintainable.
* **Core Logic**: The main functions for fetching and saving comments (`selectComments`, `saveComment`) are now handled by `slash.comments.manager`.
* **UI/Rendering**: Procedural HTML generation (`displayThread`, `dispComment`) is replaced by Jinja2 templates, with data preparation handled by `slash.comments.ui`.
* **Scoring & Moderation**: The complex `getPoints` and `_can_mod` logic is now isolated in `slash.comments.scoring`.
* **Validation**: All submission checks from `validateComment` are now in `slash.comments.validation`.
* **Utilities**: Helper functions like `makeCommentBitmap` are in `slash.comments.utils`.
* **Old Perl:**
```perl
# A single function call with many side effects
printComments($sid);
```
* **New Python:**
```python
# In a request handler:
from slash.comments.manager import get_comments_for_discussion
from slash.comments.ui import render_comment_thread
# 1. Fetch the data
comment_data = get_comments_for_discussion(sid)
# 2. Render the template
html_output = render_comment_thread(comment_data['comments'], sid)
```
---
## **Backend Daemons (`slashd`, `portald`)**
The original Perl application used several long-running daemons for background tasks. These are replaced by modern Python task scheduling and execution patterns.
* **`slashd` (Task Scheduler)**: This daemon dynamically loaded and ran standalone scripts from a `tasks` directory based on a cron-like schedule. Its modern equivalent is a dedicated task runner script, `scripts/daemon_runner.py`, likely built with a library like **APScheduler** or **Celery**. This runner imports and schedules specific Python functions that were ported from `Slash::DB::Static::MySQL` into various modules (e.g., `slash.tasks.cleanup`, `slash.tasks.freshen`). The functions for managing the daemon's own status and error reporting have been ported to `slash.tasks.daemon`.
* **`portald` (Content Builder)**: This was a long-running script responsible for fetching external content (like RSS feeds), processing it, and building various content "blocks" for the main site (e.g., 'Top Comments', 'Current Poll'). The modern equivalent is a standalone script, `scripts/portal_content_builder.py`, that performs the same function. It uses helpers from `slash.site_content` and other modules to fetch and prepare the data. This script is intended to be run periodically, either by a system cron job or by the new `daemon_runner.py`.
---
## **Request Logging (`Apache::Log`)**
The `Slash::Apache::Log` module, which contained a `PerlCleanupHandler` to log requests and update user hit counts, is replaced by `slash.auditing` and its corresponding middleware in `slash.middleware.auditing`.
* **Access Logging**: The `handler()` sub is now `slash.auditing.create_access_log`.
* **User Activity**: The `UserLog()` sub is now `slash.auditing.update_user_activity`.
These functions are executed automatically for each request by the `AuditingMiddleware`, which is configured in `main.py`. This modernizes the logging process by decoupling it from the web server's specific configuration.
* **Old Perl (in `httpd.conf`):**
```apache
PerlCleanupHandler Slash::Apache::Log
PerlCleanupHandler Slash::Apache::Log::UserLog
```
* **New Python (in `main.py`):**
```python
from fastapi import FastAPI
from slash.middleware.auditing import AuditingMiddleware
app = FastAPI()
app.add_middleware(AuditingMiddleware)
```
---
## **User Authentication & Session Management (`Apache::User`)**
The `Slash::Apache::User` module, which handled user authentication, session management, cookie processing, and URL routing, has been completely redesigned as a modern Python middleware system with several focused modules:
* **Authentication Middleware**: The main `handler()` function is now `slash.middleware.user_auth.UserAuthMiddleware`. This middleware handles user login/logout, cookie authentication, SSL access control, and ban checking.
* **Cookie Management**: Functions like `bakeUserCookie` and `eatUserCookie` are now in `slash.cookies` with modern security practices.
* **URL Routing**: The `userdir_handler` functionality is now in `slash.user_urls.UserURLRouter` with support for `/my/`, `/~user/`, and `/^user/` patterns.
* **Form Processing**: Parameter filtering and sanitization is handled by `slash.utils.filter_params`.
* **Old Perl (Apache handler):**
```perl
# In httpd.conf
PerlHandler Slash::Apache::User
# Handler code processes authentication, cookies, routing
sub handler {
my($r) = @_;
# Complex authentication and routing logic...
return Apache2::Const::OK;
}
```
* **New Python (FastAPI middleware):**
```python
from fastapi import FastAPI
from slash.middleware.user_auth import UserAuthMiddleware
from slash.user_urls import add_user_url_routes
from slash.cookies import CookieManager
app = FastAPI()
app.add_middleware(UserAuthMiddleware)
add_user_url_routes(app)
# Cookie management in request handlers:
def login_handler(response: Response):
cookie_mgr = CookieManager(response)
cookie_mgr.login_user(uid=123, remember_me=True)
```
The new implementation provides:
- **Modern Security**: HTTPS-only cookies, SameSite protection, proper CSRF handling
- **Modular Design**: Separate modules for authentication, cookies, and URL routing
- **Framework Integration**: Native FastAPI middleware instead of Apache-specific handlers
- **Type Safety**: Full type hints and modern Python patterns
- **SSL Control**: Configurable SSL access restrictions for different user types
- **Ban Management**: Integrated ban list checking with proper error responses
---
* **Old Perl (`slashd`):**
```
# sbin/slashd loads and runs tasks like 'freshenup.pl'
```
* **New Python (Conceptual `scripts/daemon_runner.py`):**
```python
from apscheduler.schedulers.blocking import BlockingScheduler
from slash.tasks import freshen, cleanup
scheduler = BlockingScheduler()
# Schedule the Python functions directly
scheduler.add_job(freshen.get_stories_to_refresh, 'cron', minute='*/5')
scheduler.add_job(cleanup.delete_daily, 'cron', day_of_week='mon-fri', hour=1)
scheduler.start()
```
* **Old Perl (`portald`):**
```
# sbin/portald fetches RDF, gets top comments, etc.
```
* **New Python (Conceptual `scripts/portal_content_builder.py`):**
```python
import sys
from slash import site_content
from slash.app import initialize_slash_for_script
def build_portal_content():
# Fetch RSS feeds, get top comments, update polls, etc.
rdf_sites = site_content.get_sites_rdf()
for site in rdf_sites:
print(f"Fetching {site['rdf']}...")
# ... processing logic ...
top_comments = site_content.get_top_comments()
# ... save block content ...
print("Portal content build finished.")
if __name__ == "__main__":
# The new script must be initialized to have access to the database
# and other framework components.
initialize_slash_for_script()
build_portal_content()
---
## **Story Presentation & Data (`Slash`)**
The global `Slash.pm` "god module" has been deconstructed. Its two primary responsibilities are now handled by separate, focused modules.
* **Story Rendering**: The `displayStory` and `dispStory` functions have been moved to `slash.stories.presentation`. The new `get_story_context` function prepares a rich context dictionary that is then passed to a Jinja2 template, separating data logic from HTML presentation.
* **Data Snippets**: The `getData` function for retrieving small, template-driven data snippets is now `slash.data_snippets.get_data_snippet`.
* **Old Perl:**
```perl
# In a script
use Slash;
print displayStory($stoid, 1);
my $snippet = getData('some_value');
```
* **New Python:**
```python
# In a request handler
from slash.stories.presentation import get_story_context
from slash.rendering import render_template
context = get_story_context('some-story-id', full=True)
html_output = render_template('story.html', context)
# To get a data snippet
from slash.data_snippets import get_data_snippet
snippet = get_data_snippet('some_value')
```
---
## **XML Generation (`XML`)**
The `Slash::XML` module and its sub-modules (`RSS`, `Atom`) for generating feeds have been consolidated into the `slash.xml` module. The new module uses the `lxml` library for robust and correct XML generation.
* **Feed Generation**: The main `xmlDisplay` function is now `slash.xml.xml_display`. It returns a `Response` object with the appropriate headers.
* **Date Formatting**: The `date2iso8601` helper is now `slash.xml.date_to_iso8601`.
* **Encoding**: The custom XML encoding functions are no longer needed, as `lxml` handles this automatically.
* **Old Perl:**
```perl
use Slash::XML;
xmlDisplay('rss', { items => \@items });
```
* **New Python:**
```python
from slash.xml import xml_display
# In a request handler, return the response object
def rss_feed_handler():
items = get_items_for_feed()
return xml_display('rss', {'items': items})
```
---
## **Template Toolkit Plugin (`Display::Plugin`)**
The `Slash::Display::Plugin`, which made various global functions available to Template Toolkit via `[% Slash.* %]`, is replaced by a global `slash` object available in all Jinja2 templates. This is made possible by the `slash.templating.slash_plugin.SlashPluginProxy` class.
This proxy object provides access to functions in modules like `slash.utils`, `slash.constants`, and `slash.db`, preserving the convenience of the original plugin.
* **Old Perl (Template Toolkit):**
```perl
[% Slash.someFunction('some data') %]
[% Slash.db.someMethod(var1, var2) %]
```
* **New Python (Jinja2):**
```python
{{ slash.some_function('some data') }}
{{ slash.db.some_method(var1, var2) }}
---
## **Installation & Setup (`Install`)**
The `Slash::Install` module, which handled both component installation and configuration management, has been split into two modules:
* **`slash.setup`**: This module is responsible for the setup and management of components like themes, plugins, and tagboxes. It handles file operations (copying, symlinking) and the execution of SQL scripts for schema, dump, and prep files.
* **`slash.config`**: This module provides an interface for managing site-wide configuration settings that are stored in the database (`site_info` table).
* **Old Perl:**
```perl
use Slash::Install;
my $install = Slash::Install->new($user);
$install->installPlugin('MyPlugin', 0, 1);
my $value = $install->getValue('some_config_key');
```
* **New Python:**
```python
from slash.setup import install_plugins
from slash.config import get_config_value
install_plugins(['MyPlugin'], symlink=True)
value = get_config_value('some_config_key')
```
```
---
## **Individual Script Pages (`about.pl`, etc.)**
Individual Perl scripts in `themes/default/htdocs/` (like `about.pl`) that were served directly by Apache/mod_perl are replaced by route handlers in the `slash.pages` module. These scripts typically followed a pattern of calling header, rendering content, and calling footer.
* **Old Perl (`about.pl`):**
```perl
#!/usr/bin/perl -w
use Slash;
use Slash::Display;
use Slash::Utility;
sub main {
my $form = getCurrentForm();
my $constants = getCurrentStatic();
header(getData('head'), '', { Page => 'index2' }) or return;
slashDisplay('about');
footer({ Page => 'index2' });
}
createEnvironment();
main();
```
* **New Python (`slash.pages`):**
```python
from fastapi import Request, Depends
from fastapi.responses import HTMLResponse
from slash.context import get_current_form, get_current_static
from slash.data_snippets import get_data_snippet
from slash.rendering import render_template
from slash.database import get_db
async def about_page_handler(request: Request, db = Depends(get_db)) -> HTMLResponse:
# Equivalent to getCurrentForm() and getCurrentStatic()
form = get_current_form()
constants = get_current_static()
# Equivalent to getData('head')
head_data = get_data_snippet('head')
# Prepare context (includes { Page => 'index2' })
context = {
'form': form,
'constants': constants,
'head_data': head_data,
'Page': 'index2',
'request': request,
}
# Equivalent to header(), slashDisplay('about'), footer()
content = render_template('about', context)
return HTMLResponse(content=content, status_code=200)
# Register route in main.py
def add_static_page_routes(app):
app.add_api_route("/about", about_page_handler, methods=["GET"])
```
The new implementation provides:
- **Route-based Handling**: Uses FastAPI routes instead of Apache file serving
- **Template Integration**: Leverages the existing Jinja2 templating system
- **Context Management**: Uses the modern request context system
- **Dependency Injection**: Proper database and service injection
- **HTTP Standards**: Returns proper HTTP responses with headers
- **Maintainability**: Clean separation of concerns and modern Python patterns
---
## **Content Blocks (`getBlock`)**
The `Slash::DB::MySQL::getBlock` method, which fetched content blocks from a 'blocks' database table, is replaced by the `getBlock` method in `slash.sqlite_implementation.SQLiteManager`. This method is used by templates to retrieve reusable content snippets like footer text, copyright notices, and other site content.
* **Old Perl:**
```perl
# In a template
[% Slash.db.getBlock('footer', 'block') %]
# In Perl code
my $footer_content = $slashdb->getBlock('footer', 'block');
my $block_title = $slashdb->getBlock('footer', 'title');
```
* **New Python:**
```python
# In SQLiteManager class
def getBlock(self, block_name: str, field_name: str = None) -> str:
"""Retrieves content blocks from the database."""
# TODO: Replace with actual database lookup when blocks table exists
known_blocks = {
'footer': {
'block': 'Copyright notice content...',
'title': 'Footer'
}
}
block_data = known_blocks.get(block_name, {})
return block_data.get(field_name, '') if field_name else block_data
# Templates continue to use the same syntax:
# {{- slash.db.getBlock('footer', 'block') -}}
```
---
## **Plugin Structure in Templates**
Templates expect plugin information in a nested structure (`constants.plugin.PluginName`), but the database stores plugin configuration as flat key-value pairs. The middleware creates the proper structure for template compatibility.
* **Old Perl (template usage):**
```perl
[% IF constants.plugin.Subscribe AND NOT constants.subscribe_admin_only %]
<li><a href="[% constants.real_rootdir %]/subscribe.pl">Subscribe</a></li>
[% END %]
```
* **New Python (middleware setup):**
```python
# In UserAuthMiddleware
constants_data = db.get_slash_conf() # Flat config from database
for name, value in constants_data.items():
set_current_static(name, value)
# Create plugin structure for template compatibility
plugin_structure = {}
if constants.get('subscribe'): # subscribe = 1 in database
plugin_structure['Subscribe'] = True
# Add to constants so templates can access constants.plugin.Subscribe
set_current_static('plugin', plugin_structure)
```
This maintains compatibility with existing templates while using modern Python configuration patterns.