Skip to content

Commit

Permalink
Merge pull request #1 from alleyinteractive/feature
Browse files Browse the repository at this point in the history
Base package
  • Loading branch information
srtfisher authored Jul 23, 2023
2 parents 4f09e3d + 3019676 commit 9a5538a
Show file tree
Hide file tree
Showing 13 changed files with 212 additions and 168 deletions.
20 changes: 0 additions & 20 deletions .github/workflows/unit-test.yml

This file was deleted.

2 changes: 1 addition & 1 deletion .phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
<properties>
<property name="prefixes" type="array">
<element value="alleyinteractive" />
<element value="alley" />
<element value="wp_plugin_loader" />
</property>
</properties>
Expand Down
26 changes: 21 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,37 @@ Code-enabled WordPress plugin loading.

## Installation

You can install the package via composer:
You can install the package via Composer:

```bash
composer require alleyinteractive/wp-plugin-loader
```

## Usage

Activate the plugin in WordPress and use it like so:
Load the package via Composer and use it like so:

```php
$plugin = Alley\WP\WP_Plugin_Loader\WP_Plugin_Loader\WP_Plugin_Loader();
$plugin->perform_magic();
use Alley\WP\WP_Plugin_Loader;

new WP_Plugin_Loader( [
'plugin/plugin.php',
'plugin-name-without-file',
] );
```

The plugin loader will load the specified plugins, be it files or folders under
`plugins`/`client-mu-plugins`, and mark them as activated on the plugins screen.

Also supports preventing activations of plugins via the plugins screen (useful
to fully lock down the plugins enabled on site);

```php
use Alley\WP\WP_Plugin_Loader;

( new WP_Plugin_Loader( [ ... ] )->prevent_activations();
```
<!--front-end-->

## Testing

Run `composer test` to run tests against PHPUnit and the PHP code in the plugin.
Expand Down
20 changes: 6 additions & 14 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "alleyinteractive/wp-plugin-loader",
"description": "Code-enabled WordPress plugin loading",
"type": "wordpress-plugin",
"type": "library",
"keywords": [
"alleyinteractive",
"wp-plugin-loader"
Expand All @@ -20,7 +20,6 @@
},
"require-dev": {
"alleyinteractive/alley-coding-standards": "^1.0",
"mantle-framework/testkit": "^0.11",
"szepeviktor/phpstan-wordpress": "^1.1"
},
"config": {
Expand All @@ -31,27 +30,20 @@
},
"sort-packages": true
},
"extra": {
"wordpress-autoloader": {
"autoload": {
"Alley\\WP\\WP_Plugin_Loader": "src"
},
"autoload-dev": {
"Alley\\WP\\WP_Plugin_Loader\\Tests": "tests"
}
}
"autoload": {
"files": [
"src/class-wp-plugin-loader.php"
]
},
"minimum-stability": "dev",
"prefer-stable": true,
"scripts": {
"phpcbf": "phpcbf .",
"phpcs": "phpcs .",
"phpunit": "phpunit",
"phpstan": "phpstan --memory-limit=512M",
"test": [
"@phpcs",
"@phpstan",
"@phpunit"
"@phpstan"
]
}
}
6 changes: 3 additions & 3 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ parameters:
# Level 9 is the highest level
level: max

scanFiles:
- stubs/constants.stub

paths:
- blocks/
- entries/
- src/
- wp-plugin-loader.php

# ignoreErrors:
# - '#PHPDoc tag @var#'
Expand Down
18 changes: 0 additions & 18 deletions phpunit.xml

This file was deleted.

179 changes: 178 additions & 1 deletion src/class-wp-plugin-loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,185 @@
namespace Alley\WP\WP_Plugin_Loader;

/**
* Example Plugin
* WordPress Plugin Loader
*/
class WP_Plugin_Loader {
/**
* Array of loaded plugins.
*
* @var array<int, string>
*/
protected array $loaded_plugins = [];

/**
* Flag to prevent any plugin activations for non-code activated plugins.
*
* @var bool
*/
protected bool $prevent_activations = false;

/**
* Constructor.
*
* @param array<int, string> $plugins Array of plugins to load.
*/
public function __construct( public array $plugins = [] ) {
if ( did_action( 'plugins_loaded' ) ) {
trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
'WP_Plugin_Loader should be instantiated before the plugins_loaded hook.',
E_USER_WARNING
);
}

$this->load_plugins();

add_filter( 'plugin_action_links', [ $this, 'filter_plugin_action_links' ], 10, 2 );
add_filter( 'option_active_plugins', [ $this, 'filter_option_active_plugins' ] );
add_filter( 'pre_update_option_active_plugins', [ $this, 'filter_pre_update_option_active_plugins' ] );
}

