Skip to content

About

Pretty Protected Downloads: a Joomla 5 & 6 custom field for access-checked file downloads

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Pretty Protected Downloads

Pretty Protected Downloads is a Joomla custom field that offers files for download without ever giving them a public URL. It works on articles, categories, contacts and user profiles. Files are stored outside the web root, and every download goes through Joomla, which checks that the visitor may see the item the field is on, the field and its field group before a single byte is sent.

Use it for member documents, meeting minutes, reports for a closed group, price lists for logged-in customers: anything that should follow the access level of the article it belongs to, rather than being one guessed or shared link away from anyone.

Features

  • A new field type, Pretty Protected Downloads, for articles, categories, contacts and user profiles: add as many files to a field as you like, each with its own button text, title, description, icon and button style.
  • Upload straight from the article form. A file uploads as soon as it is chosen, with a progress bar; the article cannot be saved while an upload is still running.
  • Files are stored outside the web root, or inside it in a folder closed with .htaccess.
  • Every download is checked the way the item's own component checks its page: publishing state and dates, the item and category access levels, and on top of that the field and field group access levels.
  • Every download needs a short-lived token bound to the visitor's session, fetched when the button is pressed, so a file can only be fetched by a visitor who passes the same checks as the download itself.
  • Visitors download the file under the name it was uploaded with.
  • Three layouts, Buttons, Cards and List, each with an optional file type and size, and each overridable from your template.
  • Works inside subform fields.
  • Allowed file types and a maximum file size. Scripts, web pages and SVG images are always refused, and every upload is inspected for hidden PHP the same way the Media Manager does it (zip archives of code, such as extension packages, can be allowed in the settings). The content must also match the extension: a web page named report.pdf is refused.
  • Removing a file from an article deletes it from disk once the article is saved, unless another article, such as a copy, still uses it.
  • A storage status on the plugin settings screen, and a button that deletes stored files no field uses any more.
  • English and Dutch language files.

Requirements

  • Joomla 5.3 or later, or Joomla 6.
  • PHP 8.1 or newer.

