mediaFal
========

File Abstraction Layer media source for MODX 3, modelled after TYPO3 FAL.

Every file is stored physically once, on a fixed path that never changes
(YYYY/MM/DD/readable-name.ext). Editors see and organise files in virtual
folders; renaming or moving a file or folder only changes the database, so
links in content, template variables and other extras keep working. mediaFal
always knows where each file is used.

Documentation: {docs_url}

Status: beta. Features are complete; test on a copy of your site first and
keep a backup of the database and the assets/ folder.

Requirements
------------
- MODX 3.0 or newer, PHP 8.1 or newer
- GD or Imagick (Imagick recommended: keeps ICC colour profiles); WebP
  variants when the server supports WebP

Installation
------------
Install through Package Management. On the first install a form offers
(all checked by default):
- create a FAL media source for images "Images" (assets/media/images/,
  allowed types jpg, jpeg, png, gif, webp, avif),
- create a FAL media source for files "Files" (assets/media/files/; allowed
  types from mediafal.kind_document/archive/audio/video; the language of an
  uploaded file is the language of the resource),
- make the images source the default media source (default_media_source),
  so editors and TVs without their own source open it,
- hide the folder assets/media/ in the core "Filesystem" source (adds
  "media" to its skipFiles; recommended), so nobody uploads past mediaFal.
  Note: skipFiles works by name, every folder named "media" is hidden there.
A source is not created again if a FAL source with the same path exists.
Upgrades keep your settings.

After installation
------------------
1. Cron job (full usage scan, trash, clean-up), e.g. nightly:
     php /path/to/core/components/mediafal/cli/scan.php
   Without cron run "Extras > mediaFal: scan usage" from time to time; the
   library warns when no full scan ran for more than 7 days.
2. More FAL sources: Media > Media Sources > Create, type "mediaFal (FAL)".
   Base paths of two FAL sources must not overlap. Property "uploadCulture": all languages or the
   language of the resource (useful for documents).
3. Template variables, ContentBlocks fields and editors that should use FAL:
   set their media source to a FAL source.
4. Existing files: Media > "Migration to mediaFal" (see Migration).

What editors get
----------------
- Media browser (core): virtual folders and names, upload, rename, move,
  "Edit metadata" (alt/title/description per language) in the file menu.
- Media > "mediaFal library": search and filters (kind, language, usage,
  resource = all files of a resource from all folders), several files at
  once, metadata per language, usage of each file, replace content (same
  file extension, path stays), rename the file in its URL (links rewritten, 301), focal point (click the preview), trash (node in the
  folder tree) with preview and restore.
- Resource tab "Attachments": files attached to the resource in groups
  (groups per template and per resource), upload of several files, drag &
  drop order, alt/title per resource, crop per attachment, "linked with
  resource ID" shows the attachments of another resource (e.g. the same page
  in another language). Switched on/off and groups per template (template
  tab "Attachment groups"). "Gather into folder" moves the resource's own
  attachments into its upload folder (not files attached to other
  resources; emptied folders are deleted; paths and URLs stay).
- TV type "falfile": one or more files (input option max), kinds, alt/title
  for this place of use, crop ratios (input option ratios, e.g. 16:9,1:1,free).

Permissions
-----------
mediaFal uses the core MODX permissions, the same as the media browser:
  file_manager        library page; "scan usage" also needs file_remove
  file_list/file_view browse files, file details
  file_upload         upload, replace content
  file_update         metadata, rename, move, language, focal point, restore
                      from trash
  file_remove         delete (to trash), delete permanently
  directory_create/update/remove   virtual folders
  view_document/save_document      Attachments tab (read / change)
  view_template/save_template      template tab "Attachment groups"
  settings            migration, migration backups, overlap warnings
Per user group the core "Media Source Access" applies as well (e.g. editors
only get the "Documents" source).

Output
------
One snippet, falFiles (falImg was merged into it).
Source (one of):
[[falFiles? &tv=`gallery`]]                     files of a falfile TV (&resource=`12`: another resource)
[[falFiles? &group=`Manuals`]]                  attachments of the resource (Files tab); no &tv = attachments
[[falFiles? &id=`12,15`]]                       files by ID
[[falFiles? &source=`2` &path=`2026/10/14/x.jpg`]]
  &source       FAL media source (ID or name): only files of that source
  &group        attachment group(s), names or IDs, comma separated;
                `0`, `-` or `none` = attachments without a group
  &resource     any resource ID (default: current resource)
Selection:
  &filter       images | documents | videos | audio | archives | other |
                files (everything except images) | .pdf | .md,.txt |
                images,.pdf (combined) | !.svg / documents,!.txt (exclude)
                Singular/plural and case do not matter. The extensions of
                each kind are system settings mediafal.kind_* (add a new
                format there; the next full usage scan updates the index).
  &index        only the n-th file, &offset, &limit
  &sortby       rank (default: order in the Files tab / TV) | random | name |
                title | created | size; &sortdir=`DESC` reverses. Sorting is
                done before &offset/&limit (random: shuffle, then limit).
                Call random uncached ([[!falFiles? ...]]), otherwise the page
                cache keeps one order. Fenom: {$_modx->runSnippet('!falFiles',
                [...])} - the modifier {'falFiles' | snippet : [...]} is cached.
  &totalVar     placeholder with the number of files before &limit (default
                total), e.g. "+[[+total]] photos"; in a pdoResources tpl use a
                unique name or read it right after the call.
