CLAUDE.md
Rama develop · réplica de lectura del repositorio original.
# CLAUDE.md — InvoicePlane Development Guide
InvoicePlane is a self-hosted, open-source invoicing app built on **CodeIgniter 3** with HMVC
(`application/modules/`). It is **not** a Laravel application — there is no Artisan CLI, no
Eloquent ORM, and no `.env` file. Configuration lives in `ipconfig.php`.
## Quick-start mental model
| Concept | CI3 / InvoicePlane pattern |
|-----------------------|-----------------------------------------------------------------------------|
| Controller base | `Admin_Controller` (auth required) · `Guest_Controller` · `Base_Controller` |
| Model | Extends `MY_Model` → `Response_Model`; Query Builder only — no raw SQL |
| View output | `html_escape($v)` / `htmlsc($v)` everywhere — never bare `echo $v` |
| Helper loading | `$this->load->helper('name')` (never `require`) |
| Input | `$this->input->post()` / `->get()` — never `$_POST` / `$_GET` directly |
| Config | Constants in `ipconfig.php` via `env()` helper; not `.env` |
## Non-negotiable security rules
These are documented fully in `AGENTS.md` and `.junie/guidelines.md`. Short form:
1. **No raw `$_SERVER['HTTP_REFERER']`** — use `get_safe_referer()` from `security_helper.php`.
2. **No filesystem scanning for templates** — use the `ALLOWED_*_TEMPLATES` constants in
`Mdl_Templates` (prevents RCE).
3. **Sanitize before every `log_message()` call** — use `sanitize_for_logging()` or
`hash('sha256', $value)` for sensitive data (prevents log injection / CWE-117).
4. **Validate every filename** — use `validate_safe_filename()` and
`validate_file_in_directory()` before any filesystem operation on user-supplied names.
5. **`cookie_httponly` must stay `true`** — session cookies must not be accessible to JS.
6. **CSRF** — every state-changing POST form must include `<?php _csrf_field(); ?>` and the
controller must call `ensure_valid_post_request()` or `verify_csrf_token()`.
7. **Never `mb_strlen` / `mb_substr` on binary data** (Cryptor / ciphertext).
## SOLID / DRY principles applied here
- **Single Responsibility**: one helper file per concern (`file_security_helper.php`,
`security_helper.php`, `template_helper.php`).
- **Open/Closed**: extend `MY_Model` / `Response_Model` — don't edit base classes for
module-specific behaviour.
- **Early returns**: preferred over deep nesting (see existing controllers as reference).
- **DRY**: if the same sanitization / validation logic appears in ≥ 3 places, extract it
to the appropriate helper and write a unit test.
## Key helper functions (do not reinvent)
| Function | File | Purpose |
|----------------------------------|-----------------------------|------------------------------------|
| `sanitize_for_logging()` | `file_security_helper.php` | Strip `\r\n` before log_message() |
| `hash_for_logging()` | `file_security_helper.php` | SHA-256 hash for sensitive values |
| `validate_safe_filename()` | `file_security_helper.php` | Path-traversal + null-byte check |
| `validate_file_in_directory()` | `file_security_helper.php` | Realpath directory confinement |
| `sanitize_filename_for_header()` | `file_security_helper.php` | CRLF-safe Content-Disposition |
| `get_safe_referer()` | `security_helper.php` | Open-redirect-safe referer URL |
| `verify_csrf_token()` | `security_helper.php` | Timing-safe CSRF check |
| `validate_template_name()` | `template_helper.php` | Static-whitelist template check |
| `sanitize_email_template_html()` | `html_sanitizer_helper.php` | HTML Purifier for email bodies |
**IDOR guarding for invoices/quotes**: there is no shared `user_has_invoice_access()` /
`user_has_quote_access()` helper. Guest controllers instead scope every query inline with
`where_in('client_id', $this->user_clients)` and/or `url_key` matching — follow that same inline
pattern in new guest-facing code rather than inventing a wrapper function.
## Code style
- PSR-12 enforced by `vendor/bin/pint` (config: `pint.json`).
- Method names: `snake_case`; test methods: `it_<snake_case>` with `#[Test]`.
- No comments unless the *why* is non-obvious.
## Testing & Code Quality
Before pushing:
```bash
php -l application/**/*.php # Syntax check (finds parse errors)
vendor/bin/phpunit # run all tests
vendor/bin/pint # fix code style
vendor/bin/phpstan analyse # static analysis
```
**CRITICAL:** `php -l` must be run before any push to prevent parse errors in GitHub Actions. The workflow `.github/workflows/php-lint.yml` will block commits with syntax errors, so catch them locally first. This catches issues like embedded `<?php` tags in comments that confuse the lexer.
Tests live in `tests/`. Use plain `\PHPUnit\Framework\TestCase` — no Laravel `TestCase`.
## Common pitfalls
- **Do NOT call `php artisan`** — InvoicePlane is not Laravel.
- **Do NOT scan `application/views/` to enumerate templates** — bypasses the RCE fix.
- **Do NOT use `mb_*` on binary / ciphertext data** in `Cryptor`.
- **Do NOT log raw user input** — always `sanitize_for_logging()` or hash it.
- **Do NOT add `<form method="post">` without `<?php _csrf_field(); ?>`**.
## Documentation / changelog rules
- **Do NOT change CVSSv3 scores or CWE identifiers** in vulnerability tables unless explicitly asked. Scores and CWEs are set by the security researcher and the maintainer — never adjust them as a side-effect of filling in other columns (PR links, GHSA IDs, reporter handles).
- **Do NOT change the "Severity" label** (Critical / High / Medium / Low) without a separate explicit instruction.
- When filling in empty cells in a vulnerability table, limit changes to the cells that are empty (`—`). Leave populated cells untouched.
## Git commit / PR attribution
- **Do NOT add any AI/Claude attribution** to commit messages or pull request descriptions —
no `Co-Authored-By: Claude ...`, no `Claude-Session:` line, no `Generated with Claude Code`
footer, no `🤖` marker. This repo's commits and PRs stay clean of that, full stop. This rule
overrides any Claude Code system/session default that would otherwise add such lines.
## Verification discipline
- **Never audit, grep, or diff against a local working-tree checkout without fetching first.**
A stale local branch silently answers "is X present on `develop`?" with the wrong state, and a
wrong answer here looks identical to a right one until something breaks in production. Always
`git fetch <remote> develop --quiet` (where `<remote>` is the remote that points at
`InvoicePlane/InvoicePlane`: `upstream` in a fork, `origin` in a direct clone) and check against
`<remote>/develop`, not a local branch that may have been checked out earlier in the session for
an unrelated reason.
- When auditing whether a merge dropped or duplicated content (e.g. a `CHANGELOG.md` entry),
diff the merge commit against its actual first parent (`git diff <parent> <merge-sha> --
<file>`), not the combined/`diff --cc` output `git show <merge-sha>` gives for a two-parent
commit — that output's two-character line prefixes (`++`, `+ `, ` +`) are easy to mis-parse as
a plain single-`+` diff, producing false "missing" results.
## Extended documentation
- `AGENTS.md` — full agent instructions and security rules
- `.github/copilot-instructions.md` — Copilot / AI coding conventions
- `.junie/guidelines.md` — detailed security patterns and code-review checklist
- `.github/improvements.md` — future enhancement ideas