Installation

  1. Download the latest release ZIP from the GitHub releases page.
  2. In Joomla Administrator, go to System → Install → Extensions.
  3. Upload and install the ZIP. The plugin is enabled on installation.
  4. Go to System → Plugins, open Fields - Pretty Protected Downloads and choose where the files are stored (see below). Save, and check the status.
  5. Go to Content → Fields (or the Fields screen of Contacts, Users, or a component's Categories), create a new field and choose the type Pretty Protected Downloads.
  6. Edit an article, save it once if it is new, and add files.

Latest release: https://github.com/TLWebdesignNL/Pretty-Protected-Downloads/releases

Storage

Outside the web root (recommended)

Enter the absolute server path of a folder next to your website folder, for example /home/account/protected-downloads when the site lives in /home/account/public_html. The folder is created on the first upload if it does not exist yet, so its parent must be writable by PHP.

Files outside the web root have no URL at all. This works on every web server.

The plugin refuses to use the website folder itself, any folder above it, or one of Joomla's own folders (administrator, images, media, …): closing one of those with .htaccess would take the site offline. A folder inside one of them, such as images/protected, is allowed.

Inside the web root

When the hosting account gives PHP no writable folder outside the website, choose Inside the web root, closed with .htaccess. The folder defaults to files/prettyprotecteddownloads and is closed with an .htaccess file that denies all direct access.

Warning

.htaccess is only read by Apache and LiteSpeed. On nginx, IIS, or any server that ignores .htaccess, the files in this folder can be downloaded by anyone who knows or guesses a name. Block the folder in the server configuration yourself, for example on nginx:

location ^~ /files/prettyprotecteddownloads/ { deny all; }

or store the files outside the web root. The status on the settings screen warns whenever the folder has a URL.

Status

The Storage tab of the plugin settings shows the folder as last saved: whether it exists or can be created, whether it is writable, whether it lies inside the web root, and the largest upload that is in effect: the lower of the plugin setting and the server's PHP limits (upload_max_filesize, post_max_size).

For a folder inside the web root it also reports Direct access: the plugin asks the web server for the folder's own index.html and shows whether the server refused it. This is the real test, because an .htaccess file on disk means nothing to a server that does not read it. If it says NOT blocked, fix the server configuration before storing anything confidential. The answer is kept for ten minutes; Check again asks the server at once.

Configuration

Plugin settings

Setting Description
Storage Location Outside the web root, or inside it closed with .htaccess.
Folder The storage folder. An absolute path outside the web root, or a path relative to the site root inside it.
Allowed File Types Comma separated extensions editors may upload. Default: pdf,doc,docx,odt,rtf,txt,csv,xls,xlsx,ods,ppt,pptx,odp,zip,jpg,jpeg,png,gif,webp,mp3,mp4. For these types the content of each upload is checked against its extension. A type you add yourself is accepted without that check, and each such upload is written to the Joomla log.
Maximum File Size (MB) The largest upload. The server's PHP limit applies as well.
Allow Code Inside Zip Archives Off by default: an upload in which PHP is found is refused, which includes any zip archive with a .php file in it. Turn it on to offer extension packages and other archives of code. The content of a zip archive is then no longer inspected; its name still is, and so is every other file type. Only content that is detected as a zip archive counts, not a file that merely has the name of one.
Download Button Lifetime (minutes) How long a download button keeps working after the page was loaded. Default: 30.

Field options

Option Description
Display Buttons: a row of download buttons. Cards: a responsive grid of Bootstrap cards with title and description. List: a compact list with title, type, size and a small button.
Show File Type and Size Shows e.g. "PDF, 1.2 MB" with each download.
Card Classes Extra classes for every card (Cards display only).
Button Classes Extra classes for every download button of the field, e.g. btn-sm w-100.

Each file in a field has its own Button Text, Title, Description, Icon Classes (e.g. fa-solid fa-file-pdf) and Button Style (defaults to btn-primary).

The field's own Access setting, and that of its field group, decide who can download, on top of the article and category access. Give the field an access level of Registered to show the files to logged-in users only, even on a public article.

Template overrides

The display layouts can be overridden like any Joomla layout. Copy the one you want from the plugin to your template:

Plugin file Override location
plugins/fields/prettyprotecteddownloads/tmpl/prettyprotecteddownloads/buttons.php templates/YOUR_TEMPLATE/html/plg_fields_prettyprotecteddownloads/prettyprotecteddownloads/buttons.php
.../tmpl/prettyprotecteddownloads/cards.php .../html/plg_fields_prettyprotecteddownloads/prettyprotecteddownloads/cards.php
.../tmpl/prettyprotecteddownloads/list.php .../html/plg_fields_prettyprotecteddownloads/prettyprotecteddownloads/list.php

Each download in a layout is a small <form method="post">: keep the $download->hidden inputs inside it, and post it to $actionUrl. The variables available to a layout are listed at the top of tmpl/prettyprotecteddownloads.php.

How a download is checked

A download is a POST to index.php?option=com_ajax&group=fields&plugin=prettyprotecteddownloads&task=download and is only served when all of these hold:

  1. The Joomla form token of the visitor's session is valid.
  2. The download token was issued to this session, for exactly this field of this item, and has not expired. The button asks for it (task=token, also a POST with the form token) the moment it is pressed, and it is only issued when checks 3 to 5 below hold.
  3. The visitor could see the item the field is on:
    • an article or contact when it is published or archived, within its publish up and publish down dates, in a published category, and the visitor has both its access level and the category's. A visitor who may edit the item, or change its state, can download from it whatever its state and dates, as they can preview it;
    • a category when Joomla's own category tree holds it for this visitor, which means it and every category above it are published and accessible;
    • a user profile only for the user it belongs to, when the account is not blocked.
  4. The field is published, and its access level, and that of its field group, is one of the visitor's.
  5. The file is listed in that field of that item, and its stored name matches the entry it belongs to.

Anything else sends the visitor back to the page with a message. The file is sent as an attachment, under the name it was uploaded with but always with the stored file's own extension, with a content type taken from that extension rather than sniffed from the bytes, with X-Content-Type-Options: nosniff and a sandboxing Content Security Policy, and is never cached by the browser.

The plugin never reads, lists or deletes anything in the storage folder that it did not write itself. Its files all end in the uuid they were given on upload, so even a folder shared with other files comes to no harm from the clean-up.

Editors upload through the same endpoint (task=upload), which requires the form token, edit permission on the item (core.edit, or core.edit.own on their own items; a user profile is edited by its owner, or by anyone who may edit users) and everything the item form itself asks before it offers the field: the field and its field group are published, the field is editable on this side of the site (Editable In), the editor's access levels include the field's and its group's (a Super User in the administrator excepted), and the editor has Edit Custom Field Value on the field. One session can have at most 50 uploads waiting for their item to be saved. Previews of a file in the form (task=preview) are checked the same way.

Every upload is recorded in the editor's session with the item and field it was uploaded for. When an item is saved, it keeps only the files it already listed and the files uploaded for it in that session; any other entry is left out, with a warning. Save as Copy may also take over the files of the item it copies, when the editor may edit that item. So an editor cannot attach a file of another item to one they edit, even when they know its name. A file that was uploaded but not saved yet can only be previewed, or replaced and deleted, from the session that uploaded it for that item.

Clean-up

Uploads become part of an item when the item is saved. When an item is deleted (an article emptied from the trash, a category, contact or user deleted), its files stay in the storage folder at first. No field names them any more, so they can no longer be downloaded, and the clean-up below removes them.

A file that was uploaded but never saved, or that belonged to a deleted item, is cleaned up automatically: once a day, the first upload also deletes the unused files. The Storage tab of the plugin settings counts these files as well and offers Delete unused files to do it at once. The folder holds a .lastcleanup file that records when the automatic clean-up last ran. A file is only counted as unused when no field of any item names it, and when it is older than Keep Unused Uploads For (7 days by default, on the same tab). Set that longer than an editor may leave an item open with a new upload in it: an upload that is cleaned up before its item is saved is lost.

Limitations

  • Supported contexts. Articles, categories (of any component), contacts and user profiles. A field of this type in another component's context shows a notice instead of the upload control, and nothing is ever served for it, because the plugin has no rule for who may see such an item.
  • User profiles. A profile's files are visible to that user alone, on their own profile page. A user can upload files to their own profile wherever the field is editable for them: set Editable In to Administrator, or deny Edit Custom Field Value on the field to the Registered group, if users should not upload to their own profile.
  • Caching. A download button fetches its download token (task=token) when it is pressed, with the page's form token, which Joomla's caches replace with the visitor's own. So downloads work on pages served by System Cache: Conservative and by the System - Page Cache plugin. Without JavaScript the button sends the token it was rendered with instead, and on a cached page that token belongs to another session, so the download fails with "expired". Exclude the pages with downloads from caching if visitors without JavaScript must be able to download.
  • Article versions. Restoring an older version of an article from its history does not bring back files that were removed in the meantime: those were deleted when the article was saved without them. Joomla 6 restores the version's file entries all the same, so until the article is saved again their download buttons answer that the file could not be found. That save leaves the entries out, with a warning that names them.
  • Uninstalling leaves the stored files in their folder. They are your site's documents, not the plugin's.

Development

The plugin has no build step and no Composer dependencies. The tests need PHP with the pdo_sqlite and fileinfo extensions, which most builds include. To run them:

php tests/run.php

Each test file runs in its own process against a temporary site root, standing up just enough of Joomla's language and registry classes to exercise the helpers that decide which file a request may reach. The tests are excluded from the release archive.

To work on the plugin inside a Joomla installation, symlink the repository to plugins/fields/prettyprotecteddownloads and media/ to media/plg_fields_prettyprotecteddownloads, then use System → Discover.

Releases and Updates

Push a tag named V{version} (e.g. V1.0.0) after updating the version in prettyprotecteddownloads.xml, media/joomla.asset.json and changelog.xml. The release workflow builds the install zip from the tag with git archive (so .gitattributes decides what goes in), creates the GitHub release with that zip attached and notes taken from changelog.xml, and then adds the version to updates.xml, pointing at that zip with its SHA-256.

The plugin includes a Joomla update server:

https://raw.githubusercontent.com/TLWebdesignNL/Pretty-Protected-Downloads/main/updates.xml

Release changelog:

https://raw.githubusercontent.com/TLWebdesignNL/Pretty-Protected-Downloads/main/changelog.xml

Links

License

GNU General Public License version 2 or later.

About

Pretty Protected Downloads: a Joomla 5 & 6 custom field for access-checked file downloads

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages