prefix . 'smooth_sandbox_recordings'; } public static function chunks(): string { global $wpdb; return $wpdb->prefix . 'smooth_sandbox_chunks'; } public static function install( bool $network_wide = false ): void { if ( $network_wide ) { wp_die( esc_html__( 'Activate Smooth Sandbox on individual sites, not network-wide.', 'smooth-sandbox' ) ); } global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $collate = $wpdb->get_charset_collate(); dbDelta( 'CREATE TABLE ' . self::table() . " ( id varchar(36) NOT NULL, user_id bigint(20) unsigned NOT NULL, title varchar(200) NOT NULL, mime varchar(40) NOT NULL, status varchar(16) NOT NULL DEFAULT 'uploading', bytes bigint(20) unsigned NOT NULL DEFAULT 0, parts int unsigned NOT NULL DEFAULT 0, public_token varchar(64) DEFAULT NULL, created_at datetime NOT NULL, PRIMARY KEY (id), KEY user_id (user_id), UNIQUE KEY public_token (public_token) ) ENGINE=InnoDB $collate;" ); dbDelta( 'CREATE TABLE ' . self::chunks() . " ( recording_id varchar(36) NOT NULL, sequence int unsigned NOT NULL, checksum varchar(64) NOT NULL, data mediumblob NOT NULL, PRIMARY KEY (recording_id,sequence) ) ENGINE=InnoDB $collate;" ); if ( ! wp_next_scheduled( 'smooth_sandbox_cleanup' ) ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'hourly', 'smooth_sandbox_cleanup' ); } } public function __construct() { add_action( 'smooth_sandbox_cleanup', [ self::class, 'cleanup' ] ); add_action( 'delete_user', [ self::class, 'delete_user' ] ); add_action( 'admin_post_smooth_sandbox_video', [ $this, 'serve' ] ); add_action( 'admin_post_nopriv_smooth_sandbox_video', [ $this, 'serve' ] ); add_action( 'admin_post_smooth_sandbox_player', [ $this, 'player' ] ); add_action( 'admin_post_nopriv_smooth_sandbox_player', [ $this, 'player' ] ); } public static function owned( string $id ): array { global $wpdb; $row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id=%s AND user_id=%d', self::table(), $id, get_current_user_id() ), ARRAY_A ); if ( ! current_user_can( 'manage_options' ) || ! $row ) { throw new \RuntimeException( __( 'Recording not found in your account.', 'smooth-sandbox' ) ); } return $row; } /** Site-wide upload reservation bounds concurrent uploads without reading any payload. */ public static function start( string $title, string $mime ): string { global $wpdb; if ( ! in_array( $mime, [ 'video/webm', 'video/mp4' ], true ) ) { throw new \RuntimeException( __( 'This video format is not supported.', 'smooth-sandbox' ) ); } // Serialize reservation creation across users. The lock contains no user data. $lock = 'smooth_sandbox_upload_lock'; if ( ! add_option( $lock, time(), '', false ) ) { if ( (int) get_option( $lock ) < time() - 60 ) { delete_option( $lock ); } throw new \RuntimeException( __( 'Another recording is being prepared. Retry saving in a moment.', 'smooth-sandbox' ) ); } try { $count = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE user_id=%d', self::table(), get_current_user_id() ) ); $reserved = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(CASE WHEN status='uploading' THEN %d ELSE bytes END),0) FROM %i", self::MAX_BYTES, self::table() ) ); if ( $count >= self::MAX_RECORDINGS || $reserved + self::MAX_BYTES > self::SITE_BYTES ) { throw new \RuntimeException( __( 'Recording storage is full. Delete an old recording and retry.', 'smooth-sandbox' ) ); } $id = wp_generate_uuid4(); if ( ! $wpdb->insert( self::table(), [ 'id' => $id, 'user_id' => get_current_user_id(), 'title' => wp_html_excerpt( sanitize_text_field( $title ) ?: __( 'Sandbox session', 'smooth-sandbox' ), 200, '' ), 'mime' => $mime, 'created_at' => current_time( 'mysql', true ) ] ) ) { throw new \RuntimeException( __( 'Could not reserve recording storage.', 'smooth-sandbox' ) ); } return $id; } finally { delete_option( $lock ); } } public static function append( string $id, int $sequence, string $encoded ): void { global $wpdb; self::owned( $id ); if ( $sequence < 0 || $sequence >= (int) ceil( self::MAX_BYTES / self::CHUNK ) || strlen( $encoded ) > 4 * (int) ceil( self::CHUNK / 3 ) ) { throw new \RuntimeException( __( 'Recording chunk is too large.', 'smooth-sandbox' ) ); } $bytes = base64_decode( $encoded, true ); if ( false === $bytes || '' === $bytes || strlen( $bytes ) > self::CHUNK ) { throw new \RuntimeException( __( 'Invalid recording chunk.', 'smooth-sandbox' ) ); } $checksum = hash( 'sha256', $bytes ); $wpdb->query( 'START TRANSACTION' ); try { $row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id=%s FOR UPDATE', self::table(), $id ), ARRAY_A ); if ( ! $row || 'uploading' !== $row['status'] || (int) $row['user_id'] !== get_current_user_id() ) { throw new \RuntimeException( __( 'This upload has ended.', 'smooth-sandbox' ) ); } $old = $wpdb->get_var( $wpdb->prepare( 'SELECT checksum FROM %i WHERE recording_id=%s AND sequence=%d', self::chunks(), $id, $sequence ) ); if ( $old ) { if ( ! hash_equals( $old, $checksum ) ) { throw new \RuntimeException( __( 'The upload retry does not match the saved chunk.', 'smooth-sandbox' ) ); } $wpdb->query( 'COMMIT' ); return; } if ( (int) $row['parts'] !== $sequence || (int) $row['bytes'] !== $sequence * self::CHUNK || (int) $row['bytes'] + strlen( $bytes ) > self::MAX_BYTES ) { throw new \RuntimeException( __( 'Recording chunks are incomplete or out of order.', 'smooth-sandbox' ) ); } if ( 0 === $sequence && ! self::signature( $bytes, $row['mime'] ) ) { throw new \RuntimeException( __( 'This is not a supported video file.', 'smooth-sandbox' ) ); } if ( false === $wpdb->insert( self::chunks(), [ 'recording_id' => $id, 'sequence' => $sequence, 'checksum' => $checksum, 'data' => $bytes ] ) || false === $wpdb->update( self::table(), [ 'bytes' => (int) $row['bytes'] + strlen( $bytes ), 'parts' => $sequence + 1 ], [ 'id' => $id ] ) ) { throw new \RuntimeException( __( 'Could not save this chunk. Retry saving.', 'smooth-sandbox' ) ); } $wpdb->query( 'COMMIT' ); } catch ( \Throwable $error ) { $wpdb->query( 'ROLLBACK' ); throw $error; } } public static function signature( string $bytes, string $mime ): bool { return 'video/webm' === $mime ? str_starts_with( $bytes, "\x1A\x45\xDF\xA3" ) : ( strlen( $bytes ) >= 12 && 'ftyp' === substr( $bytes, 4, 4 ) ); } public static function finish( string $id, int $size ): void { global $wpdb; self::owned( $id ); $wpdb->query( 'START TRANSACTION' ); try { $row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id=%s FOR UPDATE', self::table(), $id ), ARRAY_A ); if ( ! $row || (int) $row['user_id'] !== get_current_user_id() || $size <= 0 || $size !== (int) $row['bytes'] || ! in_array( $row['status'], [ 'uploading', 'ready' ], true ) ) { throw new \RuntimeException( __( 'The video is not completely uploaded. Retry saving.', 'smooth-sandbox' ) ); } if ( false === $wpdb->update( self::table(), [ 'status' => 'ready' ], [ 'id' => $id, 'user_id' => get_current_user_id() ] ) ) { throw new \RuntimeException( __( 'Could not finalize the recording.', 'smooth-sandbox' ) ); } $wpdb->query( 'COMMIT' ); } catch ( \Throwable $error ) { $wpdb->query( 'ROLLBACK' ); throw $error; } } public static function visibility( string $id, bool $public ): void { global $wpdb; $row = self::owned( $id ); if ( 'ready' !== $row['status'] ) { throw new \RuntimeException( __( 'Finish uploading before publishing.', 'smooth-sandbox' ) ); } $token = $public ? ( $row['public_token'] ?: bin2hex( random_bytes( 32 ) ) ) : null; if ( false === $wpdb->update( self::table(), [ 'public_token' => $token ], [ 'id' => $id, 'user_id' => get_current_user_id(), 'status' => 'ready' ] ) ) { throw new \RuntimeException( __( 'Could not change visibility.', 'smooth-sandbox' ) ); } } public static function remove( string $id ): void { global $wpdb; // Lock the parent first, like append(), to prevent orphan chunks during deletion. $wpdb->query( 'START TRANSACTION' ); try { $wpdb->get_var( $wpdb->prepare( 'SELECT id FROM %i WHERE id=%s FOR UPDATE', self::table(), $id ) ); if ( false === $wpdb->delete( self::chunks(), [ 'recording_id' => $id ] ) || false === $wpdb->delete( self::table(), [ 'id' => $id ] ) ) { throw new \RuntimeException( __( 'Could not delete this recording.', 'smooth-sandbox' ) ); } $wpdb->query( 'COMMIT' ); } catch ( \Throwable $error ) { $wpdb->query( 'ROLLBACK' ); throw $error; } } public static function cleanup(): void { global $wpdb; $ids = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM %i WHERE status='uploading' AND created_at < %s", self::table(), gmdate( 'Y-m-d H:i:s', time() - DAY_IN_SECONDS ) ) ); foreach ( $ids as $id ) { self::remove( $id ); } } public static function delete_user( int $user_id ): void { global $wpdb; $ids = $wpdb->get_col( $wpdb->prepare( 'SELECT id FROM %i WHERE user_id=%d', self::table(), $user_id ) ); foreach ( $ids as $id ) { self::remove( $id ); } } public static function url( array $row, bool $public = false, bool $stream = false ): string { $args = [ 'action' => $stream ? 'smooth_sandbox_video' : 'smooth_sandbox_player' ]; if ( $public ) { $args['share'] = $row['public_token']; } else { $args['id'] = $row['id']; $args['_wpnonce'] = wp_create_nonce( 'smooth_sandbox_view_' . $row['id'] ); } return add_query_arg( $args, admin_url( 'admin-post.php' ) ); } private static function accessible(): array { global $wpdb; $share = isset( $_GET['share'] ) && is_string( $_GET['share'] ) ? sanitize_text_field( wp_unslash( $_GET['share'] ) ) : ''; if ( preg_match( '/^[a-f0-9]{64}$/D', $share ) ) { $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM %i WHERE public_token=%s AND status='ready'", self::table(), $share ), ARRAY_A ); } else { $id = isset( $_GET['id'] ) && is_string( $_GET['id'] ) ? sanitize_text_field( wp_unslash( $_GET['id'] ) ) : ''; $nonce = isset( $_GET['_wpnonce'] ) && is_string( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'smooth_sandbox_view_' . $id ) ) { $row = null; } else { try { $row = self::owned( $id ); } catch ( \Throwable $error ) { $row = null; } } } nocache_headers(); header( 'Cache-Control: private, no-store, max-age=0' ); header( 'Referrer-Policy: no-referrer' ); header( 'X-Content-Type-Options: nosniff' ); header( 'X-Robots-Tag: noindex, nofollow, noarchive' ); if ( ! $row || 'ready' !== $row['status'] ) { status_header( 404 ); exit; } return $row; } public function player(): void { $row = self::accessible(); $public = isset( $_GET['share'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- accessible() already authorized this read-only public token or private nonce. // Public player can be embedded, but never execute script or submit a form. header_remove( 'X-Frame-Options' ); header( "Content-Security-Policy: default-src 'none'; media-src 'self'; style-src 'unsafe-inline'; frame-ancestors " . ( $public ? '*' : "'self'" ) . "; base-uri 'none'; form-action 'none'" ); header( 'Content-Type: text/html; charset=utf-8' ); wp_register_style( 'smooth-sandbox-player', false, [], SMOOTH_SANDBOX_VERSION ); wp_enqueue_style( 'smooth-sandbox-player' ); wp_add_inline_style( 'smooth-sandbox-player', 'html,body{margin:0;height:100%;background:#000}video{width:100%;height:100%;object-fit:contain}' ); echo '' . esc_html( $row['title'] ) . ''; // admin-post.php does not run wp_head(); print only this isolated player's queue entry. wp_print_styles( [ 'smooth-sandbox-player' ] ); echo ''; exit; } public static function range( string $header, int $size ): array { if ( '' === $header ) { return [ 0, $size - 1 ]; } if ( ! preg_match( '/^bytes=(\d*)-(\d*)$/D', $header, $match ) || ( '' === $match[1] && '' === $match[2] ) ) { throw new \RuntimeException( 'Invalid range' ); } if ( '' === $match[1] ) { $start = max( 0, $size - (int) $match[2] ); $end = $size - 1; } else { $start = (int) $match[1]; $end = '' === $match[2] ? $size - 1 : min( $size - 1, (int) $match[2] ); } if ( $start < 0 || $start >= $size || $end < $start ) { throw new \RuntimeException( 'Unsatisfiable range' ); } return [ $start, $end ]; } public function serve(): void { global $wpdb; $row = self::accessible(); $size = (int) $row['bytes']; $range = isset( $_SERVER['HTTP_RANGE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_RANGE'] ) ) : ''; try { [ $start, $end ] = self::range( $range, $size ); } catch ( \Throwable $error ) { status_header( 416 ); header( 'Content-Range: bytes */' . $size ); exit; } header( 'Accept-Ranges: bytes' ); header( 'Content-Type: ' . $row['mime'] ); header( 'Content-Disposition: inline; filename="sandbox-recording.' . ( 'video/webm' === $row['mime'] ? 'webm' : 'mp4' ) . '"' ); header( 'Content-Length: ' . ( $end - $start + 1 ) ); if ( $range ) { status_header( 206 ); header( "Content-Range: bytes $start-$end/$size" ); } if ( 'HEAD' === sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ?? '' ) ) ) { exit; } for ( $seq = (int) floor( $start / self::CHUNK ); $seq <= (int) floor( $end / self::CHUNK ); $seq++ ) { $chunk = $wpdb->get_var( $wpdb->prepare( 'SELECT data FROM %i WHERE recording_id=%s AND sequence=%d', self::chunks(), $row['id'], $seq ) ); $offset = max( 0, $start - $seq * self::CHUNK ); $length = min( strlen( (string) $chunk ), $end - $seq * self::CHUNK + 1 ) - $offset; if ( $length > 0 ) { echo substr( $chunk, $offset, $length ); } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Binary video bytes, fixed MIME, nosniff. } exit; } }