Markus Michalski FAQ

Custom CSS Loader: Technical Details

How Custom CSS Loader works: flow with output buffer as a diagram, context detection via SCRIPT_NAME, file checks, classes, auto-update and file structure.

Deutsche Version

Back to overview

Custom CSS Loader: Technical Details

Flow

graph TD
    A["Plugin file is included"] --> B["ob_start with callback"]
    C["CustomCssLoaderPlugin.bootstrap()"] --> D{"Option enabled on?"}
    D -->|"No"| E["No inclusion prepared"]
    D -->|"Yes"| F["CssLoaderOrchestrator.prepare()"]
    F --> G{"Context staff or client?"}
    G -->|"No"| E
    G -->|"Yes"| H["FilesystemCssDiscovery: find files"]
    H --> I["HtmlCssRenderer: build link tags"]
    I --> J["Page is rendered"]
    B --> J
    J --> K["Callback: CssLoaderOrchestrator.injectIntoBuffer()"]
    K --> L["OutputBufferInjectionStrategy.inject(): tags before the first head end"]

The output buffer is started as soon as the plugin file is included, that is before bootstrap(). The buffer callback returns the page unchanged as long as no link tags were prepared, and catches errors (entry in the PHP error log, page is preserved).

Context detection

ProductionContextDetector decides from constants and $_SERVER['SCRIPT_NAME'], not from the URL path.

Context Condition
staff Constant OSTSCPINC defined or SCRIPT_NAME contains /scp/
client Constant OSTCLIENTINC defined or SCRIPT_NAME contains neither /scp/ nor /api/ and ends in .php
none all other cases, for example /api/ or a call without a script name

The staff check takes precedence over the client check.

Finding and checking files

FilesystemCssDiscovery reads assets/custom/css/*.css with glob() (alphabetical) and checks every file:

  1. realpath() must resolve, otherwise the file (for example a broken symlink) is skipped.
  2. The real path must lie below the real CSS folder, otherwise the file is blocked and noted in the PHP error log.
  3. The file name must match ^[a-zA-Z0-9][a-zA-Z0-9_-]*\.css$, otherwise it is blocked and noted.
  4. The patterns /staff/i and /client/i are checked in this order, the first match wins.

HtmlCssRenderer checks the file name a second time, appends ?v=<modification time> (only when the time is greater than 0), escapes the URL with htmlspecialchars() and produces <link rel="stylesheet" href="...">. The base URL is ROOT_PATH plus assets/custom/css.

Injection

OutputBufferInjectionStrategy looks for the first </head> (case-insensitive) and inserts the block directly before it. If </head> is missing or there are no link tags, the page stays unchanged.

Classes

Class Responsibility
CustomCssLoaderPlugin Plugin class: bootstrap(), enable(), version check, folder and demo files
CustomCssLoaderConfig Configuration fields (config.php)
CssLoaderOrchestrator prepare() collects the link tags, injectIntoBuffer() inserts them
ProductionContextDetector Detect staff or client
FilesystemCssDiscovery Find and check files
HtmlCssRenderer Produce link tags
OutputBufferInjectionStrategy Insert tags into the buffer

There are also test helpers (TestContextDetector) and older methods in the plugin class marked as deprecated, which are kept for the unit tests.

Auto-update and version

On bootstrap() the plugin calls checkVersion(). If installed_version is missing or lower than the version in plugin.php, performUpdate() ensures the CSS folder exists and stores the new version. Demo files are not copied during that.

File structure

custom-css-loader/
β”œβ”€β”€ plugin.php
β”œβ”€β”€ class.CustomCssLoaderPlugin.php
β”œβ”€β”€ config.php
β”œβ”€β”€ composer.json
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ CssLoaderOrchestrator.php
β”‚   β”œβ”€β”€ Context/
β”‚   β”œβ”€β”€ Discovery/
β”‚   β”œβ”€β”€ Rendering/
β”‚   └── Injection/
β”œβ”€β”€ assets/demo/
β”œβ”€β”€ vendor/                  # only in the release ZIP or after composer install
└── tests/                   # PHPUnit