unlikelysource / filecms-core
File-based content management system. Does not require a database.
Requires
- php: >=8
- phpmailer/phpmailer: >=6.1
Requires (Dev)
- phpunit/phpunit: >=9.5
This package is auto-updated.
Last update: 2026-07-26 06:08:12 UTC
README
Simple PHP framework that builds HTML files from HTML widgets.
- Includes a class that can generate and validate CAPTCHAs (uses the GD extension).
- Includes the CKEditor for full-featured editing.
- Includes an email contact form that uses PHPMailer.
- Is able to import single files from a legacy website, or can do bulk import
- Includes a complete set of transformation filters that can be applied during import, or afterwards
- Entirely file-based: does not require a database!
- Very fast and flexible.
- Once you've got it up and running, just upload HTML snippets and/or modify the configuration file.
- IMPORTANT: minimum version is PHP 8!
License: Apache v2
Critical Updates
2026-07-26: Important Update!!!
Passwords Now Hashed in Config
The latest version of filecms-core now expects the passwords stored in src/config.php to be hashed using Bcrypt.
Run get_password_hash.sh PLAIN_TEXT_PASSWORD (or vendor/unlikelysource/filecms-core/get_password_hash.sh PLAIN_TEXT_PASSWORD) and copy and paste the output into the $config['SUPER']['password'] key and any $config['SUPER']['alt_logins'] keys you've added.
CAPTCHA Changes
Have a look at the updated CAPTCHA documentation (scroll further down to see it). Ten new config parameters have been added to make the CAPTCHA more difficult for automated hacking systems to crack. Copy the recommended settings from this documentation page, and adjust as needed.
CK Editor Replacement
2026-07-18: Important Update!!!
Run vendor/unlikelysource/filecms-core/tinymce_upgrade_2026_07.sh
Upgrading From filecms-core v2.x to v3.x
Composer versions 0.2.* of this package are referred to in commit history and elsewhere as "v2.x"; 0.3.* (the line this README's version number, above, belongs to) is "v3.x". If your site was originally built against a 0.2.* release and you're bringing it up to the current 0.3.* release, there are several breaking changes to work through -- they're listed here in the order you're likely to hit them.
1. PHP 8 is now required
As of v0.3.9, the minimum PHP version is PHP 8 (v2.x supported PHP >=7.4). Confirm your server's PHP version before doing anything else -- everything below assumes PHP 8+.
2. Authentication moved from files to native PHP sessions
v0.3.1 removed Common\Security\Profile::getAuthFileName(), Profile::build(), and the DEFAULT_AUTH_DIR / DEFAULT_AUTH_PREFIX / AUTH_FILE_TTL constants. In v2.x, a successful login wrote an auth file to disk (under an AUTH_DIR config key) that was checked on subsequent requests; v3.x stores that same information in $_SESSION instead and never touches the filesystem for it.
- Remove the
AUTH_DIRconfig key if yourconfig.phpstill has one -- it's no longer read - Make sure
session_start()is being called (thefilecms-websiteskeleton'sbootstrap.phpalready does this) and that PHP's session save path is writable - Clean up (or just ignore) any leftover auth files sitting in your old
AUTH_DIR-- they're now dead weight, not a security-relevant artifact, since nothing reads them anymore
3. Passwords must be password_hash() hashes, not plaintext
v0.3.16 changed login verification (Profile::authenticate()) to compare the submitted password against a stored hash with password_verify(), rather than comparing plaintext strings. This is the change most likely to silently break login on an in-place upgrade -- if SUPER.password (or any SUPER.alt_logins.*.password) is still a plaintext string after upgrading, no password will ever verify against it, and the account becomes unable to log in with no obvious error.
For every account, generate a hash and replace the plaintext value in config.php using this script:
./get_password_hash.sh PLAIN_TEXT_PLAIN_TEXT_PASSWORD
# or
vendor/unlikelysource/filecms-core/get_password_hash.sh PLAIN_TEXT_PASSWORD`
'SUPER' => [
'username' => 'admin',
'password' => '$2y$12$...', // output of the command above, not the plaintext password
'alt_logins' => [
'editor' => [
'username' => 'editor',
'password' => '$2y$12$...', // same here
],
],
],
alt_logins itself (multiple named accounts, each with its own username/password) has been part of the config schema since the original v2-to-v3 transition, but its verification lived in filecms-website's login.phtml template as a hand-rolled comparison; v0.3.16 moved that logic into Profile::authenticate() in core. If your site has its own copy of that old comparison code, you can retire it in favor of the shared method.
4. CKEditor replaced by TinyMCE
If your v2.x site is still running the original CKEditor integration, follow the CK Editor Replacement instructions below (v0.3.10 introduced the TinyMCE migration script). The SUPER.ckeditor config key becomes SUPER.tinymce.
5. CAPTCHA hardening (recommended, not required)
v0.3.17 reworked Common\Image\Captcha to render the whole phrase as a single distorted image instead of one clean image per character, which is substantially harder for automated (OCR) systems to read -- see the CAPTCHA section below for the full explanation and config keys. This is backward compatible: a v2.x-era config.php without the new font_files / overlap_min / overlap_max / wave_x_amplitude / wave_y_amplitude keys will keep working with built-in defaults, but you won't get the new hardening until you add them.
Checklist
- Confirm your installation is running PHP >= 8
composer update unlikelysource/filecms-core- Remove the
AUTH_DIRconfig key if you have it set; confirm sessions work - Regenerate every
SUPER.password/SUPER.alt_logins.*.passwordas apassword_hash()hash (usingvendor/unlikelysource/filecms-core/get_password_hash.sh) - Migrate CKEditor to TinyMCE, if not already done (using
vendor/unlikelysource/filecms-core/tinymce_upgrade_2026_07.sh) - Add the new
CAPTCHAconfig keys to opt into the hardened rendering - Test login (including any
alt_loginsaccounts) end-to-end before considering the upgrade complete
CK Editor Replacement
Run this from the root of your filecms-website-based project (the directory that contains composer.json, src, templates and, after composer install, vendor):
vendor/unlikelysource/filecms-core/tinymce_upgrade_2026_07.sh
This script does the following:
- Backs up
templates/super/edit.phtml,src/upload.phpandsrc/config/config.php(adds a.baksuffix) - Adds
tinymce/tinymcetocomposer.jsonand installs it - Copies the TinyMCE assets into
public/tinymce - Downloads the updated
templates/super/edit.phtmlandsrc/upload.php
Two manual steps remain afterward:
- In
src/config/config.php, rename the'SUPER' => 'ckeditor'key to'tinymce'(keep the existingwidthandheightvalues):
'tinymce' => [ 'width' => '100%', 'height' => 400 ],
- If you had customized
templates/super/edit.phtmlorsrc/upload.php, re-apply those customizations by comparing against the.bakfiles just created, then remove the.bakfiles once you're satisfied.
Website Installation
Automated Installation
To perform an automated installation, run the following command, where /path/to/website is the directory path to your new website:
- Install Composer (see https://getcomposer.org/doc/00-intro.md
- Run the following command:
composer create-project unlikelysource/filecms-website /path/to/website
This single command clones the repository, installs unlikelysource/filecms-core and its dependencies (PHPMailer, TinyMCE), and copies the TinyMCE assets into public/tinymce -- no further manual steps are required.
Basic website config
All references are from /path/to/website
- Primary config file:
/src/config/config.php - Bootstrap file:
/bootstrap.php - Pre-processing code:
/src/processing.php
Additional documentation on these three follows.
To Run Locally Using PHP
From this directory, run the following command:
cd /path/to/website
php -S localhost:8888 -t public
To Run Locally Using Docker and docker-compose
Windows
Install Docker Desktop for Windows
Open the Power Shell (some commands don't work in the regular command prompt)
To bring the docker container online, run this command:
cd \path\to\website
admin up
To stop the container do this:
admin down
To open a command shell into the container:
admin shell
Linux / Mac
Install Docker + docker-compose:
- Mac
- Install Docker Desktop for Mac
- Linux
- Install Docker
- Install docker-compose
Open a terminal window (Terminal Application)
To bring the docker container online, run this command:
cd /path/to/website
./admin.sh up
To stop the container do this:
./admin.sh down
To open a command shell into the container:
./admin.sh shell
Browser Access
To access from your browser:
http://localhost:8888/
- Or, if your IP addressing is working:
http://10.10.10.10/
Bootstrap and Document Root
Set the website document root to /public
- The central point of entry is
/public/index.php - There is a file
.htaccessthat controls URL rewriting - If you are using nginx you will need to incorporate the same logic into your primary config file
/public/index.phpfirst loads the bootstrap file/bootstrap.php- This file defines three key constants used throughout the program (summarized in the table shown next)
- The bootstrap file also loads the Composer autoloader
- If you add your own classes under
/srcbe sure to updatecomposer.jsonand refresh Composer autoloading:
composer dump-autoload
Here is a summary of the three key constants defined by /bootstrap.php. Change as needed.
| Constant | Default | Description |
|---|---|---|
| BASE_DIR | Same directory as bootstrap.php |
Project root |
| HTML_DIR | /templates/site |
Location of HTML snippets |
| SRC_DIR | /src |
Location of source code |
Pre-Processing
Before the final HTML view is rendered, /public/index.php includes /src/processsing.php.
In this file you can include any pre-processing you need done.
- The request URL is available as the variable
$uri - This is where the admin URL (e.g.
/super) is captured and sent to processing
Templates
By default templates are stored in /templates/site. You can alter this in the config file.
Config File
Default: /src/config/config.php
- Delimiter:
DELIMdefaults to%% - "Cards"
CARDSdefaults tocards- Represents the subdirectory under which view renderer expects to file HTML "cards"
Layout
The overall website look-and-feel is in a single HTML file, by default in /templates/layout/layout.phtml.
- The view rendered by requests is injected into the layout by replacing
%%CONTENTS%%.
HTML
You can create HTML snippets designed to fit into layout.phtml any place in the designated HTML directory.
- Be sure to set the constant
HTML_DIRin the file/bootstrap.php.
Cards
Important: each %%CARD%% directive you add must be on its own line!
Auto-Populate All Cards
To get an HTML file to auto-populate with cards use this syntax:
DELIM+DIR+DELIM
Example: you have a subdirectory off HTML_DIR named projects and you want to load all HTML card files under the cards folder:
%%PROJECTS%%
Auto-Populate Specific Number of Cards
To only load a certain (random) number of cards, use =.
Example: you have a subdirectory off HTML_DIR named features and you want to load 3 random HTML card files under the cards folder:
%%FEATURES=3%%
Auto-Populate Specified Cards in a Certain Order
For each card, only use the base filename, no extension (i.e. do not add .html).
Example: you have a directory HTML_DIR/blog/cards with files one.html, two.html, three.html, etc.
You want the cards to be loaded in the order one.html, two.html, three.html, etc.:
%%BUNDLES=one,two,three,etc.%%
Editing Pages
By default, if you enter the URL /super/login you're prompted to login as a super user.
Configure the username, password and secondary authentication factors in: /src/config/config.php under the SUPER config key.
SUPER config key
Example configuration for super user:
// other config not shown
'SUPER' => [
'username' => 'REPL_SUPER_NAME', // fill in your username here
// use `vendor/unlikelysource/filecms-core/get_password_hash.sh NEW_PLAIN_TEXT_PASSWORD` to get the hashed value to store here
// you can also run `vendor/unlikelysource/filecms-core/get_password_hash.sh`
'password' => '$2y$12$N57MR.2KWUMyNtdjrv7X4ejAl/5XgyPFIUH2TCbCLhbUxbSGIut9q', // hash for 'REPL_SUPER_PWD'
/*
* extra login validation fields
* change key/value pairs as desired
* add as many as you want
* they're selected at random when asked to login
*/
'validation' => [
// if value is array, authentication needs to use "in_array()"
'City' => ['London','Tokyo'],
'Postal Code' => 'NW1 6XE',
'Last Name' => ['Holmes','Lincoln'],
],
'alt_logins' => [
'REPL_OTHER_NAME' => [
'username' => 'REPL_OTHER_NAME', // fill in alt username here
// use `get_password_hash.sh NEW_PLAIN_TEXT_PASSWORD` to get the hashed value to store here:
// you can also run `vendor/unlikelysource/filecms-core/get_password_hash.sh`
'password' => '$2y$12$ytOLGb9SRaFppla4MnExtuRFzhDDn0WitMD7AD4uMEqlT9fJpLuEa', // hash for 'REPL_OTHER_PWD'
],
// add others as needed
],
'attempts' => 3,
'message' => 'Sorry! Unable to login. Please contact your administrator',
// reserved for future use:
'allowed_ip' => ['10.0.0.0/24','192.168.0.0/24'],
// array of $_SERVER keys to store in session if authenticated
'profile' => ['REMOTE_ADDR','HTTP_ACCEPT_LANGUAGE'],
// change the values to reflect the names of fields in your login.phtml form
'login_fields' => [
'name' => 'name',
'password' => 'password',
'other' => 'other',
'phrase' => 'phrase', // CAPTCHA phrase
],
// only files with these extensions can be edited
'allowed_ext' => ['html','htm'],
'ckeditor' => [
'width' => '100%',
'height' => 400,
],
'super_url' => '/super', // IMPORTANT: needs to be a subdir off the "super_dir" setting
'super_dir' => BASE_DIR . '/templates', // IMPORTANT: needs to have a subdir === "super_url" setting
'super_menu' => BASE_DIR . '/templates/layout/super_menu.html',
'backup_dir' => BASE_DIR . '/backups',
'backup_cmd' => BASE_DIR . 'zip -r %%BACKUP_FN%% %%BACKUP_SRC%%',
],
// other config not shown
Here's a breakdown of the SUPER config keys
| Key | Explanation |
|---|---|
| username | Super user login name |
| password | Super user login password hash (using ./get_password_hash.sh) |
| attempts | Maximum number of failed login attempts. If this number is exceeded, a random third authentication field is required for login. |
| validation | Set of key:value pairs randomly selected each time you login. Values can be in the form of an array. |
| alt_logins | Additional usernames and password hashes |
| message | Message that displayed if login fails |
| profile | Array of $_SERVER keys that form the super user's profile once logged in |
| login_fields | Field names drawn from your login.phtml login form |
| validation | You can specify as many of these as you want. If the login attemp exceeds attempts, the SimpleHtml framework will automatically add a random field drawn from this list. |
| allowed_ext | Only files with an extension on this list can be edited. |
| ckeditor | Default width and height of the CKeditor screen |
| super_* | Settings pertaining to the location of the super admin user URL, templates and menu |
Contact Form
The skeleton app includes under /templates a file contact.phtml that implements an email contact form with a CAPTCHA
- Uses the PHPMailer package
- Configuration can be done in
/src/config/config.phpusing theCOMPANY_EMAILkey - CAPTCHA configuration can be done in
/src/config/config.phpusing theCAPTCHAkey - The same CAPTCHA is also used to protect
/super/login
CAPTCHA
FileCMS\Common\Image\Captcha::writeImages() renders the whole phrase as a single distorted image rather than one clean image per character. Rendering each character separately hands an automated reader its segmentation step for free -- it doesn't even need to figure out where one character ends and the next begins. Instead:
- Each character is drawn directly onto one shared canvas, using a randomly chosen font (from
font_files), size, rotation and baseline - Characters are placed with overlapping ("negative kerning") spacing, so adjacent glyphs touch or overlap
- Background/foreground noise (lines and dots) is colored close to the text's own color range instead of fully random, so it can't be stripped out by simple color thresholding
- The finished image is warped with a 2D wave distortion (
FileCMS\Common\Image\Strategy\Wave)
Example configuration:
'CAPTCHA' => [
'input_tag_name' => 'phrase',
'sess_hash_key' => 'hash',
'font_file' => SRC_DIR . '/fonts/FreeSansBold.ttf',
'font_files' => [
SRC_DIR . '/fonts/FreeSansBold.ttf',
SRC_DIR . '/fonts/FreeSansBoldOblique.ttf',
SRC_DIR . '/fonts/FreeSerifBold.ttf',
SRC_DIR . '/fonts/FreeSerifBoldItalic.ttf',
SRC_DIR . '/fonts/FreeMonoBold.ttf',
SRC_DIR . '/fonts/FreeMonoBoldOblique.ttf',
],
'img_dir' => BASE_DIR . '/public/img/captcha',
'num_bytes' => 3, // each byte == 2 characters
'rotate_min' => -40, // degrees, per character
'rotate_max' => 40,
// pixels shaved off each character's advance so adjacent glyphs
// touch/overlap -- denies clean per-character segmentation
'overlap_min' => 9,
'overlap_max' => 17,
'line_min' => 20, // count of background noise lines
'line_max' => 40,
'dot_min' => 40, // count of foreground noise dots
'dot_max' => 70,
// amplitude (pixels) of the 2D wave distortion applied to the
// finished image
'wave_x_amplitude' => 2,
'wave_y_amplitude' => 1,
],
Here's a breakdown of the CAPTCHA config keys
| Key | Explanation |
|---|---|
| input_tag_name | Name of the $_POST field expected to hold the phrase the user typed in |
| sess_hash_key | Name of the $_SESSION key holding the password_hash() of the correct phrase |
| font_file | Fallback font, used if font_files is empty |
| font_files | Pool of fonts randomized per character; more variety makes it harder for an OCR model trained on a single font |
| img_dir | Directory the generated CAPTCHA PNG is written to (must be web-accessible) |
| num_bytes | CAPTCHA phrase length is num_bytes * 2 hex characters |
| rotate_min / rotate_max | Degrees of random rotation applied to each character |
| overlap_min / overlap_max | Pixels shaved off each character's horizontal advance so adjacent glyphs touch/overlap |
| line_min / line_max | Range for the random count of background noise lines |
| dot_min / dot_max | Range for the random count of foreground noise dots |
| wave_x_amplitude / wave_y_amplitude | Max pixel displacement of the horizontal/vertical wave distortion applied to the finished image |
Import Feature
You can enable the import feature by setting the IMPORT::enable config key to TRUE.
The importer itself is at /templates/site/super/import.phtml.
Selected transformation filters can be applied to one or more pages during the import process.
Here are some notes on config file settings under the IMPORT config key:
IMPORT::enable- Set this value to
FALSEif you do not wish this feature to be available.
- Set this value to
IMPORT::delim_start- tells the importer where to start cutting out content from the HTML source
- default: <body>
IMPORT::delim_stop- tells the importer where to stop cutting out content from the HTML source
- default: </body>
IMPORT::trusted_src- list of one or more prefixes from "trusted" sources for import
- allows you to limit where imports can be taken from
- in case you get hacked, this prevents attackers from importing malicious from their own sites
IMPORT::import_file_field- this file must be in JSON format
- name of the file upload field used in the form
- you can upload a list of URLs to import followed by a list of transforms to apply
- the URLs key is 'URLS'
- the 'IMPORT' key lets you override any Import configuration including the transforms to apply during import
IMPORT::transform- sub-array of transforms to make available to the importer
callback: anything that's callable- if your own PHP function or anonymous function, signature must match
SimpleHtml\Transform\TransformInterface
- if your own PHP function or anonymous function, signature must match
params: array of parameters the callback expectsdescription: shows up when you run/super/importAfter logging in as the admin user, go to/super/import.
Transform Feature
You can apply transformation filters on existing pages.
The importer itself is at /templates/site/super/import.phtml.
Included transformation classes are located in /src/Transform.
You can add your own by simply extending FileCMS\Common\Transform\Base.
After logging in as the admin user, go to /super/transform.
Here are some notes on config file settings under the TRANSFORM config key:
TRANSFORM::enable- Set this value to
FALSEif you do not wish this feature to be available.
- Set this value to
TRANSFORM::backup_dir- Directory where backups will be placed prior to transformation
TRANSFORM::transform_dir- Directory where transform classes are found
TRANSFORM::transform_file_field- Name of the form field that is used if you want to upload a set of transforms
Clicks
A class FileCMS\Common\Stats\Clicks was added as of version 0.2.1.
Records the following information into a CSV file:
- URL
- Date
- Time
- IP address
- Referrer
- 1
The "1" can be used in a spreadsheet to create totals by any of the other fields.
After logging in as the admin user, go to
/super/clicks.
Statistical Methods
The following methods are available for your use:
Clicks::get(string $click_fn) : array
Returns an array keyed and sorted by URL, with hit grand totals.
Clicks::get_by_page_by_day(string $click_fn) : array
Returns an array keyed and sorted by URL + Y-m-d, with hit totals for each day
Clicks::get_by_path(string $click_fn, string $path) : array
Returns the same as get_by_page_by_day() except that it filters results based on $path.
Use this to return stats on URLs such as /practice/dr_tom/.
CSV
You can use a CSV file just like a database using the new FileCMS\Common\Data\Csv class
public function getItemsFromCsv($key_field = NULL) : array
- Gets list of items from CSV
- @param string|array $key_field : header(s) to use as key; leave blank for numeric array
- @return array $select :
[key => value]; key === practice_key; value = $row
public function writeRowToCsv(array $post, array $csv_fields = []) : bool
- Writes row to CSV
- @param array $post : normally sanitized $_POST
- @param array $csv_fields : array of CSV headers; leave blank if headers not used
- @return bool : TRUE if entry made OK
public function findItemInCSV(string $search, bool $case = FALSE, bool $first = TRUE) : array
- Finds key in CSV file
- Assumes first row is headers unless $first === FALSE
- Stores contents of CSV file in $this->lines
- If found, sets $this->pos to the line number of the row found in $this->lines
- @param string $search : any value that might be in the CSV file
- @param bool $case : TRUE: case sensitive; FALSE:
[default]case insensitive search - @param bool $first_row : TRUE
[default]: first row is headers; FALSE: first row is data - @return array
public function updateRowInCsv(string $search, array $data, array $csv_fields = [], bool $case = FALSE) : bool
- Updates row in CSV file
- If you don't supply $csv_fields, assumes no headers
- If no headers, update does delete and then insert
- @param string $search : any value that might be in the CSV file
- @param array $data : array of items to update
- @param array $csv_fields : array of fields names; leave blank if you don't use headers
- @param bool $case : TRUE: case sensitive; FALSE:
[default]case insensitive search - @return bool : TRUE if entry made OK
public static function array2csv(array $data) : string
- This writes an array to CSV
- Credits: https://stackoverflow.com/questions/13108157/php-array-to-csv
- @param array $data : data to be written
- @return string $csv_string
Change Log
tag: v0.2.2 / v0.2.3
- 2022-04-22 DB: Finished testing modifications to Profile
- 2022-04-18 DB: Updated tests
- 2022-02-17 DB: Added option to prevent %%CARDS%% tags from being overwritten + implemented messages marker replacement for static HTML pages + expanded tests
tag: v0.2.1
- 2022-02-16 DB: Updated tests + removed user key from Common\Security\Profile
tag: v0.2.2
- 2022-02-13 DB: Fixed bug whereby you can never login
tag: v0.2.4
2022-05-12 DB:
- Created
Email::trustedSend()that allows you to directly call the core email send function - Refactored
Email::confirmAndSend()to calltrustedSend() - Added
$debugoption to facilitate testing and debugging- If set
TRUEthe email is not actually sent, and aPHPMailerinstance is set toEmail::$phpMailer
- If set
- Added
FileCMSTest\Common\Contact\EmailTesttest class
tag: v0.2.5
FileCMS\Common\Contact\Email::trustedSend()- Fixed bug whereby you were only allowed to send a string to
$ccand$bcc - These inputs now allowed
mixedtypes (expected string|array)
- Fixed bug whereby you were only allowed to send a string to
- Updated
FileCMSTest\Common\Contact\EmailTestandFileCMSTest\Common\Security\ProfileTest FileCMS\Common\Import\Import- Added message if URL not found
- Wrapped
file_get_contents($url)call intry/catchto prevent expected errors from messing up test results
tag: v0.2.6
FileCMS\Common\Contact\Email::trustedSend()- Fixed bug whereby PHPMailer was always set to SMTP regardless of config settings
- Updated
FileCMSTest\Common\Contact\EmailTest- Added tests to see if PHPMailer instance is set to "smtp" or "mail"
tag: v0.2.8
FileCMS\Common\Contact\Email::confirmAndSend()- Removed CAPTCHA verification logic and put into new
AntiSpamclass
- Removed CAPTCHA verification logic and put into new
FileCMS\Common\Contact\AntiSpam- Added static function
verifyCaptcha($config)
- Added static function
tag: v0.2.9
FileCMS\Common\Security\Profile::verify()- Removed type-hint from method signature for backward compatibility
tag: v0.2.10
Date: Sat Jun 25 16:42:03 2022 +0700
- 2022-06-25 DB: Enhancing security in Common\Contact\Email
- 2022-06-21 DB: Minor fix to Common\Security\Profile::verify()
tag: v0.2.11
Date: Sun Jul 10 12:50:24 2022 +0700 FileCMS\Common\Stats\Clicks: Added new column
add()includesjson_encode($_GET)raw_get()doesjson_decode()on new column- Updated
CLICK_HEADERS
tag: v0.2.12
Date: Thu Aug 18 10:33:15 2022 +0700 Modified FileCMS\Common\Stats\Clicks to track all URLs but allow users to add list of URLs to be ignored
tag: v0.2.13
Date: Thu Sep 8 09:54:26 2022 +0700 FileCMS\Common\Stats\Clicks:
- Added
get_by_page_by_month() - Fixed
get_by_path()FileCMS\Common\View\Table: - New class
- Renders multi-dimensional array data
render_table()produces <table> structure with optional CSS classes for table, tr, th and tdrender_as_div()produces table structure using <div class="row"> and <div class="col">
tag: v0.3.0
Date: Thu Nov 3 11:09:53 2022 +0700 Added FileCMS\Common\Data\Csv
- See documentation above for method information
tag: v0.3.1
FileCMS\Common\Security\Profile
- Removed the following methods:
getAuthFileName()build()
- Removed the following constants:
DEFAULT_AUTH_DIRDEFAULT_AUTH_PREFIXAUTH_FILE_TTL
- Added these constants:
PROFILE_KEY = __CLASS__;PROFILE_DEF_SRC = 'HTTP_USER_AGENT';
Profile::init()- Revised to make backwards compatible
- Always adds
$_SERVER[Profile::PROFILE_DEF_SRC]to profile - If profile config keys are present, also adds these to profile
- All keys must be valid
$_SERVERkeys
Profile::verify()- Revised to make backwards compatible
- Added config file as 2nd argument
- Always checks value of
$_SESSION[Profile::PROFILE_KEY][Profile::PROFILE_DEF_SRC] - If profile config keys are present, also confirms these values match
tag: v0.3.2
FileCMS\Common\Data\Csv
- If CSV file doesn't exist, first Csv instance creates it
- If array of headers are supplied, first instance writes headers
- Added new method
deleteRowInCsv() - Slightly refactored
updateRowInCsv()but functionality is the same
tag: v0.3.3
FileCMS\Common\Data\Csv
writeRowToCsv()- If you already have headers in the CSV file, this method will now allow you to write a row without using headers as the 2nd argument
getItemsFromCsv()- Now allows you to read rows even if header count doesn't match
- If your headers are > the count of the CSV headers, just appends empty strings
- If your headers are < the count of the CSV headers, adds fake headers
Header_1,Header_2, etc.
- Also updated tests:
Common\Data\CsvTestCommon\Security\ProfileTest
tag: v0.3.4
Fixed bad sprintf() call in FileCMS\Common\Data\Csv::getItemsFromCsv()
tag: v0.3.5
Arghhhh ... struggling with git
tag: v0.3.6
FileCMS\Common\Data\BigCsv
- New class
- Handles files of any size
- Doesn't use
file() - Low memory consumption
- Not as fast as
Csv
FileCMS\Common\Data\CsvTrait
- Hold common constants and methods
- Used by
CsvandBigCsv - Added new method
array_combine_whatever()- If header count === data count runs
array_combine() - If header count < data count starts creating headers
header_01,header_02etc. - If header count > data count just assigns the headers to the data items and drops remaining headers
- If header count === data count runs
FileCMS\Common\Data\Csv
- Refactored slightly to use
CsvTrait - Added flag
$alltofindItemInCSV()- If set
FALSE(default) only returns 1st match - If set
TRUEreturns all matching rows
- If set
tag: v0.3.7
FileCMS\Common\Data\*
- Moved
CsvTrait::array2csv()andarray_combine_whatever()toFileCMS\Common\Generic\Functions - Moved remaining
CsvTraitfunctionality toCsvBase - Removed
CsvTrait - Refactored
CsvandBigCsvto extendCsvBase
FileCMS\Common\Generic\Functions
public static function array2csv(array $data) : string
- Writes an array to CSV
- Credits: https://stackoverflow.com/questions/13108157/php-array-to-csv
public static function array_combine_whatever(array $headers, array $data, string $prefix = '') : array
- Does the equivalent of
array_combine()even ifcount($headers)doesn't matchcount($data)
tag: v0.3.8
FileCMS\Common\View\Html
- Modified to accept a layout file with a
phtmlextension- Invokes
ob_start()and does a PHPrequireon the layout file - Runs layout as a PHP script
- Allows you to automate things like the copyright date (e.g.
<?= date('Y); ?>
- Invokes
tag: v0.3.9
Updated PHP Minimum Version
- Updated the minimum PHP version to PHP 8
tag: v0.3.10
FileCMS\Common\Install\UpgradeTinymce
- New Composer script
composer upgrade-2026-07, PHP equivalent ofupgrade_2026_07.shfromfilecms-website - Switches the Super editor from CKEditor to TinyMCE: backs up affected files, requires
tinymce/tinymce, copies its assets intopublic/tinymce, and downloads the updatedtemplates/super/edit.phtmlandsrc/upload.php
tag: v0.3.14
- Removed create-project + Moved TinyMCE update to BASH script
tag: v0.3.15
FileCMS\Common\File\Upload
- Fixed
handle(): the finalmove_uploaded_file()call was hardcoded to read$_FILES['upload']['tmp_name']instead of reusing$tmp_file(already extracted from$_FILES[$field]), so any caller using a field name other than'upload'always failed at the move step even after passing validation
tag: v0.3.16
FileCMS\Common\Security\Profile
- Added
Profile::authenticate(array $config, string $name, string $pwd) : bool- Looks up the account by
$name, checkingSUPER.alt_loginsfirst, then falling back to the defaultSUPERusername/password - Verifies
$pwdagainst the stored hash withpassword_verify()
- Looks up the account by
- Added
ProfileTestcoverage forauthenticate(): matching credentials, wrong password, unknown username,alt_loginsmatching,alt_loginsnot falling back to the default account's password, and a missingSUPERconfig
src/config/config.php
SUPER.passwordandSUPER.alt_logins.*.passwordare now expected to bepassword_hash()bcrypt hashes instead of plaintext- Generate one with:
php -r "echo password_hash('your password', PLAIN_TEXT_PASSWORD_BCRYPT), PHP_EOL;"
- Generate one with:
tests/Common/Contact/AntiSpamTest.php
- Switched from
PLAIN_TEXT_PASSWORD_DEFAULTtoPLAIN_TEXT_PASSWORD_BCRYPT, the onlypassword_hash()call in the codebase that wasn't already pinned to it
tag: v0.3.17
FileCMS\Common\Image\Captcha
- Hardened the CAPTCHA against automated (OCR) reading.
writeImages()used to render each character as its own separate PNG file, which handed an automated reader a free segmentation step -- it never had to figure out where one character ends and the next begins. It now renders the whole phrase as a single distorted image:- Each character is drawn directly onto one shared canvas with a randomly chosen font (from the new
font_filesconfig key), size, rotation and baseline - Characters are placed with overlapping ("negative kerning") spacing so adjacent glyphs touch
- The finished image is warped with a new 2D wave distortion,
FileCMS\Common\Image\Strategy\Wave - Noise lines/dots are now colored close to the text's own color range instead of fully random, so they can't be stripped out with simple color thresholding
- Wired up the
rotate_min/rotate_max,line_min/line_maxanddot_min/dot_maxconfig keys, which existed in the config skeleton but were never actually read by the code - Added new config keys:
font_files,overlap_min,overlap_max,wave_x_amplitude,wave_y_amplitude
- Each character is drawn directly onto one shared canvas with a randomly chosen font (from the new
- Added
CaptchaTest,SingleCharTestandWaveTestcoverage
FileCMS\Common\Image\SingleChar
- Constructor now accepts an optional shared
\GdImage, so multiple characters can be drawn onto the same canvas - Switched from a 256-color palette image to a truecolor image (a shared canvas with several characters plus noise can easily allocate more than 256 distinct colors)
randFgColor()now accepts optional$min/$maxbounds
FileCMS\Common\Image\Strategy\LineFill / DotFill
writeFill()now accepts optional$colorMin/$colorMaxbounds instead of always using fully random 0-255 colors
FileCMS\Common\Image\Strategy\Wave
- New class: ripples an image with a 2D sine-wave pixel displacement (
Wave::distort())
src/config/config.php
CAPTCHA.rotate_min/rotate_maxdefault changed from -50/50 to -33/33;line_min/line_maxfrom 5/50 to 12/24;dot_min/dot_maxfrom 20/60 to 30/50 (tuned empirically for the new fused-image rendering)- Added
font_files,overlap_min,overlap_max,wave_x_amplitude,wave_y_amplitude
tag: v0.3.18
- Updated README.md
- Added
get_password_hash.shto project root