/**
* Prevent any plugin activations for non-code activated plugins.
*
* @todo Harden with a capability check.
*
* @param bool $prevent Whether to prevent activations.
*/
public function prevent_activations( bool $prevent = true ): void {
$this->prevent_activations = $prevent;
}

/**
* Load the requested plugins.
*/
protected function load_plugins(): void {
$client_mu_plugins = is_dir( WP_CONTENT_DIR . '/client-mu-plugins' );

foreach ( $this->plugins as $plugin ) {
if ( file_exists( WP_PLUGIN_DIR . "/$plugin" ) && ! is_dir( WP_PLUGIN_DIR . "/$plugin" ) ) {
require_once WP_PLUGIN_DIR . "/$plugin";

$this->loaded_plugins[] = trim( $plugin, '/' );

continue;
} elseif ( $client_mu_plugins && file_exists( WP_CONTENT_DIR . "/client-mu-plugins/$plugin" ) && ! is_dir( WP_CONTENT_DIR . "/client-mu-plugins/$plugin" ) ) {
$plugin = ltrim( $plugin, '/' );

require_once WP_CONTENT_DIR . "/client-mu-plugins/$plugin";

continue;
} elseif ( false === strpos( $plugin, '.php' ) ) {
// Attempt to locate the plugin by name if it isn't a file.
$sanitized_plugin = $this->sanitize_plugin_name( $plugin );

$paths = [
WP_PLUGIN_DIR . "/$sanitized_plugin/$sanitized_plugin.php",
WP_PLUGIN_DIR . "/$sanitized_plugin/plugin.php",
WP_PLUGIN_DIR . "/$sanitized_plugin.php",
];

$match = false;

foreach ( $paths as $path ) {
if ( file_exists( $path ) ) {
require_once $path; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable

$match = true;

$this->loaded_plugins[] = trim( substr( $path, strlen( WP_PLUGIN_DIR ) + 1 ), '/' );
break;
}
}

// Bail if we found a match.
if ( $match ) {
continue;
}
}

$error_message = sprintf( 'WP Plugin Loader: Plugin %s not found.', $plugin );

trigger_error( esc_html( $error_message ), E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error

if ( extension_loaded( 'newrelic' ) && function_exists( 'newrelic_notice_error' ) ) {
newrelic_notice_error( $error_message );
}

// Bye bye!
die( esc_html( $error_message ) );
}
}

/**
* Ensure code activated plugins are shown as such on core plugins screens
*
* @param array<string, string> $actions The existing list of actions.
* @param string $plugin_file The path to the plugin file.
* @return array<string, string>
*/
public function filter_plugin_action_links( $actions, $plugin_file ): array {
$screen = get_current_screen();

if ( in_array( $plugin_file, $this->loaded_plugins, true ) ) {
unset( $actions['activate'] );
unset( $actions['deactivate'] );
$actions['wp-plugin-loader-code-activated-plugin'] = __( 'Enabled via code', 'wp-plugin-loader' );

if ( $screen && is_a( $screen, 'WP_Screen' ) && 'plugins' === $screen->id ) {
unset( $actions['network_active'] );
}
} elseif ( $this->prevent_activations ) {
unset( $actions['activate'] );
unset( $actions['deactivate'] );
}

return $actions;
}

/**
* Filters the list of active plugins to include the ones we loaded via code.
*
* @param array<int, string> $value The existing list of active plugins.
* @return array<int, string>
*/
public function filter_option_active_plugins( $value ): array {
if ( ! is_array( $value ) ) {
$value = [];
}

$value = array_unique( array_merge( $value, $this->loaded_plugins ) );

sort( $value );

return $value;
}

/**
* Exclude code-active plugins from the database option.
*
* @param array<int, string> $value The saved list of active plugins.
* @return array<int, string>
*/
public function filter_pre_update_option_active_plugins( $value ) {
if ( ! is_array( $value ) ) {
$value = [];
}

$value = array_diff( $value, $this->loaded_plugins );

sort( $value );

return $value;
}

/**
* Helper function to sanitize plugin folder name.
*
* @param string $folder Folder name.
* @return string Sanitized folder name
*/
protected function sanitize_plugin_name( string $folder ): string {
$folder = preg_replace( '#([^a-zA-Z0-9-_.]+)#', '', $folder );
return str_replace( '..', '', (string) $folder ); // To prevent going up directories.
}
}
3 changes: 3 additions & 0 deletions stubs/constants.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php
define( 'WP_CONTENT_DIR', 'wp-content' );
define( 'WP_PLUGIN_DIR', 'wp-content/plugins' );
16 changes: 0 additions & 16 deletions tests/bootstrap.php

This file was deleted.

17 changes: 0 additions & 17 deletions tests/class-test-case.php

This file was deleted.

Loading

0 comments on commit 9a5538a

Please sign in to comment.