1+ <?php
2+ /**
3+ * Plugin Name: Kntnt Must Use Scripts
4+ * Description: Copies PHP files with namespace Kntnt\Mu_Scripts\* from scripts subfolder to mu-plugins directory on activation, removes them on deactivation.
5+ * Version: 1.0.0
6+ * Plugin URI: https://www.kntnt.com/
7+ * Author: Kntnt
8+ * Author URI: https://www.kntnt.com/
9+ * Requires at least: 6.0
10+ * Requires PHP: 8.3
11+ * License: GPL v2 or later
12+ * License URI: https://www.gnu.org/licenses/gpl-2.0.html
13+ */
14+
15+ declare ( strict_types = 1 );
16+
17+ namespace Kntnt \Mu_Scripts ;
18+
19+ if ( ! defined ( 'ABSPATH ' ) ) {
20+ exit ;
21+ }
22+
23+ enum LogLevel: string {
24+
25+ case INFO = 'info ' ;
26+ case ERROR = 'error ' ;
27+ case WARNING = 'warning ' ;
28+
29+ }
30+
31+ class Plugin {
32+
33+ /**
34+ * Required namespace prefix for mu-plugin files
35+ */
36+ private const string REQUIRED_NAMESPACE_PREFIX = 'Kntnt \\Mu_Scripts \\' ;
37+
38+ /**
39+ * Plugin file path
40+ */
41+ private static string $ plugin_file ;
42+
43+ /**
44+ * Initialize the plugin
45+ */
46+ public static function init (): void {
47+ self ::$ plugin_file = __FILE__ ;
48+
49+ // Register activation and deactivation hooks
50+ register_activation_hook ( self ::$ plugin_file , self ::on_activation ( ... ) );
51+ register_deactivation_hook ( self ::$ plugin_file , self ::on_deactivation ( ... ) );
52+ }
53+
54+ /**
55+ * Plugin activation callback
56+ */
57+ public static function on_activation (): void {
58+ $ plugin_dir = plugin_dir_path ( self ::$ plugin_file );
59+ $ scripts_dir = $ plugin_dir . 'scripts/ ' ;
60+
61+ if ( ! is_dir ( $ scripts_dir ) ) {
62+ self ::log ( LogLevel::ERROR , "Scripts directory not found: {$ scripts_dir }" );
63+ return ;
64+ }
65+
66+ $ mu_plugins_dir = WPMU_PLUGIN_DIR . '/ ' ;
67+
68+ // Create mu-plugins directory if it doesn't exist
69+ // Note: We create it but never delete it, even on deactivation
70+ if ( ! is_dir ( $ mu_plugins_dir ) && ! wp_mkdir_p ( $ mu_plugins_dir ) ) {
71+ self ::log ( LogLevel::ERROR , "Failed to create mu-plugins directory: {$ mu_plugins_dir }" );
72+ return ;
73+ }
74+
75+ $ copied_files = [];
76+
77+ try {
78+ $ php_files = self ::get_php_files ( $ scripts_dir );
79+
80+ foreach ( $ php_files as $ source_file ) {
81+ // Check if file has the required namespace
82+ if ( ! self ::has_required_namespace ( $ source_file ) ) {
83+ $ filename = basename ( $ source_file );
84+ self ::log ( LogLevel::WARNING , "Skipping {$ filename } - missing required namespace " . self ::REQUIRED_NAMESPACE_PREFIX );
85+ continue ;
86+ }
87+
88+ $ filename = basename ( $ source_file );
89+ $ destination_file = $ mu_plugins_dir . $ filename ;
90+
91+ if ( copy ( $ source_file , $ destination_file ) ) {
92+ $ copied_files [] = $ filename ;
93+ self ::log ( LogLevel::INFO , "Copied {$ filename } to mu-plugins " );
94+ }
95+ else {
96+ self ::log ( LogLevel::ERROR , "Failed to copy {$ filename }" );
97+ }
98+ }
99+
100+ $ count = count ( $ copied_files );
101+ if ( $ count > 0 ) {
102+ self ::log ( LogLevel::INFO , "Successfully copied {$ count } PHP files to mu-plugins " );
103+ }
104+
105+ } catch ( Exception $ e ) {
106+ self ::log ( LogLevel::ERROR , "Error during file copying: {$ e ->getMessage ()}" );
107+ }
108+ }
109+
110+ /**
111+ * Plugin deactivation callback
112+ */
113+ public static function on_deactivation (): void {
114+ $ mu_plugins_dir = WPMU_PLUGIN_DIR . '/ ' ;
115+
116+ if ( ! is_dir ( $ mu_plugins_dir ) ) {
117+ self ::log ( LogLevel::INFO , 'No mu-plugins directory found ' );
118+ return ;
119+ }
120+
121+ $ removed_count = 0 ;
122+ $ php_files = self ::get_php_files ( $ mu_plugins_dir );
123+
124+ foreach ( $ php_files as $ file_path ) {
125+ if ( self ::has_required_namespace ( $ file_path ) ) {
126+ $ filename = basename ( $ file_path );
127+
128+ if ( unlink ( $ file_path ) ) {
129+ $ removed_count ++;
130+ self ::log ( LogLevel::INFO , "Removed {$ filename } from mu-plugins " );
131+ }
132+ else {
133+ self ::log ( LogLevel::ERROR , "Failed to remove {$ filename }" );
134+ }
135+ }
136+ }
137+
138+ self ::log ( LogLevel::INFO , "Successfully removed {$ removed_count } files from mu-plugins " );
139+ }
140+
141+ /**
142+ * Get all PHP files from a directory (root level only)
143+ */
144+ private static function get_php_files ( string $ directory ): array {
145+ if ( ! is_dir ( $ directory ) ) {
146+ return [];
147+ }
148+
149+ $ php_files = [];
150+ $ files = scandir ( $ directory );
151+
152+ foreach ( $ files as $ file ) {
153+ if ( $ file === '. ' || $ file === '.. ' ) {
154+ continue ;
155+ }
156+
157+ $ file_path = $ directory . $ file ;
158+ if ( is_file ( $ file_path ) && pathinfo ( $ file , PATHINFO_EXTENSION ) === 'php ' ) {
159+ $ php_files [] = $ file_path ;
160+ }
161+ }
162+
163+ return $ php_files ;
164+ }
165+
166+ /**
167+ * Check if a PHP file has the required namespace
168+ * Only accepts format: namespace Kntnt\Mu_Scripts\Something;
169+ */
170+ private static function has_required_namespace ( string $ file_path ): bool {
171+ // Read first 2KB of file (namespace should be early in file)
172+ $ content = file_get_contents ( $ file_path , false , null , 0 , 2048 );
173+ if ( $ content === false ) {
174+ return false ;
175+ }
176+
177+ // Remove comments to avoid false matches
178+ $ content = preg_replace ( [
179+ '/\/\*.*?\*\//s ' , // Block comments
180+ '/\/\/.*$/m ' , // Line comments
181+ '/#.*$/m ' , // Hash comments
182+ ], '' , $ content );
183+
184+ // Look for our required namespace declaration
185+ $ pattern = '/^\s*namespace\s+ ' . preg_quote ( self ::REQUIRED_NAMESPACE_PREFIX , '/ ' ) . '/m ' ;
186+ return preg_match ( $ pattern , $ content ) === 1 ;
187+ }
188+
189+ /**
190+ * Log messages with different levels
191+ */
192+ private static function log ( LogLevel $ level , string $ message ): void {
193+ $ prefix = match ( $ level ) {
194+ LogLevel::INFO => 'Kntnt Must Use Scripts ' ,
195+ LogLevel::ERROR => 'Kntnt Must Use Scripts ERROR ' ,
196+ LogLevel::WARNING => 'Kntnt Must Use Scripts WARNING ' ,
197+ };
198+
199+ error_log ( "{$ prefix }: {$ message }" );
200+ }
201+
202+ }
203+
204+ // Initialize the plugin
205+ Plugin::init ();
0 commit comments