PluginProbe
wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin / trunk
wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin vtrunk
6.5.1.7 6.5.1.6 6.5.1.5 6.5.1.4 6.5.1.3 6.5.1.2 6.5.1.1 6.5.0.9 6.5.0.8 6.5.0.7 6.5.0.6 trunk 3.4.2.40 3.4.2.41 3.4.2.42 3.4.2.43 3.4.2.44 3.4.2.45 3.4.2.46 3.4.2.47 3.4.2.48 3.4.2.49 3.4.2.50 6.3.2 6.3.3.1 All 47 releases
wpdatatables / Infrastructure / WP / MCP / Abilities / upload-media-from-url.php

upload-media-from-url.php in wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin trunk, at Infrastructure/WP/MCP/Abilities/upload-media-from-url.php

257 lines 9.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Ability: wpdatatables/upload-media-from-url
4 *
5 * Downloads an image from a URL and adds it to the WordPress media library.
6 * Returns attachment_id and source_url for use in table cells.
7 *
8 * @package wpDataTables_MCP_Server
9 * @since 0.2.0
10 */
11
12 defined( 'ABSPATH' ) or die('Access denied.');
13
14 add_action( 'wp_abilities_api_init', 'wdtmcp_register_upload_media_from_url_ability' );
15
16 function wdtmcp_register_upload_media_from_url_ability() {
17 wp_register_ability(
18 'wpdatatables/upload-media-from-url',
19 array(
20 'label' => __( 'Upload Media from URL', 'wpdatatables' ),
21 'description' => __( 'Downloads an image from a URL and adds it to the WordPress media library. Returns attachment_id and source_url for use in table cells via attachment_id or direct img src. Supports jpg, png, gif, webp. Use for product images, logos, etc. WHEN TO CALL: when the user provides a remote image URL and you need it in a Simple table cell, or when list-media finds no suitable existing image. REQUIRED INPUT: url (string, public http/https image URL). OPTIONAL: alt_text, title. RETURNS: attachment_id, source_url, title, alt_text. NEXT STEP: use attachment_id in create-simple-table or update-simple-table-styles as {"attachment_id":ID,"size":"medium","alt":"..."}. FOR LOCAL FILES on disk: do not use this tool — upload via WordPress REST API /wp/v2/media or ask the user to upload via Media → Add New, then use list-media.', 'wpdatatables' ),
22 'category' => 'wpdatatables-data',
23
24 'input_schema' => array(
25 'type' => 'object',
26 'properties' => array(
27 'url' => array(
28 'type' => 'string',
29 'description' => 'Public URL of the image to download (http or https).',
30 ),
31 'alt_text' => array(
32 'type' => 'string',
33 'description' => 'Alt text for the image (accessibility).',
34 ),
35 'title' => array(
36 'type' => 'string',
37 'description' => 'Title for the media attachment.',
38 ),
39 ),
40 'required' => array( 'url' ),
41 ),
42
43 'output_schema' => array(
44 'type' => 'object',
45 'properties' => array(
46 'attachment_id' => array( 'type' => 'integer', 'description' => 'WordPress attachment ID.' ),
47 'source_url' => array( 'type' => 'string', 'description' => 'URL to the uploaded file.' ),
48 'title' => array( 'type' => 'string', 'description' => 'Attachment title.' ),
49 'alt_text' => array( 'type' => 'string', 'description' => 'Alt text.' ),
50 ),
51 ),
52
53 'execute_callback' => 'wdtmcp_execute_upload_media_from_url',
54
55 'permission_callback' => function () {
56 return current_user_can( 'manage_options' );
57 },
58
59 'meta' => array(
60 'annotations' => array(
61 'instructions' => __( 'Requires a public http/https image URL. Each call creates a new Media Library attachment; use attachment_id in Simple table cells afterward.', 'wpdatatables' ),
62 'readonly' => false,
63 'destructive' => false,
64 'idempotent' => false,
65 ),
66 ),
67 )
68 );
69 }
70
71 /**
72 * Execute callback for wpdatatables/upload-media-from-url.
73 *
74 * @param array $input
75 * @return array|WP_Error
76 */
77 function wdtmcp_execute_upload_media_from_url( $input ) {
78 $url = isset( $input['url'] ) ? esc_url_raw( trim( (string) $input['url'] ) ) : '';
79 $alt = isset( $input['alt_text'] ) ? sanitize_text_field( $input['alt_text'] ) : '';
80 $title = isset( $input['title'] ) ? sanitize_text_field( $input['title'] ) : '';
81
82 if ( '' === $url ) {
83 return new \WP_Error( 'wdtmcp_missing_param', __( 'url is required.', 'wpdatatables' ) );
84 }
85
86 $parsed = wp_parse_url( $url );
87 if ( ! $parsed || empty( $parsed['scheme'] ) || ! in_array( strtolower( $parsed['scheme'] ), array( 'http', 'https' ), true ) ) {
88 return new \WP_Error( 'wdtmcp_invalid_url', __( 'Invalid or unsupported URL scheme.', 'wpdatatables' ) );
89 }
90
91 $url_check = wdtmcp_validate_public_remote_source_url( $url );
92 if ( is_wp_error( $url_check ) ) {
93 return $url_check;
94 }
95
96 require_once ABSPATH . 'wp-admin/includes/media.php';
97 require_once ABSPATH . 'wp-admin/includes/file.php';
98 require_once ABSPATH . 'wp-admin/includes/image.php';
99
100 $tmp = download_url( $url, 30 );
101 if ( is_wp_error( $tmp ) ) {
102 return $tmp;
103 }
104
105 $url_path = isset( $parsed['path'] ) ? $parsed['path'] : '';
106 $filename = basename( $url_path );
107 // If the URL has no path (unlikely for image URLs), fall back to the full URL.
108 if ( '' === $filename ) {
109 $filename = basename( $url );
110 }
111
112 $file_array = array(
113 'name' => $filename,
114 'tmp_name' => $tmp,
115 );
116
117 $allowed = array( 'jpg', 'jpeg', 'jpe', 'png', 'gif', 'webp' );
118 $ext = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) );
119 if ( ! in_array( $ext, $allowed, true ) ) {
120 @unlink( $tmp );
121 return new \WP_Error( 'wdtmcp_invalid_type', __( 'Only jpg, png, gif, webp images are allowed.', 'wpdatatables' ) );
122 }
123
124 $id = media_handle_sideload( $file_array, 0, $title );
125 if ( is_wp_error( $id ) ) {
126 @unlink( $tmp );
127 return $id;
128 }
129
130 if ( '' !== $alt ) {
131 update_post_meta( $id, '_wp_attachment_image_alt', $alt );
132 }
133 if ( '' !== $title ) {
134 wp_update_post( array(
135 'ID' => $id,
136 'post_title' => $title,
137 ) );
138 }
139
140 $source_url = wp_get_attachment_url( $id );
141 if ( ! $source_url ) {
142 wp_delete_attachment( $id, true );
143 return new \WP_Error( 'wdtmcp_upload_failed', __( 'Failed to get attachment URL after upload.', 'wpdatatables' ) );
144 }
145
146 return array(
147 'attachment_id' => (int) $id,
148 'source_url' => $source_url,
149 'title' => get_the_title( $id ),
150 'alt_text' => (string) get_post_meta( $id, '_wp_attachment_image_alt', true ),
151 );
152 }
153
154 if ( ! function_exists( 'wdtmcp_validate_public_remote_source_url' ) ) {
155 /**
156 * SSRF guard for remote URLs: only allow public http(s) URLs that resolve to
157 * public (non-private, non-reserved) IP addresses.
158 *
159 * @param mixed $url Candidate URL.
160 * @return true|\WP_Error
161 */
162 function wdtmcp_validate_public_remote_source_url( $url ) {
163 $blocked = new \WP_Error(
164 'wdtmcp_url_blocked',
165 __(
166 'This URL cannot be used. Only public http(s) URLs are allowed — local addresses, private networks, and invalid URLs are blocked. For local files, upload via Media Library or MCP and pass attachment_id.',
167 'wpdatatables'
168 )
169 );
170
171 if ( ! is_string( $url ) || '' === trim( $url ) ) {
172 return $blocked;
173 }
174
175 if ( ! function_exists( 'wp_http_validate_url' ) ) {
176 return $blocked;
177 }
178
179 $validated = wp_http_validate_url( trim( $url ) );
180 if ( false === $validated ) {
181 return $blocked;
182 }
183
184 $parsed = wp_parse_url( $validated );
185 if ( empty( $parsed['host'] ) ) {
186 return $blocked;
187 }
188
189 $host = $parsed['host'];
190 if ( '[' === substr( $host, 0, 1 ) && ']' === substr( $host, -1 ) ) {
191 $host = substr( $host, 1, -1 );
192 }
193
194 $ip_flags = FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE;
195 $ips = array();
196
197 if ( filter_var( $host, FILTER_VALIDATE_IP ) ) {
198 $ips[] = $host;
199 } elseif ( function_exists( 'dns_get_record' ) ) {
200 $a = @dns_get_record( $host, DNS_A );
201 if ( is_array( $a ) ) {
202 foreach ( $a as $row ) {
203 if ( ! empty( $row['ip'] ) ) {
204 $ips[] = $row['ip'];
205 }
206 }
207 }
208 $aaaa = @dns_get_record( $host, DNS_AAAA );
209 if ( is_array( $aaaa ) ) {
210 foreach ( $aaaa as $row ) {
211 if ( ! empty( $row['ipv6'] ) ) {
212 $ips[] = $row['ipv6'];
213 }
214 }
215 }
216 }
217
218 if ( empty( $ips ) && function_exists( 'gethostbynamel' ) ) {
219 $list = @gethostbynamel( $host );
220 if ( is_array( $list ) ) {
221 foreach ( $list as $ipv4 ) {
222 if ( is_string( $ipv4 ) && '' !== $ipv4 ) {
223 $ips[] = $ipv4;
224 }
225 }
226 }
227 }
228
229 if ( empty( $ips ) ) {
230 $gh = @gethostbyname( $host );
231 if ( is_string( $gh ) && $gh !== $host && filter_var( $gh, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
232 $ips[] = $gh;
233 }
234 }
235
236 $ips = array_unique( $ips );
237
238 if ( empty( $ips ) ) {
239 return new \WP_Error(
240 'wdtmcp_url_blocked',
241 __(
242 'Could not resolve the URL host to a public address. Check the hostname or use attachment_id for local files.',
243 'wpdatatables'
244 )
245 );
246 }
247
248 foreach ( $ips as $ip ) {
249 if ( false === filter_var( $ip, FILTER_VALIDATE_IP, $ip_flags ) ) {
250 return $blocked;
251 }
252 }
253
254 return true;
255 }
256 }
257