Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### TBD
## [1.1.0] - 2026-05-13

### Added

- `AdminAjax::endpoint()` gains an `$auth_only` parameter; when `true`, the `wp_ajax_nopriv_` hook is not registered, restricting the action to authenticated users only.
- `AdminAjax::verify_nonce()` static helper — verifies a nonce from `$_POST` and calls `wp_die()` on failure.
- `WPModelFactory::wrap_models()` and `WPModelFactory::apply_meta_fields()` as base implementations, eliminating duplicated code across all four factory subclasses. `wrap()` is now abstract.

### Changed

- `WPModelFactory::wrap()` is now `abstract` — subclasses that previously relied on the no-op base must implement it explicitly.
- `WPMeta` interface method signatures updated to use strict PHP 8.1 types (`?string $key`, union return types).
- `WPTaxonomyTerms` interface method signatures updated to use strict PHP 8.1 types.
- PHP 8.1 typed properties, parameter types, and return types added throughout the framework.
- All model classes (`GenericPostModel`, `UserModel`, `TaxonomyModel`, `TaxonomyTermModel`, `CommentModel`) annotated with `#[\AllowDynamicProperties]` for intentional dynamic property use.
- `gettype()` checks replaced with `instanceof` across all model classes and factories.
- `isset`/ternary patterns replaced with null coalescing (`??`) throughout.

### Fixed

- `Application::setup_controllers()` collapsed from three separate controller loops into one, eliminating 6 `apply_filters()` calls per controller per request and fixing a correctness bug where filter-replaced controller instances were not persisted back to `$this->controllers`, causing later loops to operate on the original (pre-filter) instance.
- `REST::handle_callback()` no longer redundantly re-assigns `$this->current_request` and re-calls `set_current_endpoint()` — both were already set by `handle_perm_callback()`, which WordPress always runs first; this eliminates a full endpoint scan on every matched REST request.
- `EmailView::send()` now correctly returns `false` if any recipient fails when passed an array of addresses; previously the return value of each recursive call was discarded and the method always returned `false` for multi-recipient sends regardless of outcome.
- `CommentModelFactory`: fixed typo `$commend` → `$comment` (caused undefined variable on lookup failure); `create_comment()` now passes the `$comment` argument; `delete_comment()` now uses `comment_ID`; `update_comment()` captures the return value.
- `Config::get()` no longer overwrites `$default` before it is used; `glob()` calls fall back to `[]` instead of `false`.
- `Route`: `REQUEST_URI` sanitised via `sanitize_url(wp_unslash(...))` and parsed with `parse_url()` instead of `strtok()`; match check corrected from `!empty($matches)` to checking `$match` directly.
- `REST`: `extract()` calls removed and replaced with explicit key access; `_doing_it_wrong()` is now emitted when `permission_callback` is absent, discouraging open endpoints.
- `EmailView`: `$content_templater->subject` dynamic property assignment replaced — subject is now passed through `$content_params` so it is available inside the email body template.
- `Templater`: constructor bug where `$this->params['template_slug']` referenced a removed local variable; corrected to `$this->slug`.

### Security

- `REQUEST_URI` in `Route` sanitised with `sanitize_url(wp_unslash(...))` before use.
- `WP_ENV` in `Config` restricted to `[a-zA-Z0-9_-]` via `preg_replace` to prevent path traversal in config file lookups.
- `EmailView::shortcodes()` switched from `preg_replace` with `$1`/`$2` backreferences to `preg_replace_callback`, eliminating potential backreference injection via user-controlled param values.
- `REST` emits `_doing_it_wrong()` when `permission_callback` is not provided, discouraging inadvertently public endpoints.

## [1.0.0-alpha] - 2024-11-01

Expand All @@ -16,5 +51,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Initial package release v1.0.0-alpha.


[unreleased]: https://github.com/TheCodeCompany/wpmvc/compare/v1.0.0-alpha...HEAD
[unreleased]: https://github.com/TheCodeCompany/wpmvc/compare/v1.1.0...HEAD
[1.1.0]: https://github.com/TheCodeCompany/wpmvc/compare/v1.0.0-alpha...v1.1.0
[1.0.0-alpha]: https://github.com/TheCodeCompany/wpmvc/releases/tag/v1.0.0-alpha
59 changes: 17 additions & 42 deletions src/Core/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
namespace WPMVC\Core;

