PluginProbe
Code Engine – PHP Snippets, AI Functions & Automation for WordPress / trunk
Code Engine – PHP Snippets, AI Functions & Automation for WordPress vtrunk
0.5.6 0.5.5 0.5.4 0.5.3 0.5.2 0.5.1 0.5.0 0.4.9 0.4.8 0.4.7 0.4.6 trunk 0.0.1 0.0.2 0.2.8 0.2.9 0.3.0 0.3.1 0.3.2 0.3.3 0.3.4 0.3.5 0.3.6 0.3.7 0.3.8 All 32 releases
← All changes | common/rest.php +203 -30 0.3.4trunk View file →
@@ -1,13 +1,13 @@
1 1 <?php
2 2
3 -class MeowCommon_Rest {
3 +class MeowKit_MWCODE_Rest {
4 4 private $namespace = 'meow-common/v1';
5 5 public static $instance = null;
6 6
7 7 public static function init_once() {
8 - if ( !MeowCommon_Rest::$instance ) {
9 - MeowCommon_Rest::$instance = new self();
8 + if ( !MeowKit_MWCODE_Rest::$instance ) {
9 + MeowKit_MWCODE_Rest::$instance = new self();
10 10 }
11 11 }
12 12
13 13 private function __construct() {
@@ -13,64 +13,217 @@
13 13 private function __construct() {
14 14 add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
15 15 }
16 16
17 + /**
18 + * Capability gate for plugin admin endpoints. Defaults to manage_options but
19 + * honours a per-plugin filter so a site can grant access to other roles
20 + * without re-implementing every permission_callback. The filter name is
21 + * derived from the class name (`MeowKit_<PREFIX>_Rest` => `<prefix>_allow_setup`),
22 + * so each plugin gets its own filter automatically after Nekofy substitution.
23 + */
24 + private function can_setup() {
25 + static $filter = null;
26 + if ( $filter === null ) {
27 + $parts = explode( '_', __CLASS__ );
28 + $prefix = isset( $parts[1] ) ? strtolower( $parts[1] ) : '';
29 + $filter = $prefix !== '' ? $prefix . '_allow_setup' : '';
30 + }
31 + $default = current_user_can( 'manage_options' );
32 + return $filter !== '' ? apply_filters( $filter, $default ) : $default;
33 + }
34 +
17 35 public function rest_api_init() {
18 - if ( !current_user_can( 'manage_options' ) ) {
36 + if ( !$this->can_setup() ) {
19 37 return;
20 38 }
39 + $permission = function () {
40 + return $this->can_setup();
41 + };
21 42 register_rest_route( $this->namespace, '/empty_request/', [
22 43 'methods' => 'POST',
23 - 'permission_callback' => function () {
24 - return current_user_can( 'manage_options' );
25 - },
44 + 'permission_callback' => $permission,
26 45 'callback' => [ $this, 'empty_request' ]
27 46 ] );
28 47 register_rest_route( $this->namespace, '/file_operation/', [
29 48 'methods' => 'POST',
30 - 'permission_callback' => function () {
31 - return current_user_can( 'manage_options' );
32 - },
49 + 'permission_callback' => $permission,
33 50 'callback' => [ $this, 'file_operation' ]
34 51 ] );
35 52 register_rest_route( $this->namespace, '/sql_request/', [
36 53 'methods' => 'POST',
37 - 'permission_callback' => function () {
38 - return current_user_can( 'manage_options' );
39 - },
54 + 'permission_callback' => $permission,
40 55 'callback' => [ $this, 'sql_request' ]
41 56 ] );
42 57 register_rest_route( $this->namespace, '/error_logs/', [
43 58 'methods' => 'POST',
44 - 'permission_callback' => function () {
45 - $ok = current_user_can( 'manage_options' );
46 - return $ok;
47 - },
59 + 'permission_callback' => $permission,
48 60 'callback' => [ $this, 'rest_error_logs' ]
49 61 ] );
50 62 register_rest_route( $this->namespace, '/all_settings/', [
51 63 'methods' => 'POST',
52 - 'permission_callback' => function () {
53 - $ok = current_user_can( 'manage_options' );
54 - return $ok;
55 - },
64 + 'permission_callback' => $permission,
56 65 'callback' => [ $this, 'rest_all_settings' ]
57 66 ] );
58 67 register_rest_route( $this->namespace, '/update_option/', [
59 68 'methods' => 'POST',
60 - 'permission_callback' => function () {
61 - $ok = current_user_can( 'manage_options' );
62 - return $ok;
63 - },
69 + 'permission_callback' => $permission,
64 70 'callback' => [ $this, 'rest_update_option' ]
65 71 ] );
72 + register_rest_route( $this->namespace, '/installed_plugins/', [
73 + 'methods' => 'POST',
74 + 'permission_callback' => $permission,
75 + 'callback' => [ $this, 'rest_installed_plugins' ]
76 + ] );
77 + // The analysis needs PHP for one reason: $mwai is a PHP global, so the AI
78 + // call cannot be made from the dashboard's JavaScript. Everything it
79 + // analyses is gathered in the browser and posted here.
80 + register_rest_route( $this->namespace, '/analysis_status/', [
81 + 'methods' => 'POST',
82 + 'permission_callback' => $permission,
83 + 'callback' => [ $this, 'rest_analysis_status' ]
84 + ] );
85 + register_rest_route( $this->namespace, '/analysis_run/', [
86 + 'methods' => 'POST',
87 + 'permission_callback' => $permission,
88 + 'callback' => [ $this, 'rest_analysis_run' ]
89 + ] );
90 + register_rest_route( $this->namespace, '/analysis_forget/', [
91 + 'methods' => 'POST',
92 + 'permission_callback' => $permission,
93 + 'callback' => [ $this, 'rest_analysis_forget' ]
94 + ] );
66 95 }
67 96
97 + /**
98 + * Throw away the stored analysis and the consent that went with it.
99 + *
100 + * The analysis writes a verdict about this site into the options table and
101 + * keeps it. That is what makes it survive a reload, but it also means a
102 + * description of your server's weaknesses, written by a third-party model,
103 + * sits there indefinitely with nothing to remove it. Somebody should be able
104 + * to take it back, and clearing the consent means the panel explaining what
105 + * gets sent is shown again before anything is sent a second time.
106 + */
107 + public function rest_analysis_forget() {
108 + delete_option( 'meowapps_analysis_last' );
109 + delete_option( 'meowapps_analysis_consented' );
110 + return new WP_REST_Response( [ 'success' => true ], 200 );
111 + }
112 +
113 + /**
114 + * Whether an analysis can be run, and the last one if there is one.
115 + *
116 + * Three states matter and they are not the same: AI Engine absent, AI Engine
117 + * present but with no API key configured, and ready. Telling the second from
118 + * the first is what lets the dashboard say "finish setting it up" rather than
119 + * "install this", which would be wrong and mildly insulting.
120 + */
121 + public function rest_analysis_status() {
122 + global $mwai;
123 + $installed = !empty( $mwai );
124 + $ready = $installed && method_exists( $mwai, 'hasAI' ) && $mwai->hasAI();
125 + return new WP_REST_Response( [ 'success' => true, 'data' => [
126 + 'installed' => $installed,
127 + 'ready' => $ready,
128 + 'consented' => (bool) get_option( 'meowapps_analysis_consented', false ),
129 + 'last' => get_option( 'meowapps_analysis_last', null ),
130 + ] ], 200 );
131 + }
132 +
133 + public function rest_analysis_run( $request ) {
134 + global $mwai;
135 + if ( empty( $mwai ) || !method_exists( $mwai, 'simpleJsonQuery' ) ) {
136 + return new WP_REST_Response( [ 'success' => false,
137 + 'message' => 'AI Engine is not available on this site.' ], 200 );
138 + }
139 + if ( !method_exists( $mwai, 'hasAI' ) || !$mwai->hasAI() ) {
140 + return new WP_REST_Response( [ 'success' => false,
141 + 'message' => 'AI Engine has no AI environment configured yet.' ], 200 );
142 + }
143 +
144 + $params = $request->get_json_params();
145 + $facts = isset( $params['facts'] ) ? $params['facts'] : [];
146 +
147 + // Consent is recorded when a run is actually asked for, so the panel is
148 + // shown once rather than on every visit.
149 + update_option( 'meowapps_analysis_consented', '1' );
150 +
151 + try {
152 + $reply = $mwai->simpleJsonQuery( $this->analysis_prompt( $facts ) );
153 + }
154 + catch ( Exception $e ) {
155 + return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 200 );
156 + }
157 +
158 + $verdict = is_string( $reply ) ? json_decode( $reply, true ) : $reply;
159 + if ( empty( $verdict ) || !is_array( $verdict ) ) {
160 + return new WP_REST_Response( [ 'success' => false,
161 + 'message' => 'The AI reply could not be read as JSON.' ], 200 );
162 + }
163 +
164 + $verdict['ranAt'] = current_time( 'mysql' );
165 + update_option( 'meowapps_analysis_last', $verdict );
166 +
167 + return new WP_REST_Response( [ 'success' => true, 'data' => $verdict ], 200 );
168 + }
169 +
170 + /**
171 + * Written for the person who owns the site, not for a developer: somebody who
172 + * installed a plugin, not somebody who knows what max_execution_time is.
173 + *
174 + * Meow Apps plugins may be named as a remedy, but only where one genuinely
175 + * fixes the finding. An analysis that recommends its own author's plugins for
176 + * everything is an advert, and would be read as one.
177 + */
178 + private function analysis_prompt( $facts ) {
179 + $json = wp_json_encode( $facts, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
180 + return "You are reviewing a WordPress site's health for the person who owns it.\n"
181 + . "They are not a developer: explain what something means and why it matters before saying "
182 + . "what to do, and never assume they know what a PHP directive is.\n\n"
183 + . "Here is what was measured on their site:\n\n$json\n\n"
184 + . "Reply with JSON in exactly this shape:\n"
185 + . "{\n"
186 + . ' "areas": [ { "area": "Speed|Environment|Errors", "rating": 1-5, '
187 + . '"label": "a two or three word verdict", "summary": "one plain sentence" } ],' . "\n"
188 + . ' "findings": [ { "area": "Speed|Environment|Errors", "severity": "high|medium|low", '
189 + . '"title": "the recommendation itself, plain, under 60 characters", '
190 + . '"detail": "at most two short sentences on what it means and why it matters", '
191 + . '"action": "one sentence saying what to do", '
192 + . '"plugin": "meow plugin slug or null" } ],' . "\n"
193 + . ' "conclusion": "two short sentences tying it together"' . "\n"
194 + . "}\n\n"
195 + . "Rules:\n"
196 + . "- Rate each of the three areas from 1 (bad) to 5 (good). Do not invent an overall score.\n"
197 + . "- Only report findings genuinely worth acting on. An empty findings list is a perfectly "
198 + . "good answer for a healthy site. Do not manufacture problems.\n"
199 + . "- Order findings by how much they matter, worst first.\n"
200 + . "- Be brief. The reader sees the titles first and opens the ones they care about, so a "
201 + . "title has to work on its own and the detail must not repeat it. No preamble, no restating "
202 + . "the question, no filler.\n"
203 + . "- Set \"plugin\" to one of these Meow Apps slugs ONLY when that plugin genuinely fixes the "
204 + . "finding, otherwise null: ai-engine, code-engine, contact-form-block, database-cleaner, "
205 + . "media-cleaner, media-file-renamer, meow-gallery, meow-lightbox, meow-mailer, seo-engine, "
206 + . "social-engine, wp-retina-2x, wplr-sync.\n"
207 + . "- If an error in the log comes from a specific plugin, say which one.\n"
208 + . "- Write in the language of this site: " . get_bloginfo( 'language' ) . ".";
209 + }
210 +
68 211 public function file_rand( $filesize ) {
69 - $tmp_file = tmpfile();
70 - fseek( $tmp_file, $filesize - 1, SEEK_CUR );
71 - fwrite( $tmp_file, 'a' );
72 - fclose( $tmp_file );
212 + // Write the benchmark file inside a dedicated subfolder of the uploads
213 + // directory (created on demand), then remove it. wp.org forbids writing to
214 + // the plugin folder or the uploads root, only a sanctioned subfolder.
215 + $upload = wp_upload_dir();
216 + if ( !empty( $upload['error'] ) || empty( $upload['basedir'] ) ) { return; }
217 + $dir = trailingslashit( $upload['basedir'] ) . 'meowapps';
218 + if ( !wp_mkdir_p( $dir ) ) { return; }
219 + $path = trailingslashit( $dir ) . 'speedtest-' . wp_generate_password( 12, false ) . '.tmp';
220 + $fh = @fopen( $path, 'wb' );
221 + if ( $fh === false ) { return; }
222 + fseek( $fh, $filesize - 1, SEEK_CUR );
223 + fwrite( $fh, 'a' );
224 + fclose( $fh );
225 + @unlink( $path );
73 226 }
74 227
75 228 public function empty_request() {
76 229 return new WP_REST_Response( [ 'success' => true ], 200 );
@@ -107,8 +260,28 @@
107 260 public function rest_all_settings() {
108 261 return new WP_REST_Response( [ 'success' => true, 'data' => $this->get_all_options() ], 200 );
109 262 }
110 263
264 + public function rest_installed_plugins() {
265 + if ( !function_exists( 'get_plugins' ) ) {
266 + require_once ABSPATH . 'wp-admin/includes/plugin.php';
267 + }
268 + $all_plugins = get_plugins();
269 + $result = [];
270 + foreach ( $all_plugins as $plugin_file => $plugin_data ) {
271 + // Plugin file looks like "ai-engine/ai-engine.php", the slug is the
272 + // first path segment. Some plugins live at the root (single file),
273 + // those we just skip; we only care about Meow Apps directories anyway.
274 + $parts = explode( '/', $plugin_file );
275 + if ( count( $parts ) < 2 ) {
276 + continue;
277 + }
278 + $slug = $parts[0];
279 + $result[ $slug ] = is_plugin_active( $plugin_file ) ? 'active' : 'inactive';
280 + }
281 + return new WP_REST_Response( [ 'success' => true, 'data' => $result ], 200 );
282 + }
283 +
111 284 public function rest_update_option( $request ) {
112 285 $params = $request->get_json_params();
113 286 try {
114 287 $name = $params['name'];
@@ -128,8 +301,8 @@
128 301 }
129 302 }
130 303
131 304 public function rest_error_logs( $request ) {
132 - return new WP_REST_Response( [ 'success' => true, 'data' => MeowCommon_Helpers::php_error_logs() ], 200 );
305 + return new WP_REST_Response( [ 'success' => true, 'data' => MeowKit_MWCODE_Helpers::php_error_logs() ], 200 );
133 306 }
134 307
135 308 }