Version 3.0.0
I can’t seem to get out of Parkinson’s law, sorry.
Table of Contents
It took me four years to get convinced to release this version, and it was out of a sense of urgency. As someone who develops my own work (I don’t call it a job), there is no barrier that prevents me from stopping at some point so that a work is considered stable and viable. I am stuck with my belief that the last commit I have pushed into the repository is always the best version of my work. Until one day someone warned me about a security vulnerability in my application that was too severe to ignore.
It was all caused by this one line, which turned out not to be very reliable in securing the file system:
$path = strtr($path, ['../' => ""]);
I will provide the link that explains how a hacker could damage a user’s system by exploiting this vulnerability in the future, but if you are currently using the latest version before I publish this article, I suggest you to change that line to this one, which should cover most of the vulnerability issues. I would also suggest you to update the User and Panel extension to the latest version as well:
while (false !== strpos($path, '../')) {
$path = strtr($path, ['../' => ""]);
}
I suggest you also read the version 3.0.1 release article to get a more secure update. Apart from that, here are the changes that I have made over the past 4 years.
Class-Less Markup
Extensions should not specify classes in their HTML markup (except for certain extensions that produce complex HTML markup, such as the Comment extension). Alternatively, extension developers can consider using the attributes as standardized by WAI-ARIA, or else they can use custom elements for client-side rendering purposes.
Secure Extension and Layout Loader
Loading extensions/layouts is now wrapped in a try/catch block, so any extensions/layouts that are not compatible or broken can be disabled immediately. If you ever install an extension/layout and have trouble getting it to work, check to see if the index.php
file in the extension/layout you just installed has been renamed to an .index.php
file. If so, the core application may have rejected it. If you have the test feature enabled, you should find log files in the .\engine\log
folder named error-x
(for extension rejections) and error-y
(for layout rejections), or error
(for general rejections).
Reduction of Hook Sortation Task
A hook will be sorted once for each addition. This can improve the performance of hooks, especially when they are executed inside a loop. Previously, every time a Hook::fire()
method is called, a task to sort the added hooks by the stack
property is performed. This task can actually be reduced by performing it as rarely as possible.
Sorting is quite expensive. It is like doing a preparation loop before doing the actual loop: the first loop is to re-order the hooks, and the second loop is to execute the hooks in order.
There are some indicators that will be used to trigger the re-sorting task of the hooks:
- Calling
Hook::set()
method will trigger the sorting task. - Calling
Hook::get()
method will not trigger the sorting task. - Calling
Hook::let()
method will not trigger the sorting task as it’s just an action to remove or halt a hook. - Calling
Hook::fire()
method will not trigger the sorting task, as it’s already done by theHook::set()
method. - If there is only one hook in the current hook container, the sorting task will not be performed.
Layout Switcher Feature
The Layout switcher feature is now available (again). Just after 2 years, 2 months and 28 days have passed since the release of version 2.2.0, I started to realize that I have intolerable difficulties in synchronizing the local layout file structure to the remote repository associated with that layout. It’s very difficult for me to develop automatic update features for the layout, because when a layout is uploaded, its folder name is gone and all the files in it are combined into one in the .\lot\layout
folder. In contrast, extensions are uploaded with the folder, so the mapping between the extension name and the remote repository associated with that extension can be retained.
Typical extension file structure in version 2.6.4:
.\
└── lot\
└── x\
└── foo-bar\ ← this is `foo-bar` extension, obviously!
├── engine\
├── lot\
├── about.page
├── index.php
└── state.php
Typical extension repository structure in version 2.6.4:
https://github.com/
└── mecha-cms/
└── x.foo-bar/
└── foo-bar/
├── engine/
├── lot/
├── about.page
├── index.php
└── state.php
Typical layout file structure in version 2.6.4:
.\
└── lot\
└── layout\ ← what layout is this?
├── asset\
├── about.page
├── index.php
└── state.php
Typical layout repository structure in version 2.6.4:
https://github.com/
└── mecha-cms/
└── layout.foo-bar/
├── asset/
├── about.page
├── index.php
└── state.php
Initially, I tried to make a git
property proposal in about.page
file 1 to solve this problem. Hopefully this can also be applied to third-party extensions uploaded outside the https://github.com/mecha-cms organization, so that they can tell the Panel about their repository name in the git
property of an about.page
file to be fetched.
---
title: Layout Title
description: Layout description.
git: 'taufik-nurrohman/layout.foo-bar'
type: Markdown
...
Lorem ipsum dolor sit amet.
This should work fine when the git
property exists, but gets a bit hacky when I have to guess the repository name by creating a folder name from the layout title. Layout title is meant to be freely definable and has no relation to the internal structure of the file system. Repository name errors will occur when an unaware developer decides to change their layout title without changing their remote repository name.
Layout Folder Name
In order to match the naming style of the extension folder, I also decided to change the path of the layout folder from .\lot\layout
to .\lot\y
. We now have unique folders called .\lot\x
and .\lot\y
, just like in algebraic mathematics. Here, x becomes an abbreviation for extension (noun) and extend (verb), and y becomes an abbreviation for layout (noun) and yield (verb).
.\
└── lot\
├── asset\
├── page\
├── x\
│ ├── extension-1\
│ ├── extension-2\
│ ├── extension-3\
│ └── …
└── y\
├── layout-1\
├── layout-2\
├── layout-3\
└── …
Layout Behavior
Layout now behaves like extensions.
I have made the .\lot\y\*\index.php
file mandatory, so that if it does not exist, the front-end view will display a blank web page. This allows Mecha to continue to be a “tiny web framework-ish” CMS, rather than a “has the potential to be full feature-ish” CMS that starts out as a blogging CMS, just like WordPress. One could use Mecha as an application without a front-end 2 by having a layout that does not display any content.
Layout Error
File 404.php
is no longer a requirement for a layout. To make an error page, you can mix it in the page.php
and pages.php
files by verifying the existence of a page:
<?php if ($page->exist): ?>
<h2>
<?= $page->title; ?>
</h2>
<div>
<?= $page->content; ?>
</div>
<?php else: ?>
<p role="status">
<?= i('%s does not exist.', 'Page'); ?>
</p>
<?php endif; ?>
Fewer Classes, More Functions
I finally threw away some classes that were more of a cosmetic than a necessity, and substituted them with more practical generic functions. Some of the classes that have been thrown away include:
Cache
I simply provide a .\lot\cache
folder. It is up to you how you want to generate the cache file, I don’t care. An example of how a cache file might be created would be something like this:
$comments = [];
$comments_cache = LOT . D . 'cache' . D . 'comments.php';
if (!exist($comments_cache, 1)) {
save($comments_cache, '<?' . 'php return' . z($comments = recent_comments()) . ';', 0600);
$comments = new Comments($comments);
} else {
$comments = new Comments(require $comments_cache);
}
foreach ($comments as $comment) { … }
Client
Method Client::IP()
is now replaced by function ip()
, and method Client::UA()
is now replaced by function ua()
.
Cookie
This class is now replaced by a simple cookie()
function.
Files
It does make sense as a base class for Pages
, but I didn’t find any use for the Folders
class, so I decided to throw away both of them. This class name is also in conflict with the choices I have made for the names of the classes Cookie
, Get
, Post
, Request
, Server
, and Session
based on the global variable names $_COOKIE
, $_GET
, $_POST
, $_REQUEST
, $_SERVER
, and $_SESSION
.
I have no room for $_FILES
variable name 🙁
Folders
There has been no practical use for it so far, so I threw it away!
Get
I decided to throw away this class, just as I decided to throw away the Files
class. This was to resolve the inconsistency in class naming semantics. Methods like Get::get()
, Get::let()
, and Get::set()
should be replaceable in this fashion:
$status = get($_GET, 'user.status'); // `Get::get('user.status')`
let($_GET, 'user.status'); // `Get::let('user.status')`
set($_GET, 'user.status', 1); // `Get::set('user.status', 1)`
Guard
All of the methods in this class are now available as generic functions: abort()
, check()
, token()
.
Path
Not cool. It was meant to be paired with the URL
class, but this class is more of a converter where the methods are callable statically.
Post
I decided to throw away this class, just as I decided to throw away the Get
class.
Request
I decided to throw away this class, just as I decided to throw away the Get
class.
Route
Route complexity has been taken care of by a simple hook named route
. From the very beginning, the Route
class is really just a syntactic sugar for constructing “if” conditions on the URL::path()
value. Both of the following instructions can be considered identical:
Route::set("", function () { … });
Route::set('*', function ($path) { … });
Route::set('product/:name', function ($name) { … });
Route::start();
$lot = [];
$route = trim($url->path ?? "", '/');
$then = function () {};
if ("" === $route) {
$then = function () { … };
}
if ("" !== $route) {
$lot = [$route];
$then = function ($path) { … };
}
if (preg_match('/^product\/([^\/]+)$/', $route, $m)) {
$lot = [$m[1]];
$then = function ($name) { … };
}
call_user_func($then, $lot);
Just because routes are executed at the very end and have a function stack feature, it becomes a safer option for extensions to add custom pages because extensions can control the execution priority of the route functions by adding a stack value to the route definition so that the core system can execute them in order. But in fact, this can be implemented with a simple hook system as well:
echo Hook::fire('route', [null, $url->path]);
Now a developer can add new route rules and override the existing route rules in the same way that a developer would add a new hook:
Hook::set('route', function ($content, $path) { … });
Of course, it doesn’t have pattern functionality and such, but other developers can make extensions for that and run the pattern parser against the $path
value in the hook.
Server
I decided to throw away this class, just as I decided to throw away the Get
class.
Session
I decided to throw away this class, just as I decided to throw away the Get
class.
SGML
The SGML syntax is too generic, and to develop a parser for its alternative syntax (the short tag syntax) is a bit of a pain, so I decided to focus on the more consistent and strict XML syntax instead.
Miscellaneous
- #159 The
DEBUG
constant has been renamed toTEST
to make it consistent with thetest()
function. - The
DS
constant has been renamed toD
. - The
ROOT
constant has been renamed toPATH
. - #165 The
$parent
variable is no longer available through the Page extension, but methodPage::parent()
is available on variable$page
as the replacement. - #166: The
get()
,has()
,let()
, andset()
methods are mostly no longer present in instantiatable classes. They all have been replaced by the built-in__get()
,__isset()
,__set()
, and__unset()
methods. - #167 The
e()
function will no longer evaluate string in the form of a JSON to keep it reversible with thes()
function. - #233 You now have the option 3 to recursively parse elements with
HTML
andXML
classes. - Alert, Art, and Form extensions are no longer part of the core extensions.
0 Comments
No comments yet.