| 1 |
<?php |
| 2 |
/** Session-private screenshots: no public uploads or model credentials. */ |
| 3 |
if ( ! defined( 'ABSPATH' ) ) { exit; } |
| 4 |
function bkbg_external_render_section( $request ) { |
| 5 |
$s = bkbg_external_read( $request['session'] ); |
| 6 |
if ( ! $s ) { return bkbg_external_error( 'Session expired.', 410 ); } |
| 7 |
$content = $request['content']; |
| 8 |
if ( ! is_string( $content ) || strlen( $content ) > 1000000 ) { return bkbg_external_error( 'Section content is too large.', 413 ); } |
| 9 |
$blocks = parse_blocks( $content ); |
| 10 |
if ( count( $blocks ) !== 1 || empty( $blocks[0]['blockName'] ) ) { return bkbg_external_error( 'Supply one serialized section.' ); } |
| 11 |
$previous = $GLOBALS['post'] ?? null; |
| 12 |
try { |
| 13 |
$GLOBALS['post'] = get_post( $s['post'] ); |
| 14 |
$html = render_block( $blocks[0] ); |
| 15 |
return array( 'html' => $html, 'themeCss' => function_exists( 'wp_get_global_stylesheet' ) ? wp_get_global_stylesheet() : '' ); |
| 16 |
} finally { $GLOBALS['post'] = $previous; } |
| 17 |
} |
| 18 |
function bkbg_external_save_capture( $request ) { |
| 19 |
$id = $request->get_url_params()['capture']; |
| 20 |
$data = $request['data']; |
| 21 |
if ( strlen( $request->get_body() ) > 4300000 || ! is_string( $data ) || strpos( $data, 'data:image/png;base64,' ) !== 0 ) { return bkbg_external_error( 'Supply a PNG screenshot up to 3 MB.', 413 ); } |
| 22 |
$bytes = base64_decode( substr( $data, 22 ), true ); |
| 23 |
$info = $bytes ? @getimagesizefromstring( $bytes ) : false; |
| 24 |
if ( ! $info || 'image/png' !== $info['mime'] || $info[0] > 1600 || $info[1] > 6000 || ! in_array( $request['viewport'], array( 'desktop', 'mobile' ), true ) || ! is_string( $request['clientId'] ) || strlen( $request['clientId'] ) > 100 ) { return bkbg_external_error( 'Invalid section screenshot.' ); } |
| 25 |
$warnings = array_slice( array_filter( (array) $request['warnings'], 'is_string' ), 0, 10 ); |
| 26 |
$record = array( 'data' => base64_encode( $bytes ), 'clientId' => $request['clientId'], 'viewport' => $request['viewport'], 'width' => $info[0], 'height' => $info[1], 'warnings' => array_map( 'sanitize_text_field', $warnings ), 'captured_at' => time(), 'reviewed' => false, 'delivered' => false ); |
| 27 |
return bkbg_external_change( $request['session'], function ( &$s ) use ( $id, $record ) { |
| 28 |
$captures = $s['captures'] ?? array(); |
| 29 |
if ( isset( $captures[ $id ] ) ) { |
| 30 |
if ( $captures[ $id ]['data'] !== $record['data'] ) { return bkbg_external_error( 'Capture id already used.', 409 ); } |
| 31 |
} else { |
| 32 |
foreach ( $captures as $old_id => $old ) { if ( $old['clientId'] === $record['clientId'] && $old['viewport'] === $record['viewport'] ) { unset( $captures[ $old_id ] ); } } |
| 33 |
$record['revision'] = $s['visual_revision'] ?? 0; |
| 34 |
$captures[ $id ] = $record; |
| 35 |
$s['captures'] = array_slice( $captures, -4, null, true ); |
| 36 |
} |
| 37 |
return array( 'id' => $id, 'path' => '/captures/' . $id, 'width' => $record['width'], 'height' => $record['height'], 'viewport' => $record['viewport'], 'warnings' => $record['warnings'], 'kind' => 'wordpress-static-render', 'review_required' => true ); |
| 38 |
} ); |
| 39 |
} |
| 40 |
function bkbg_external_get_capture( $request ) { |
| 41 |
$s = bkbg_external_read( $request['session'] ); |
| 42 |
$id = $request->get_url_params()['capture']; |
| 43 |
if ( ! $s ) { return bkbg_external_error( 'Session expired.', 410 ); } |
| 44 |
if ( empty( $s['captures'][ $id ] ) ) { return bkbg_external_error( 'Screenshot expired. Capture the section again.', 404 ); } |
| 45 |
$delivery = bkbg_external_change( $request['session'], function ( &$current ) use ( $id ) { |
| 46 |
if ( empty( $current['captures'][ $id ] ) ) { return bkbg_external_error( 'Screenshot expired.', 404 ); } |
| 47 |
$current['captures'][ $id ]['delivered'] = true; |
| 48 |
return true; |
| 49 |
} ); |
| 50 |
if ( is_wp_error( $delivery ) ) { return $delivery; } |
| 51 |
return new WP_REST_Response( base64_decode( $s['captures'][ $id ]['data'] ), 200, array( 'Content-Type' => 'image/png', 'X-Content-Type-Options' => 'nosniff', 'Cache-Control' => 'no-store, private' ) ); |
| 52 |
} |
| 53 |
function bkbg_external_review_capture( $request ) { |
| 54 |
$ids = $request['capture_ids']; $decision = $request['decision']; $notes = $request['notes']; |
| 55 |
if ( ! is_array( $ids ) || ! count( $ids ) || count( $ids ) > 4 || array_filter( $ids, function ( $id ) { return ! is_string( $id ); } ) || ! in_array( $decision, array( 'passed', 'needs_changes' ), true ) || ! is_string( $notes ) || ! trim( $notes ) || strlen( $notes ) > 6000 ) { return bkbg_external_error( 'Supply capture_ids, decision (passed or needs_changes), and visual review notes.' ); } |
| 56 |
return bkbg_external_change( $request['session'], function ( &$s ) use ( $ids, $decision, $notes ) { |
| 57 |
foreach ( $ids as $id ) { |
| 58 |
if ( empty( $s['captures'][ $id ] ) || $s['captures'][ $id ]['revision'] !== ( $s['visual_revision'] ?? 0 ) ) { return bkbg_external_error( 'Screenshot is missing or predates the last edit. Capture again before reviewing.', 409 ); } |
| 59 |
} |
| 60 |
foreach ( $ids as $id ) { if ( empty( $s['captures'][ $id ]['delivered'] ) ) { return bkbg_external_error( 'Download and open each screenshot before submitting its visual review.', 409 ); } } |
| 61 |
foreach ( $ids as $id ) { $s['captures'][ $id ]['reviewed'] = $decision; } |
| 62 |
$s['visual_review'] = array( 'decision' => $decision, 'notes' => sanitize_textarea_field( $notes ), 'updated' => time() ); |
| 63 |
return array( 'ok' => true, 'decision' => $decision ); |
| 64 |
} ); |
| 65 |
} |
| 66 |
add_filter( 'rest_pre_serve_request', function ( $served, $response, $request ) { |
| 67 |
if ( preg_match( '#^/blockenberg/v1/ai/external/[a-f0-9]{32}/captures/[a-zA-Z0-9_-]+$#D', $request->get_route() ) && 'GET' === $request->get_method() && 200 === $response->get_status() && ( $response->get_headers()['Content-Type'] ?? '' ) === 'image/png' ) { |
| 68 |
echo $response->get_data(); // Validated PNG, available only after bearer authorization. |
| 69 |
return true; |
| 70 |
} |
| 71 |
return $served; |
| 72 |
}, 10, 3 ); |
| 73 |
|