PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.14
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.14
2.0.14 2.0.13 2.0.12 2.0.11 2.0.10 2.0.9 trunk 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8
blockenberg / assets / php / ai-external-media.php

ai-external-media.php in Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor 2.0.14, at assets/php/ai-external-media.php

133 lines 9.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /** Progress and raster uploads from a paired external agent. */
3 if ( ! defined( 'ABSPATH' ) ) { exit; }
4
5 /** Start at actual agent work, not while waiting for pairing. Finish only once. */
6 function bkbg_external_run_clock( &$s, $finish = false ) {
7 if ( $finish ) {
8 if ( ! empty( $s['run'] ) && empty( $s['run']['finished_at'] ) ) { $s['run']['finished_at'] = microtime( true ); }
9 } elseif ( empty( $s['run'] ) || ! empty( $s['run']['finished_at'] ) ) {
10 $s['run'] = array( 'id' => wp_generate_uuid4(), 'started_at' => microtime( true ), 'finished_at' => null );
11 }
12 }
13 function bkbg_external_progress( $request ) {
14 $phase = $request['phase']; $message = $request['message']; $plan = $request['plan'];
15 if ( strlen( $request->get_body() ) > 16000 || ! in_array( $phase, array( 'planning', 'building', 'generating_image', 'reviewing', 'waiting', 'done', 'error' ), true ) || ! is_string( $message ) || mb_strlen( $message ) > 500 ) {
16 return bkbg_external_error( 'Supply a supported phase and a message of up to 500 characters.' );
17 }
18 $clean = array();
19 if ( null !== $plan ) {
20 if ( ! is_array( $plan ) || count( $plan ) > 12 ) { return bkbg_external_error( 'A plan can have up to 12 steps.' ); }
21 foreach ( $plan as $step ) {
22 if ( ! is_array( $step ) || ! is_string( $step['id'] ?? null ) || ! preg_match( '/^[a-zA-Z0-9_-]{1,60}$/D', $step['id'] ) || isset( $clean[ $step['id'] ] ) || ! is_string( $step['label'] ?? null ) || mb_strlen( $step['label'] ) > 160 || ! in_array( $step['status'] ?? '', array( 'pending', 'active', 'done' ), true ) ) {
23 return bkbg_external_error( 'Plan steps need unique ids, short labels and pending, active or done status.' );
24 }
25 $clean[ $step['id'] ] = array( 'id' => $step['id'], 'label' => sanitize_text_field( $step['label'] ), 'status' => $step['status'] );
26 }
27 }
28 return bkbg_external_change( $request['session'], function ( &$s ) use ( $phase, $message, $plan, $clean ) {
29 if ( 'done' === $phase ) {
30 foreach ( $s['messages'] ?? array() as $message ) {
31 if ( 'panel' === $message['source'] && 'queued' === $message['status'] ) { return bkbg_external_error( 'New user messages are waiting. Read /inbox and acknowledge them before finishing.', 409 ); }
32 }
33 $current_capture = empty( $s['captures'] );
34 foreach ( $s['captures'] ?? array() as $capture ) {
35 if ( $capture['revision'] === ( $s['visual_revision'] ?? 0 ) ) { $current_capture = true; }
36 if ( 'passed' !== $capture['reviewed'] ) { return bkbg_external_error( 'Capture the finished section again and review its current screenshots before marking done.', 409 ); }
37 }
38 }
39 if ( 'done' === $phase && !$current_capture ) { return bkbg_external_error( 'The page changed after its screenshots. Capture and review the updated section.', 409 ); }
40 bkbg_external_run_clock( $s, in_array( $phase, array( 'done', 'error' ), true ) );
41 $s['agent_seen'] = time();
42 $s['progress'] = array( 'phase' => $phase, 'message' => sanitize_text_field( $message ), 'updated' => time(), 'plan' => null === $plan ? ( $s['progress']['plan'] ?? array() ) : array_values( $clean ) );
43 return array( 'ok' => true );
44 } );
45 }
46
47 function bkbg_external_image_result( $request ) {
48 if ( ! is_string( $request['upload'] ) || ! preg_match( '/^[a-zA-Z0-9_-]{8,80}$/D', $request['upload'] ) ) { return bkbg_external_error( 'Invalid upload id.' ); }
49 $s = bkbg_external_read( $request['session'] );
50 if ( ! $s ) { return bkbg_external_error( 'Session expired.', 410 ); }
51 $item = $s['images'][ $request['upload'] ] ?? null;
52 if ( ! $item ) { return bkbg_external_error( 'Unknown upload.', 404 ); }
53 return array_intersect_key( $item, array_flip( array( 'status', 'image', 'error' ) ) );
54 }
55
56 function bkbg_external_upload_image( $request ) {
57 if ( ! is_string( $request['upload'] ) || ! preg_match( '/^[a-zA-Z0-9_-]{8,80}$/D', $request['upload'] ) ) { return bkbg_external_error( 'Invalid upload id.' ); }
58 $s = bkbg_external_read( $request['session'] );
59 if ( ! $s ) { return bkbg_external_error( 'Session expired.', 410 ); }
60 if ( ! user_can( $s['user'], 'upload_files' ) ) { return bkbg_external_error( 'This user cannot upload images.', 403 ); }
61 $bytes = $request->get_body();
62 if ( ! strlen( $bytes ) || strlen( $bytes ) > min( 20 * MB_IN_BYTES, wp_max_upload_size() ) ) { return bkbg_external_error( 'Image exceeds the upload limit or is empty.', 413 ); }
63 $info = @getimagesizefromstring( $bytes );
64 $extensions = array( 'image/png' => 'png', 'image/jpeg' => 'jpg', 'image/webp' => 'webp' );
65 if ( ! $info || ! isset( $extensions[ $info['mime'] ] ) || $request->get_header( 'content-type' ) !== $info['mime'] || $info[0] * $info[1] > 40000000 ) {
66 return bkbg_external_error( 'Upload a valid PNG, JPEG or WebP image up to 40 megapixels, with its matching Content-Type.' );
67 }
68 $meta = array();
69 foreach ( array( 'filename' => 200, 'title' => 200, 'alt' => 1000 ) as $key => $limit ) {
70 $value = $request[ $key ] ?? '';
71 if ( ! is_string( $value ) || mb_strlen( $value ) > $limit ) { return bkbg_external_error( 'Invalid image metadata.' ); }
72 $meta[ $key ] = sanitize_text_field( $value );
73 }
74 $id = $request['upload'];
75 $fingerprint = hash( 'sha256', $bytes . wp_json_encode( $meta ) );
76 // Reserve before writing any file. A retry cannot create a second attachment.
77 $reservation = bkbg_external_change( $request['session'], function ( &$current ) use ( $id, $fingerprint ) {
78 $images = $current['images'] ?? array();
79 if ( isset( $images[ $id ] ) ) {
80 return $images[ $id ]['fingerprint'] === $fingerprint ? array( 'existing' => true ) : bkbg_external_error( 'This upload id belongs to another image.', 409 );
81 }
82 if ( count( $images ) >= 40 ) { return bkbg_external_error( 'Session image capacity reached.', 409 ); }
83 foreach ( $images as $image ) {
84 if ( 'uploading' === $image['status'] ) { return bkbg_external_error( 'Wait for the previous image upload.', 409 ); }
85 }
86 if ( time() - $current['heartbeat'] > 15 ) { return bkbg_external_error( 'The editor is offline. Open the AI sidebar.', 409 ); }
87 bkbg_external_run_clock( $current );
88 $current['agent_seen'] = time();
89 $current['images'][ $id ] = array( 'fingerprint' => $fingerprint, 'status' => 'uploading' );
90 return array( 'existing' => false );
91 } );
92 if ( is_wp_error( $reservation ) ) { return $reservation; }
93 if ( $reservation['existing'] ) { return bkbg_external_image_result( $request ); }
94
95 require_once ABSPATH . 'wp-admin/includes/file.php';
96 require_once ABSPATH . 'wp-admin/includes/media.php';
97 require_once ABSPATH . 'wp-admin/includes/image.php';
98 $filename = sanitize_file_name( pathinfo( $meta['filename'] ?: 'generated-image', PATHINFO_FILENAME ) ) . '.' . $extensions[ $info['mime'] ];
99 $temp = wp_tempnam( $filename );
100 $attachment = 0;
101 $previous_user = get_current_user_id();
102 try {
103 if ( ! $temp || file_put_contents( $temp, $bytes ) !== strlen( $bytes ) ) { throw new RuntimeException( 'Could not write the image upload.' ); }
104 wp_set_current_user( $s['user'] );
105 $attachment = media_handle_sideload( array( 'name' => $filename, 'tmp_name' => $temp, 'error' => 0, 'size' => strlen( $bytes ) ), $s['post'], $meta['title'] ?: 'Generated image' );
106 if ( is_wp_error( $attachment ) ) { throw new RuntimeException( $attachment->get_error_message() ); }
107 update_post_meta( $attachment, '_wp_attachment_image_alt', $meta['alt'] );
108 update_post_meta( $attachment, '_bkbg_ai_generated', true );
109 update_post_meta( $attachment, '_bkbg_ai_external_agent', $s['agent'] );
110 $size = wp_get_attachment_metadata( $attachment );
111 $image = array( 'id' => $attachment, 'url' => wp_get_attachment_url( $attachment ), 'alt' => $meta['alt'], 'width' => $size['width'] ?? $info[0], 'height' => $size['height'] ?? $info[1], 'mime' => get_post_mime_type( $attachment ) );
112 $saved = bkbg_external_change( $request['session'], function ( &$current ) use ( $id, $image ) {
113 $current['images'][ $id ]['status'] = 'done';
114 $current['images'][ $id ]['image'] = $image;
115 $current['agent_seen'] = time();
116 return array( 'status' => 'done', 'image' => $image );
117 } );
118 if ( is_wp_error( $saved ) ) { throw new RuntimeException( $saved->get_error_message() ); }
119 return $saved;
120 } catch ( Throwable $error ) {
121 if ( is_int( $attachment ) && $attachment > 0 ) { wp_delete_attachment( $attachment, true ); }
122 bkbg_external_change( $request['session'], function ( &$current ) use ( $id, $error ) {
123 $current['images'][ $id ]['status'] = 'error';
124 $current['images'][ $id ]['error'] = $error->getMessage();
125 return true;
126 } );
127 return bkbg_external_error( $error->getMessage(), 500 );
128 } finally {
129 wp_set_current_user( $previous_user );
130 if ( $temp && file_exists( $temp ) ) { wp_delete_file( $temp ); }
131 }
132 }
133