use WPMVC\Library\Config;
use WPMVC\Library\ControllerSetup;
use WPMVC\Library\Route;
use WPMVC\Library\REST;
use WPMVC\Library\AdminAjax;
Expand All @@ -37,28 +36,28 @@ class Application {
*
* @var string
*/
protected $name = '';
protected string $name = '';

/**
* The root directory of the application.
*
* @var string
*/
protected $directory = '';
protected string $directory = '';

/**
* All of the controllers defined in the application.
*
* @var array
*/
protected $controllers = [];
protected array $controllers = [];

/**
* The config helper instance.
*
* @var \WPMVC\Library\Config
* @var Config
*/
protected $config = null;
protected ?Config $config = null;

/**
* Constructor.
Expand All @@ -67,7 +66,7 @@ class Application {
* @param string $directory The root directory of the application.
* @param array $controllers All of the applications controllers.
*/
public function __construct( $name, $directory, $controllers ) {
public function __construct( string $name, string $directory, array $controllers ) {

$this->name = $name;
$this->directory = $directory;
Expand All @@ -84,59 +83,35 @@ public function __construct( $name, $directory, $controllers ) {
*
* @return void
*/
public function setup_controllers() {
public function setup_controllers(): void {

// Set helper instances.
$route = new Route();
$rest = new REST();
$ajax = new AdminAjax();
foreach ( $this->controllers as $controller ) {

/**
* Apply filters to the controller before we set instances.
*/
foreach ( $this->controllers as $index => $controller ) {

$controller = apply_filters( 'wpmvc_pre_controller_set_instances', $controller );

$controller->set_route_instance( $route );
$controller->set_rest_instance( $rest );
$controller->set_admin_ajax_instance( $ajax ); // TODO: implement and test.
$controller->set_admin_ajax_instance( $ajax );

/**
* Apply filters to the controller after we set instances.
*/
$controller = apply_filters( 'wpmvc_post_controller_set_instances', $controller );
}

// Set config.
foreach ( $this->controllers as $controller ) {

/**
* Apply filters to the controller before we set the config.
*/
$controller = apply_filters( 'wpmvc_pre_controller_set_config', $controller );

$controller->set_config_instance( $this->config );

/**
* Apply filters to the controller after we set the config.
*/
$controller = apply_filters( 'wpmvc_post_controller_set_config', $controller );
}

// Set up each controller.
foreach ( $this->controllers as $controller ) {

/**
* Apply filters to the controller before we set it up.
*/
$controller = apply_filters( 'wpmvc_pre_controller_set_up', $controller );

$controller->set_up();

/**
* Apply filters to the controller after we set it up.
*/
$controller = apply_filters( 'wpmvc_post_controller_set_up', $controller );

// Write back so filter-replaced instances are persisted for callers.
$this->controllers[ $index ] = $controller;
}
}

Expand All @@ -145,7 +120,7 @@ public function setup_controllers() {
*
* @return Config
*/
public function get_config() {
public function get_config(): Config {

return $this->config;
}
Expand All @@ -155,7 +130,7 @@ public function get_config() {
*
* @return string
*/
public function get_directory() {
public function get_directory(): string {

return $this->directory;
}
Expand All @@ -165,7 +140,7 @@ public function get_directory() {
*
* @return string
*/
public function get_name() {
public function get_name(): string {

return $this->name;
}
Expand All @@ -175,7 +150,7 @@ public function get_name() {
*
* @return void
*/
protected function load_config() {
protected function load_config(): void {

$this->config = new Config( $this );
$this->config->autoload();
Expand Down
32 changes: 13 additions & 19 deletions src/Core/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,59 +28,53 @@
* ```
*/
abstract class Controller {
/*
* TODO
* If the "app" stuff comes under a different library, then only
* config should be defined in the base controller class.
* All of the other 'helper' instances should be loaded separately.
*/

/**
* The config helper instance.
* This is automatically set when the controller is booted.
*
* @var Config
*/
protected $config = null;
protected ?Config $config = null;

/**
* Route helper instance.
* This is automatically set when the controller is booted.
*
* @var Route
*/
protected $route;
protected Route $route;

/**
* REST helper instance.
* This is automatically set when the controller is booted.
*
* @var REST
*/
protected $rest;
protected REST $rest;

/**
* Admin AJAX helper instance.
* This is automatically set when the controller is booted.
*
* @var AdminAjax
*/
protected $admin_ajax;
protected AdminAjax $admin_ajax;

/**
* Called automatically at `plugins_loaded`.
* This must be overridden by child controllers.
*
* @return void
*/
abstract public function set_up();
abstract public function set_up(): void;

/**
* Get the Config instance.
*
* @return Config
*/
public function get_config() {
public function get_config(): ?Config {
return $this->config;
}

Expand All @@ -89,7 +83,7 @@ public function get_config() {
*
* @return Route
*/
public function get_route() {
public function get_route(): Route {
return $this->route;
}

Expand All @@ -98,7 +92,7 @@ public function get_route() {
*
* @return REST
*/
public function get_rest() {
public function get_rest(): REST {
return $this->rest;
}

Expand All @@ -107,7 +101,7 @@ public function get_rest() {
*
* @return AdminAjax
*/
public function get_admin_ajax() {
public function get_admin_ajax(): AdminAjax {
return $this->admin_ajax;
}

Expand All @@ -118,7 +112,7 @@ public function get_admin_ajax() {
*
* @return void
*/
public function set_config_instance( Config $config ) {
public function set_config_instance( Config $config ): void {

$this->config = $config;
}
Expand All @@ -130,7 +124,7 @@ public function set_config_instance( Config $config ) {
*
* @return void
*/
public function set_route_instance( Route $route ) {
public function set_route_instance( Route $route ): void {

$this->route = $route;
}
Expand All @@ -142,7 +136,7 @@ public function set_route_instance( Route $route ) {
*
* @return void
*/
public function set_rest_instance( REST $rest ) {
public function set_rest_instance( REST $rest ): void {

$this->rest = $rest;
}
Expand All @@ -154,7 +148,7 @@ public function set_rest_instance( REST $rest ) {
*
* @return void
*/
public function set_admin_ajax_instance( AdminAjax $admin_ajax ) {
public function set_admin_ajax_instance( AdminAjax $admin_ajax ): void {

$this->admin_ajax = $admin_ajax;
}
Expand Down
8 changes: 4 additions & 4 deletions src/Core/View.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ abstract class View {
*
* @var array
*/
protected $params;
protected array $params = [];

/**
* Set a parameter that will be passed to the template.
Expand All @@ -28,7 +28,7 @@ abstract class View {
* @param mixed $value The value of the parameter.
* @return void
*/
public function set_param( $name, $value ) {
public function set_param( string $name, mixed $value ): void {
$this->params[ $name ] = $value;
}

Expand All @@ -38,7 +38,7 @@ public function set_param( $name, $value ) {
* @param string $name The name/slug of the parameter.
* @return mixed
*/
public function get_param( $name ) {
return $this->params[ $name ];
public function get_param( string $name ): mixed {
return $this->params[ $name ] ?? null;
}
}
Loading