PluginProbe ʕ •ᴥ•ʔ
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback / 5.1
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback v5.1
5.1.3 5.1.2 5.1.1 5.1 5.0 trunk 3.10 3.11 3.12 3.13 3.14 3.15 3.16 3.17 3.18 3.19 3.2.0 3.2.1 3.22 3.22.1 3.22.2 3.22.3 3.22.4 3.22.5 3.22.6 3.3.0 3.3.1 3.3.2 3.3.2.1 3.3.2.2 3.3.3 3.30 3.31 3.32 3.4 3.4.1 3.4.3 3.4.4 3.5 3.5.1 3.6 3.6.1 3.7 3.8 3.9 3.9.1 3.9.2 3.9.3 3.9.4 3.9.6 3.9.6.1 4.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.2 4.1.3 4.2 4.2.1 4.2.2 4.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.4
atarim-visual-collaboration / doit / abilities / class-avcf-abilities-base.php
atarim-visual-collaboration / doit / abilities Last commit date
class-avcf-abilities-base.php 3 weeks ago class-avcf-abilities-block-navigation.php 3 weeks ago class-avcf-abilities-cache.php 3 weeks ago class-avcf-abilities-content.php 3 weeks ago class-avcf-abilities-core.php 3 weeks ago class-avcf-abilities-global-styles.php 3 weeks ago class-avcf-abilities-gutenberg.php 3 weeks ago class-avcf-abilities-media.php 3 weeks ago class-avcf-abilities-metadata.php 3 weeks ago class-avcf-abilities-navigation.php 3 weeks ago class-avcf-abilities-patterns.php 3 weeks ago class-avcf-abilities-plugins.php 3 weeks ago class-avcf-abilities-settings.php 3 weeks ago class-avcf-abilities-taxonomies.php 3 weeks ago class-avcf-abilities-templates.php 3 weeks ago class-avcf-abilities-theme-files.php 3 weeks ago class-avcf-abilities-themes.php 3 weeks ago class-avcf-abilities-users.php 3 weeks ago
class-avcf-abilities-base.php
208 lines
1 <?php
2 /**
3 * Base class for Atarim MCP ability category classes.
4 *
5 * Every ability-category class (content, taxonomies, users, plugins, etc.)
6 * extends this class. The orchestrator (AVCF_MCP) instantiates each subclass
7 * and calls register() to register all abilities in that category with the
8 * WordPress Abilities API.
9 *
10 * Shared helpers used by multiple ability categories (e.g. post body
11 * preparation, date normalization, capability check patterns) live here so
12 * they have a single home.
13 *
14 * @package atarim-visual-collaboration
15 */
16
17 if ( ! defined('ABSPATH') ) {
18 exit;
19 }
20
21 abstract class AVCF_Abilities_Base {
22
23 /**
24 * Register all abilities in this category.
25 *
26 * Called by AVCF_MCP::avcf_mcp_register_abilities() on the
27 * wp_abilities_api_init hook. Each subclass implements this to register
28 * its abilities via wp_register_ability().
29 */
30 abstract public function register();
31
32 /**
33 * Prepare a post body string for storage in post_content.
34 *
35 * Handles three formats:
36 * - "raw" — store the string exactly as provided. No processing.
37 * - "blocks" — caller asserts the string is already valid block markup.
38 * Pass through unchanged.
39 * - "auto" — (default) detect block delimiters. If a "<!-- wp:" marker
40 * is present anywhere in the string, treat as blocks and
41 * pass through. Otherwise, treat as plain text / HTML and
42 * wrap each blank-line-separated chunk as a wp:paragraph
43 * block so the result stays editable in the block editor.
44 * Single newlines inside a chunk are preserved as <br>
45 * soft line breaks.
46 *
47 * @param string $content
48 * @param string $format One of "auto", "raw", "blocks".
49 * @return string
50 */
51 protected function avcf_prepare_content_body( $content, $format = 'auto' ) {
52 if ( ! is_string( $content ) || $content === '' ) {
53 return '';
54 }
55
56 $format = in_array( $format, [ 'auto', 'raw', 'blocks' ], true ) ? $format : 'auto';
57
58 if ( $format === 'raw' || $format === 'blocks' ) {
59 return $content;
60 }
61
62 // auto: if block delimiters are already present, trust the caller.
63 if ( strpos( $content, '<!-- wp:' ) !== false ) {
64 return $content;
65 }
66
67 // No block markup — split on blank lines and wrap each chunk as a
68 // wp:paragraph block. Treats Markdown-style double newlines as paragraph
69 // breaks; single newlines inside a chunk become <br> soft line breaks.
70 $normalized = str_replace( [ "\r\n", "\r" ], "\n", $content );
71 $chunks = preg_split( '/\n{2,}/', trim( $normalized ) );
72
73 if ( empty( $chunks ) ) {
74 return '';
75 }
76
77 $blocks = [];
78 foreach ( $chunks as $chunk ) {
79 $chunk = trim( $chunk );
80 if ( $chunk === '' ) {
81 continue;
82 }
83 $html = str_replace( "\n", "<br>", $chunk );
84 $blocks[] = "<!-- wp:paragraph -->\n<p>{$html}</p>\n<!-- /wp:paragraph -->";
85 }
86
87 return implode( "\n\n", $blocks );
88 }
89
90 /**
91 * Validate and normalise a date string for post_date / post_date_gmt.
92 *
93 * Accepts ISO 8601 (2026-05-22T14:30:00) or any strtotime()-parseable
94 * string. The parsed timestamp is treated as site-local time and GMT is
95 * derived from it via get_gmt_from_date().
96 *
97 * Returns an array of three elements:
98 * [0] string|null local datetime in MySQL format (Y-m-d H:i:s), or null on failure
99 * [1] string|null GMT datetime in MySQL format, or null on failure
100 * [2] string|null error message, or null on success
101 *
102 * @param string $date
103 * @return array{0:?string,1:?string,2:?string}
104 */
105 protected function avcf_normalize_post_date( $date ) {
106 if ( ! is_string( $date ) || trim( $date ) === '' ) {
107 return [ null, null, 'date must be a non-empty string.' ];
108 }
109
110 $ts = strtotime( $date );
111 if ( $ts === false ) {
112 return [
113 null,
114 null,
115 sprintf( 'Could not parse date "%s". Use ISO 8601 (e.g. 2026-05-22T14:30:00).', $date ),
116 ];
117 }
118
119 // strtotime() in WordPress parses in UTC (WP sets default_timezone to UTC).
120 // Treat the parsed timestamp as a site-local datetime (matches what a user
121 // means when they type "2026-05-22 14:30") and derive GMT from it.
122 $local = date( 'Y-m-d H:i:s', $ts );
123 $gmt = get_gmt_from_date( $local );
124
125 return [ $local, $gmt, null ];
126 }
127
128 /**
129 * Fetch a remote file's body for use as post content.
130 *
131 * Retrieves the response body verbatim — no sanitisation and no charset
132 * conversion — so the caller receives exactly what the URL served: HTML,
133 * PHP source, plain text, Gutenberg block markup, anything. The returned
134 * string is meant to be handed to avcf_prepare_content_body() with the
135 * caller's chosen content_format (use "raw" to store byte-for-byte).
136 *
137 * Uses wp_safe_remote_get(), which rejects requests to private/loopback
138 * hosts (SSRF guard) on top of wp_http_validate_url().
139 *
140 * Returns an array of two elements:
141 * [0] string|null the fetched body, or null on failure
142 * [1] string|null error message, or null on success
143 *
144 * @param string $url
145 * @return array{0:?string,1:?string}
146 */
147 protected function avcf_fetch_content_from_url( $url ) {
148 $url = is_string( $url ) ? trim( $url ) : '';
149 if ( $url === '' ) {
150 return [ null, 'content_url must be a non-empty string.' ];
151 }
152
153 if ( ! wp_http_validate_url( $url ) ) {
154 return [ null, sprintf( 'content_url "%s" is not a valid or allowed URL.', $url ) ];
155 }
156
157 $response = wp_safe_remote_get(
158 $url,
159 [
160 'timeout' => 15,
161 'redirection' => 3,
162 ]
163 );
164
165 if ( is_wp_error( $response ) ) {
166 return [ null, sprintf( 'Failed to fetch content_url: %s', $response->get_error_message() ) ];
167 }
168
169 $code = (int) wp_remote_retrieve_response_code( $response );
170 if ( $code < 200 || $code >= 300 ) {
171 return [ null, sprintf( 'content_url returned HTTP %d.', $code ) ];
172 }
173
174 return [ (string) wp_remote_retrieve_body( $response ), null ];
175 }
176
177 /**
178 * Validate that an attachment ID exists and represents an image.
179 *
180 * Used when setting a featured image via _thumbnail_id meta. WordPress
181 * does not validate the ID on its own — it will happily store a meta
182 * value pointing to a non-existent or non-image attachment, and the
183 * featured image simply won't render. For an AI action layer we want
184 * explicit failure so the caller can correct the input rather than
185 * silently producing a post with no featured image.
186 *
187 * @param int $attachment_id
188 * @return string|null Error message on failure, or null on success.
189 */
190 protected function avcf_validate_attachment_id( $attachment_id ) {
191 $attachment_id = (int) $attachment_id;
192 if ( $attachment_id <= 0 ) {
193 return 'featured_media must be a positive integer attachment ID.';
194 }
195
196 $attachment = get_post( $attachment_id );
197 if ( ! $attachment || $attachment->post_type !== 'attachment' ) {
198 return sprintf( 'Attachment %d does not exist.', $attachment_id );
199 }
200
201 if ( ! wp_attachment_is_image( $attachment_id ) ) {
202 return sprintf( 'Attachment %d is not an image (mime type: %s).', $attachment_id, get_post_mime_type( $attachment_id ) );
203 }
204
205 return null;
206 }
207 }
208