Output:
  default       images: chunk falFilesImage (thumbnail &thumbWidth=`300`,
                linked to the full image) inside falFilesImages; other files:
                falFilesFile (link, extension, size) inside falFilesFiles.
                The chunks are installed once and can be edited.
  &tpl          one template for all files (chunk, @INLINE or @FILE), no group
                wrappers. @FILE path is relative to pdotools_elements_path (then
                the site root); with pdoTools Fenom syntax works too.
  &tplImage, &tplFile, &tplImages, &tplFiles (wrappers; `` = none), &wrapperTpl
  &field        only one value of the first file (url, alt, title, srcset, ...)
  single image  [[falFiles? &tv=`image` &limit=`1` &tpl=`falFilesImg`]]
                (chunk falFilesImg = [[+img]], full <img> with srcset)
  &srcset=`480,960,1440` &sizes &webp   responsive variants (cached)
  &ratio=`16:9`   manual crop for this ratio, otherwise around the focal point
  &crop=`0`       no crop
  &class, &loading (for [[+img]]), &outputSeparator, &toPlaceholder
Placeholders: url, url_original, path, alt, title, description, name, label
(title or name), extension, extension_upper, kind, size, size_formatted,
width, height, created, thumb, thumb_width, thumb_height, img, srcset, srcset_webp,
sizes, id, source, idx, total.
Alt/title: place of use > file metadata in the language of the context
(cultureKey) > default > empty. URLs get ?v=<hash> (also in content and plain
TVs) so replaced files are reloaded by browsers.

Migration
---------
Media > "Migration to mediaFal" (or cli/migrate.php for large sites): from any
file media source, a subfolder, FileMan or files under a FAL source that are
not in the index. Dry run with a report (used/unused, duplicates, blocked
types, references), copy in batches, rewrite references (content, TVs,
chunks, templates, settings, ContentBlocks, predefinedBlocks), finish (old
files moved to core/export/mediafal-backup/, 301 redirects from old URLs).
TVs with relative paths (image, file, MIGX ...) are resolved through the TV's
source for each resource (also per-resource paths like migxResourceMediaPath)
and switched to the FAL source already in a partial migration: migrated files
get the FAL path, others an absolute path (/assets/...) that the FAL source
outputs unchanged. The report lists what to check manually.
Repeatable: files already migrated are skipped. JPEG photos with an EXIF
orientation are turned upright (not resized). Types the target source does
not allow (allowedFileTypes) stay and are listed in the report. Empty files
(0 bytes) are not migrated (they would become one file); they stay and are
listed under "check manually". Links to other hosts are not rewritten (add
old/production domains of the site to site_hosts); a different letter case
is rewritten and redirected when unambiguous. Failed copies are retried by a
new run. Files migrated before 0.12.1:
  php core/components/mediafal/cli/orient.php           (dry run)
  php core/components/mediafal/cli/orient.php --apply

Settings (namespace mediafal)
-----------------------------
content_root         folder of the resource folders (default Content/). Folders in it
                     are managed by mediaFal: no manual create/rename/move/delete,
                     files only in the folder of a resource; uploading or moving a
                     file there attaches it to the resource. Empty = no protection.
                     FAL source property contentRoot overrides it (/ = none).
hide_file_tree       hide the core Files tab in the left tree for everyone except
                     sudo users (default No); or remove permission file_tree
upload_folder        folder of a resource inside content_root for uploads from the
                     Attachments tab; default {uri}/. Placeholders: {uri} (aliases of parents and
                     resource, about-us/team), {alias}, {parentAlias}, {pagetitle},
                     {parentTitle}, {template}, {context}, {id}, {folderId}
                     (435 -> 4/3/5), {year}, {month}. Empty alias/title -> ID.
                     A FAL source can override it (property uploadFolder).
                     When a resource's alias, title or parent changes, its folder
                     (and the folders of its children) is renamed/moved, unless
                     an editor moved it or the new folder already exists.
duplicates           reuse (same content returns the existing file) | reject
blocked_extensions   never uploaded (svg, html, js, php ...)
dir_max_files        files per day folder before DD-2/ is used (1000)
image_max            images are downscaled to this size on upload (2560; 0 = off)
image_quality        JPEG/WebP quality (85)
webp                 WebP variants for srcset when supported (1)
cache_bust           ?v=<hash> in URLs (1)
crop_ratios          crop ratios for attachments without group ratios (free)
protect_used         files in use cannot be deleted (1)
trash_days           days in the trash before permanent deletion (30; 0 = never)
unused_days          days after a file lost its last use before it is moved
                     to the trash during the full scan (0 = off)
migrate_skip_dirs    folders skipped by the migration
site_hosts           other hosts of this site, comma separated (production
                     domain on a dev copy, old domain in a migration);
                     http_host with and without www. always counts

For developers
--------------
- Pick files through the MODX media browser and upload through
  $source->uploadObjectsToContainer(); the final path is reported with
  OnFileManagerFileRename and $source->getLastUploaded(). Writing files
  directly to the filesystem bypasses the index.
- Event OnMediaFalCollectUsage (on resource save and full scan): extras with
  their own data report usage, without depending on mediaFal classes:
    $scriptProperties['usage']->add($sourceId, 'path/in/source.jpg', 'myfield');
  If the extra cannot read its data, it calls ->fail('reason') (check with
  method_exists): the previous usage of the resource is kept.

Uninstall
---------
Removes the plugin, snippets, menus, event and settings. Tables, files on
disk and FAL media sources are kept (the sources stop working without
mediaFal: change their type or remove them).
