# CPalius CMF 1.1.0 "Liman"

**Released:** 2026-09-14 · **Channel:** stable · **Critical:** no

This release is about where files live and how they travel: copying uploads to a
remote target, getting backups off the server, serving images through a CDN, and
updating a single file without downloading a full package. Hence the name —
*liman* is Turkish for harbour, the place where cargo is held and shipped out.

Turkish: [1.1.0-tr.md](1.1.0-tr.md)

## New

- **Storage targets (AACP → Maintenance → Storage & CDN).** Amazon S3,
  Cloudflare R2 and FTP/FTPS. Every S3-compatible service works through the same
  driver: MinIO, DigitalOcean Spaces, Backblaze B2. **No AWS SDK is installed** —
  requests are signed with SigV4 directly, because ~90 MB of service definitions
  for three HTTP verbs would be felt on every install of a package that operators
  upload to shared hosting over FTP.

- **One button: save and test.** A target is not used until it has passed a
  **live write probe** against exactly the configuration now stored: the probe
  writes a real file, reads it back and deletes it. A bucket policy that allows
  listing but denies writes, and a read-only FTP account, both pass every check
  lighter than this one.

- **Media offload.** Uploads are always written to `public/uploads` and *then*
  copied to the selected target. The original stays on this server — thumbnails
  are generated from disk by GD, the `public/uploads/.htaccess` hardening does
  not travel to a bucket with the bytes, and the files backup keeps containing
  the site's media. If the remote is unreachable the site still serves images
  from its own disk.

- **Nightly sweep.** Collects files whose upload-time push failed, and
  thumbnails. It works from a cursor: each run continues where the last stopped
  and wraps to the beginning on completion. This is the setting to turn on when
  moving an existing site across for the first time. Off by default.

- **Off-site backups.** The archive is uploaded after it is written and then
  **read back from the destination to confirm it arrived**. With *keep the local
  copy* off, the local archive is removed only after that confirmation. Remote
  retention is configurable; deleting the local and the remote copy are two
  separate buttons.

- **CDN for images.** One setting covers both common setups: the base URL
  *replaces* the `/uploads` part. For a pull zone in front of this origin use
  `https://cdn.example.com/uploads`; for a public bucket use
  `https://pub-xxxx.r2.dev`. The CDN is independent of offload — a pull zone
  needs no upload at all.

- **File-level updates (patches).** When one file changes, installations in the
  field can take just that file instead of a forty-megabyte archive. The panel
  lists every file a patch would replace, and its SHA-256, **before** it is
  applied.

- **`cp:patch` command.** Prints the pending patch and its file list; writes
  nothing. Applying requires `--apply`, and asks for confirmation on top of that
  in an interactive session.

## Security

- Patch bodies are fetched only from CPalius-owned addresses, and **every file
  is SHA-256 verified before it is written**. If one does not match, not a
  single byte reaches the project tree — a half-applied patch is a mixture of
  versions nobody has ever tested.

- A patch **cannot write** to `cp-includes/vendor`, `public/uploads`,
  `cp-core/var` or `.env`. The rule lives in one place shared with the full
  updater; a rule kept in two copies eventually diverges, and the diverging side
  becomes a hole.

- A patch must ship `CpVersion.php`, and the downloaded file's constant must
  state the manifest's version. Without this, a manifest announcing 1.1.1 while
  shipping 1.1.0 code would apply, leave the running version where it was, and
  be offered again on every check forever.

- **No patch is ever applied on its own.** There is no code path from the cron
  check to the installer; a CSRF-protected form or a deliberate command is
  required. Auto-applying would mean whoever controls the CPalius GitHub
  organisation controls every installation in the field without an operator in
  the loop.

- Storage credentials are stored **encrypted** in `cp_settings`
  (`SettingSecretCodec`). The screen never renders a secret back; an empty
  password field means "leave the stored value alone".

## Fixes

- Upload URLs are now produced in one place. They were written out at six
  separate call sites as `'/uploads/'.$key`; a CDN is the first situation in
  which that answer stops being the same everywhere.

- `cp:backup:create` now returns a **non-zero exit code** when the off-site copy
  fails. A command run from crontab that reports success while the backup never
  left the server sells its operator a year of not looking.

## Breaking changes

None. No default changes behaviour: storage targets, the nightly sweep and the
CDN all ship switched off. Install this release and touch nothing, and the site
behaves exactly as it did on 1.0.0.

## Upgrade notes

- Upgrading from 1.0.0 needs no extra step. There is no schema change; the new
  settings are created the first time they are written.
- On a live server, **`cp-core/var/cache/prod/` must be emptied**. Symfony does
  not rebuild the compiled container on its own in prod, so new services and the
  new cron job stay invisible until the cache is cleared.
- Updating from the panel does this for you.

## Known limitations

- The FTP target requires `ext-ftp`. Where the extension is missing, the card
  says so up front.
- A patch cannot write into `cp-includes/vendor`. A change that needs a new
  dependency requires a full release.
- Patches apply as a chain: the panel only offers the patch that upgrades from
  the running version. Intermediate versions cannot be skipped.
- Remote-primary storage — the original living only in the bucket — is not in
  this release. The remote target is always a copy.
