mirror of
https://git.tt-rss.org/fox/tt-rss.git
synced 2026-05-04 23:26:09 +02:00
Add '.github/copilot-instructions.md' to help the bots out
This commit is contained in:
parent
032193f305
commit
0aaae551ee
302
.github/copilot-instructions.md
vendored
Normal file
302
.github/copilot-instructions.md
vendored
Normal file
@ -0,0 +1,302 @@
|
||||
# Tiny Tiny RSS (tt-rss) - AI Coding Agent Instructions
|
||||
|
||||
## Project Overview
|
||||
Tiny Tiny RSS is a web-based RSS/Atom feed reader and aggregator built with PHP (backend) and JavaScript with Dojo Toolkit (frontend). Forked in October 2025 to continue development after original tt-rss.org shutdown.
|
||||
|
||||
## Architecture & Stack
|
||||
|
||||
### Backend (PHP 8.2+)
|
||||
- **PHP Version**: Minimum version enforced in `Config::sanity_check()` (currently 8.2.0) - this is the source of truth
|
||||
- **Database**: PostgreSQL exclusively (DB_TYPE constant deprecated)
|
||||
- **ORM**: Idiorm (`ORM::for_table('table_name')`) - simple active record pattern
|
||||
- **Config**: Environment variables prefixed with `TTRSS_` (e.g., `TTRSS_DB_HOST`) set in `.env` or `config.php`
|
||||
- **Handlers**: Request routing via Handler classes (Handler → Handler_Protected → Handler_Administrative hierarchy)
|
||||
- Methods are public entry points accessed via `?op=ClassName&method=methodName`
|
||||
- Access control enforced by `before()` method:
|
||||
- `Handler`: No restrictions (returns true)
|
||||
- `Handler_Protected`: Requires authenticated user (`$_SESSION['uid']`)
|
||||
- `Handler_Administrative`: Requires admin access level
|
||||
- Methods starting with underscore (`_`) are blocked from external access
|
||||
- Methods with required parameters are blocked (security measure)
|
||||
- Use `csrf_ignore($method)` to bypass CSRF token validation for specific methods
|
||||
- Examples: `classes/Feeds.php`, `classes/Article.php`, `classes/RPC.php`
|
||||
- **Plugin System**: Extensible via `PluginHost` with hooks (see `classes/PluginHost.php` for ~30 hook types)
|
||||
- Plugins extend `Plugin` class, implement `init($host)` and `about()`
|
||||
- Place in `plugins/` (official/bundled) or `plugins.local/` (personal/unbundled, separate repos at `https://github.com/tt-rss/tt-rss-plugin-*`)
|
||||
- Example: `plugins/note/init.php`
|
||||
|
||||
### Frontend (JavaScript + Dojo Toolkit)
|
||||
- **Framework**: Legacy Dojo Toolkit (AMD modules: `define(["dojo/_base/declare", ...])`)
|
||||
- **Widgets**: dijit (Dojo UI library) - `dojoType="dijit.form.TextBox"`, `dijit.Dialog`, etc.
|
||||
- **Global Object**: `App` in `js/App.js` - contains utilities, translations, form helpers
|
||||
- **Main Modules**: `Feeds.js`, `Headlines.js`, `Article.js`, `CommonDialogs.js`
|
||||
- **Build**: Gulp for LESS compilation (`gulpfile.js`) - run `npx gulp` to watch/compile themes
|
||||
|
||||
### Database & ORM Patterns
|
||||
```php
|
||||
// Idiorm usage - fluent query builder
|
||||
$user = ORM::for_table('ttrss_users')->find_one($user_id);
|
||||
$feeds = ORM::for_table('ttrss_feeds')
|
||||
->where('owner_uid', $_SESSION['uid'])
|
||||
->find_many();
|
||||
$feed->save(); // UPDATE if exists, INSERT if new
|
||||
```
|
||||
|
||||
### Configuration System
|
||||
- **Primary**: `classes/Config.php` defines all config constants (e.g., `Config::DB_HOST`)
|
||||
- **Override**: Set via environment variables with `TTRSS_` prefix or in `config.php` via `putenv()`
|
||||
- **User Prefs**: `classes/Prefs.php` - per-user settings stored in `ttrss_user_prefs2`
|
||||
- Most preferences are associated with a user profile (`$_SESSION['profile']`)
|
||||
- Some preferences in `_PROFILE_BLACKLIST` are user-level only (e.g., `ENABLE_API_ACCESS`, `USER_TIMEZONE`, `DIGEST_ENABLE`)
|
||||
- Profile-blacklisted preferences ignore profile parameter and always apply to the user
|
||||
|
||||
## Key Workflows
|
||||
|
||||
### Development Setup
|
||||
```bash
|
||||
# Local development with Docker (no persistence)
|
||||
cp .env-dist .env # Configure TTRSS_DB_* variables
|
||||
docker-compose up # Starts db, app, updater, web-nginx
|
||||
|
||||
# Install PHP dependencies
|
||||
composer install
|
||||
|
||||
# Install JS dependencies & watch
|
||||
npm install
|
||||
npx gulp # Watch LESS files and compile on changes
|
||||
```
|
||||
|
||||
### Code Quality & Testing
|
||||
```bash
|
||||
# PHP Static Analysis
|
||||
phpstan analyze --no-progress # Level 6, config in phpstan.neon
|
||||
|
||||
# PHP Code Modernization
|
||||
vendor/bin/rector process # PHP 8.2 upgrades, config in rector.php
|
||||
|
||||
# JavaScript Linting
|
||||
npx eslint js/**/*.js plugins/**/*.js # Config in eslint.config.js
|
||||
|
||||
# Unit Tests
|
||||
vendor/bin/phpunit # Bootstrap: tests/autoload.php, config: phpunit.xml
|
||||
```
|
||||
|
||||
### Translation Management
|
||||
```bash
|
||||
# Update translation template (messages.pot) from source
|
||||
utils/rebase-translations.sh # Extracts strings from PHP/JS files
|
||||
# Note: .po/.mo files are managed via Weblate
|
||||
```
|
||||
|
||||
### Dojo Toolkit Updates
|
||||
```bash
|
||||
# Rebuild customized Dojo layer (requires Java runtime)
|
||||
cd lib/dojo-src
|
||||
./rebuild-dojo.sh # Downloads Dojo 1.17.3 source and builds custom layer
|
||||
```
|
||||
|
||||
### Plugin Development
|
||||
1. Create plugin directory:
|
||||
- `plugins/myplugin/init.php` for official/bundled plugins (included in main repo)
|
||||
- `plugins.local/myplugin/init.php` for personal/unbundled plugins (separate repo at `https://github.com/tt-rss/tt-rss-plugin-myplugin`)
|
||||
2. Implement `init($host)` to register hooks: `$host->add_hook($host::HOOK_ARTICLE_BUTTON, $this)`
|
||||
3. Add handler methods (e.g., `hook_article_button($article)`)
|
||||
4. Optional: `get_js()`, `get_css()` for client-side assets
|
||||
5. Enable in preferences: System → Preferences → Plugins
|
||||
|
||||
### Feed Update Process
|
||||
- **Daemon**: `update_daemon2.php` or Docker updater service calls `RSSUtils::update_daemon_common()`
|
||||
- **Core Logic**: `classes/RSSUtils.php` - fetches feeds, parses articles, applies filters
|
||||
- **Scheduler**: `classes/Scheduler.php` - manages periodic tasks (stored in `ttrss_scheduled_tasks`)
|
||||
- **Feed Parser**: `classes/FeedParser.php` - handles RSS/Atom with `FeedItem_RSS` and `FeedItem_Atom`
|
||||
|
||||
## Project-Specific Conventions
|
||||
|
||||
### PHP Style
|
||||
- **Type Hints**: Required for method signatures (param and return types)
|
||||
- **Doc Blocks**: Include `@param`, `@return`, `@var` annotations (PHPStan level 6)
|
||||
- **Strict Types**: Not universally declared - check file headers
|
||||
- **Namespaces**: None - all classes in global namespace, PSR-4 autoload from `classes/`
|
||||
- **Legacy Functions**: `include/functions.php` has deprecated helpers (e.g., `get_pref()` → use `Prefs::get()`)
|
||||
|
||||
### JavaScript Patterns
|
||||
- **AMD Modules**: Use `define()` for custom widgets/modules
|
||||
- **Script Type**: Code uses `sourceType: 'script'` (not ES modules) - compatible with legacy Dojo AMD pattern
|
||||
- **Dojo Queries**: `dojo.query()`, `dojo.queryToObject()`, `dijit.byId()`
|
||||
- **XHR**: `xhr.json()` wrapper (custom) or `dojo.xhrPost()`
|
||||
- **Dialogs**: Create via `App.dialogOf(this).hide()` or `new SingleUseDialog({})`
|
||||
|
||||
### Code Generation & Templates
|
||||
- **PHP HTML Helpers**: Prefer `\Controls\*` functions (e.g., `\Controls\submit_tag()`, `\Controls\hidden_tag()`, `\Controls\select_tag()`)
|
||||
- Only use raw HTML when helper functions don't support required functionality (explain why to user)
|
||||
- Example: `plugins/note/init.php` uses `\Controls\` namespace functions
|
||||
- **JavaScript HTML Helpers**: Prefer `App.FormFields.*` methods (e.g., `App.FormFields.submit_tag()`, `App.FormFields.checkbox_tag()`)
|
||||
- **Templates**: Use inline PHP/HTML (with `<?= ?>` short tags) rather than `Templator` class
|
||||
- Inline approach is preferred for maintainability and readability
|
||||
|
||||
### Deprecation & Migration
|
||||
- **Active Refactoring**: When modifying code, replace deprecated usage with best practice equivalents
|
||||
- Apply to tt-rss deprecations (e.g., `get_pref()` → `Prefs::get()`, avoid `DB_TYPE` constant)
|
||||
- Apply to dependency deprecations and language-level deprecations (PHP, JavaScript)
|
||||
- **No Timeline Pressure**: tt-rss deprecations have no deadline unless explicitly stated in deprecation comments
|
||||
- **Examples**: `include/functions.php` contains deprecated helpers - use modern equivalents in new/modified code
|
||||
|
||||
### Frontend State Management
|
||||
- **Context-Specific Module Loading**: Entry point files determine which modules are available
|
||||
- `index.php` loads `js/tt-rss.js` → includes `Feeds`, `Headlines`, `Article` modules (main app)
|
||||
- `prefs.php` loads `js/prefs.js` → includes `PrefUsers`, `PrefHelpers`, preference-specific modules
|
||||
- Check module availability with `typeof ModuleName !== 'undefined'` before use
|
||||
- **Context Detection**: Use `App.isPrefs()` to check if code is running in preferences vs main app
|
||||
- Returns `true` in preferences context, `false` in main app
|
||||
- Example: `CommonDialogs.js` uses this to determine post-action behavior (reload feeds vs refresh prefs)
|
||||
- **Plugin Context**: Plugins load via `get_js()` (main app) or `get_prefs_js()` (preferences)
|
||||
- **Shared Code**: `js/common.js` and `js/App.js` loaded in both contexts
|
||||
|
||||
### XHR Communication Patterns
|
||||
- **Response Format**: Backend handlers return JSON via `print json_encode($data)` - format varies by method
|
||||
- Simple responses: `{"wide": 0}`, `{"param": "key", "value": true}`
|
||||
- Counter updates: `{"message": "UPDATE_COUNTERS", "feeds": [...], "labels": [...]}`
|
||||
- Runtime info: `{"runtime-info": {...}}`
|
||||
- Errors: Use `Errors::to_json()` → `{"error": {"code": "E_UNAUTHORIZED", "params": {...}}}`
|
||||
- **Frontend Processing**: `xhr.json()` wrapper automatically calls `App.handleRpcJson()`
|
||||
- Processes standard fields: `error`, `seq`, `counters`, `runtime-info`, `message`
|
||||
- `message: "UPDATE_COUNTERS"` triggers `Feeds.requestCounters()` for specified feeds/labels
|
||||
- Fatal errors (non-`E_SUCCESS`) are handled centrally by `App.Error.fatal()`
|
||||
- **No Strict Schema**: Response structure is method-specific - handlers return whatever data frontend needs
|
||||
- Check existing handlers in same class for patterns
|
||||
- Frontend typically expects specific fields based on the action performed
|
||||
|
||||
### Transaction & Data Consistency
|
||||
- **PDO Transactions**: Use `Db::pdo()` for transactions (Idiorm doesn't provide transaction methods)
|
||||
```php
|
||||
$pdo = Db::pdo();
|
||||
$pdo->beginTransaction();
|
||||
// ... database operations ...
|
||||
$pdo->commit(); // or $pdo->rollBack() on error
|
||||
```
|
||||
- **Handler Instance PDO**: Handlers have `$this->pdo` available (initialized in `Handler` base class)
|
||||
- **ORM Access**: Idiorm uses `ORM::get_db()` to access PDO - acceptable alternative if it simplifies code and avoids unnecessary `Db::pdo()` calls
|
||||
- **Plugin Data Separation**: `PluginHost` uses separate `$pdo_data` instance for plugin storage
|
||||
- Prevents transaction conflicts between plugin data saves and main app operations
|
||||
- Only initialized when first needed
|
||||
- Comment in code: "separate handle for plugin data so transaction while saving wouldn't clash with possible main tt-rss code transactions"
|
||||
- **Multi-table Operations**: Always use transactions when updating related tables (e.g., articles + labels + counters)
|
||||
|
||||
### Database Schema
|
||||
- **Migrations**: Tracked via `Config::SCHEMA_VERSION` (currently 151)
|
||||
- When making schema changes:
|
||||
1. Update `sql/pgsql/schema.sql` to reflect the desired end state (for new installations)
|
||||
2. Increment `Config::SCHEMA_VERSION` in `classes/Config.php`
|
||||
3. Create migration file `sql/pgsql/migrations/{new_version}.sql` with ALTER statements
|
||||
- Example: Incrementing from 151→152 requires updating `schema.sql`, setting `SCHEMA_VERSION = 152`, and creating `migrations/152.sql`
|
||||
- **Conventions**: Tables prefixed `ttrss_` (e.g., `ttrss_feeds`, `ttrss_entries`, `ttrss_user_entries`)
|
||||
- **Special Feeds**: Negative IDs (constants in `classes/Feeds.php`): -1 (Starred), -2 (Published), -3 (Fresh), -4 (All), -6 (Recently Read)
|
||||
|
||||
### Input Validation & Sanitization
|
||||
- **HTML Content**: Use `Sanitizer::sanitize()` for user-generated HTML (e.g., feed content, article text)
|
||||
```php
|
||||
$clean_html = Sanitizer::sanitize($content, $strip_images = false, $owner_uid, $site_url);
|
||||
```
|
||||
- **User Input**: Use `clean()` function for HTTP parameters where HTML is not needed
|
||||
```php
|
||||
$feed_id = clean($_REQUEST['id']); // Strips tags, trims whitespace
|
||||
// For arrays:
|
||||
$selected_ids = clean($_REQUEST['ids']); // Applies to each element
|
||||
```
|
||||
- **Prefer Centralized Helpers**: Avoid duplicating validation logic - use existing helpers or add to appropriate utility class
|
||||
- **Type Casting**: Validate and cast types explicitly (e.g., `(int)$id`, `(bool)$flag`) after cleaning
|
||||
- **Examples**: See `classes/Feeds.php`, `classes/Article.php` for typical sanitization patterns
|
||||
|
||||
### Translation & Internationalization
|
||||
- **PHP Backend**: Use `__($msgid)` function for translatable strings (from `lib/gettext/gettext.inc.php`)
|
||||
```php
|
||||
echo __("Hello, world!"); // Returns translated string
|
||||
// Plural forms:
|
||||
$msg = _ngettext("article", "articles", $count);
|
||||
```
|
||||
- **JavaScript Frontend**: Use global `__()` function (defined in `js/common.js`)
|
||||
```javascript
|
||||
alert(__("This function is only available in combined mode."));
|
||||
// Fallback to English if App.l10n not available
|
||||
```
|
||||
- **Plugin Translations**: Plugins use `_dgettext()` via `Plugin::__()` method
|
||||
```php
|
||||
$this->__("Plugin-specific string"); // Uses plugin's translation domain
|
||||
```
|
||||
- **Best Practice**: Always use translated strings for user-facing messages; English fallback acceptable for internal logging/debugging
|
||||
- **Translation Files**: Managed via Weblate, extracted with `utils/rebase-translations.sh` (updates `messages.pot`)
|
||||
|
||||
### Security Patterns
|
||||
- **CSRF**: Token in `$_SESSION["csrf_token"]`, validated unless `csrf_ignore()` returns true
|
||||
- **Auth**: Session-based, sequence in `UserHelper::login_sequence()`
|
||||
- **Sanitization**: See "Input Validation & Sanitization" section above
|
||||
|
||||
### Logging & Debugging
|
||||
- **Debug Logging**: Use `Debug::log()` for development/diagnostic output (feed updates, plugin execution)
|
||||
```php
|
||||
Debug::log("Processing feed: $feed_url", Debug::LOG_VERBOSE);
|
||||
Debug::log("Article data:", Debug::LOG_EXTENDED);
|
||||
if (Debug::get_loglevel() >= Debug::LOG_EXTENDED) {
|
||||
print_r($data); // Only shown at extended level
|
||||
}
|
||||
```
|
||||
- Log levels: `Debug::LOG_DISABLED` (-1), `Debug::LOG_NORMAL` (0), `Debug::LOG_VERBOSE` (1), `Debug::LOG_EXTENDED` (2)
|
||||
- Controlled via command-line options: `--log-level 1`, `--log /path/to/file.log`
|
||||
- Check if enabled: `Debug::enabled()`
|
||||
- **Error Logging**: Use `Logger::log_error()` for production error tracking (logs to SQL/syslog/stdout)
|
||||
```php
|
||||
Logger::log_error(E_USER_WARNING, "Failed to process feed: $error", __FILE__, __LINE__, $context);
|
||||
// Or shorter form:
|
||||
Logger::log(E_USER_NOTICE, "User logged in: $username");
|
||||
```
|
||||
- **User Errors**: Use `user_error()` for error conditions that should be logged and optionally displayed
|
||||
```php
|
||||
user_error("Invalid parameter: $param", E_USER_WARNING);
|
||||
```
|
||||
- **Plugin Error Handling**: Hooks wrap plugin calls in try/catch - exceptions/errors automatically logged as `E_USER_WARNING`
|
||||
- **Context**: Debug output includes timestamp and PID; Logger includes backtrace via `format_backtrace()`
|
||||
|
||||
## Critical Files & Entry Points
|
||||
- **Main App**: `index.php` → loads session, plugins, renders main UI
|
||||
- **API**: `api/index.php` → JSON API (see `classes/API.php`)
|
||||
- **Prefs**: `prefs.php` → admin/preferences UI
|
||||
- **Public**: `public.php` → unauthenticated endpoints (e.g., RSS feed publishing)
|
||||
- **Update**: `update.php`, `update_daemon2.php` → feed updating
|
||||
|
||||
## Docker & Deployment
|
||||
|
||||
### Official Docker Images
|
||||
- **Images**: Built via GitHub Actions (`.github/workflows/publish.yml`)
|
||||
- GitHub Container Registry: `ghcr.io/tt-rss/tt-rss` (app) and `ghcr.io/tt-rss/tt-rss-web-nginx` (web)
|
||||
- Docker Hub: `supahgreg/tt-rss` (app) and `supahgreg/tt-rss-web-nginx` (web)
|
||||
- **PHP Version Strategy**:
|
||||
- Docker images use `PHP_SUFFIX` env var (currently `84`) to determine PHP version → runs on PHP 8.4
|
||||
- Codebase maintains backward compatibility with PHP 8.2 for non-Docker users
|
||||
- Source of truth for minimum version: `Config::sanity_check()` checks PHP 8.2.0
|
||||
- When updating Docker PHP version: change `ENV PHP_SUFFIX=84` in `.docker/app/Dockerfile`
|
||||
- **Architecture**: Multi-container setup
|
||||
- **app**: Alpine-based PHP-FPM container (`.docker/app/Dockerfile`)
|
||||
- Installs PHP extensions via Alpine packages: `php${PHP_SUFFIX}-<extension>`
|
||||
- Environment variables: `OWNER_UID/GID`, `PHP_WORKER_MAX_CHILDREN`, `PHP_WORKER_MEMORY_LIMIT`
|
||||
- Auto-configuration: `ADMIN_USER_PASS`, `AUTO_CREATE_USER`, etc.
|
||||
- Plugins: Automatically clones `nginx_xaccel` to `plugins.local/` on build
|
||||
- **web-nginx**: Nginx reverse proxy (`.docker/web-nginx/Dockerfile`)
|
||||
- Configurable via `APP_UPSTREAM`, `APP_WEB_ROOT`, `APP_BASE` env vars
|
||||
- To run tt-rss on root instead of `/tt-rss`: set `APP_WEB_ROOT=/var/www/html/tt-rss` and `APP_BASE=`
|
||||
- **Volumes**: Map `/var/www/html/tt-rss` for persistent data and development
|
||||
- **Environment**: `.env` file with `TTRSS_*` variables (see `config.php-dist`)
|
||||
- Database: `TTRSS_DB_HOST`, `TTRSS_DB_PORT`, `TTRSS_DB_USER`, `TTRSS_DB_PASS`
|
||||
- Docker Secrets: Support `<VAR>__FILE` suffix (e.g., `TTRSS_DB_PASS__FILE=/run/secrets/db_password`)
|
||||
- XDebug: `TTRSS_XDEBUG_ENABLED`, `TTRSS_XDEBUG_HOST`, `TTRSS_XDEBUG_PORT`
|
||||
|
||||
## Testing Notes
|
||||
- **Unit Tests**: Limited coverage (see `tests/` directory)
|
||||
- **Integration Tests**: `tests/integration/` - require database setup
|
||||
- **Manual Testing**: Use Docker Compose setup with local source mounted
|
||||
|
||||
## Common Gotchas
|
||||
- **Config Changes**: Restart Docker containers after modifying `.env`
|
||||
- **Plugin State**: Plugin data cached in `ttrss_plugin_storage` - may need DB clear for dev
|
||||
- **Theme Changes**: Run `npx gulp` to recompile LESS after CSS edits
|
||||
- **ORM Caching**: Idiorm uses identity map - call `ORM::reset_db()` to clear
|
||||
- **Database-Only**: PostgreSQL is the only supported database (MySQL support removed)
|
||||
Loading…
x
Reference in New Issue
Block a user