self::MAX_SIZE ) { return new \WP_Error( 'breznflow_too_large', __( 'Workflow JSON exceeds the 2MB size limit.', 'breznflow' ) ); } // Check 1: JSON parse. try { $data = json_decode( $raw, true, 512, JSON_THROW_ON_ERROR ); } catch ( \JsonException $e ) { return new \WP_Error( 'breznflow_invalid_json', sprintf( /* translators: %s: JSON parse error message */ __( 'Invalid JSON: %s', 'breznflow' ), $e->getMessage() ) ); } // Check 2: Top-level schema. if ( ! is_array( $data ) ) { return new \WP_Error( 'breznflow_not_object', __( 'Workflow must be a JSON object.', 'breznflow' ) ); } if ( empty( $data['name'] ) || ! is_string( $data['name'] ) ) { return new \WP_Error( 'breznflow_missing_name', __( 'Workflow must have a non-empty "name" field.', 'breznflow' ) ); } if ( ! isset( $data['nodes'] ) || ! is_array( $data['nodes'] ) || empty( $data['nodes'] ) ) { return new \WP_Error( 'breznflow_missing_nodes', __( 'Workflow must have a non-empty "nodes" array.', 'breznflow' ) ); } if ( ! isset( $data['connections'] ) || ! is_array( $data['connections'] ) ) { return new \WP_Error( 'breznflow_missing_connections', __( 'Workflow must have a "connections" object.', 'breznflow' ) ); } // Check 3: Node structure + count. if ( count( $data['nodes'] ) > self::MAX_NODES ) { return new \WP_Error( 'breznflow_too_many_nodes', sprintf( /* translators: %d: maximum node count */ __( 'Workflow has too many nodes (max %d).', 'breznflow' ), self::MAX_NODES ) ); } foreach ( $data['nodes'] as $index => $node ) { $err = self::validate_node( $node, $index ); if ( is_wp_error( $err ) ) { return $err; } } // Check 4: Connection integrity. $node_names = array_column( $data['nodes'], 'name' ); foreach ( array_keys( $data['connections'] ) as $conn_node_name ) { if ( ! in_array( $conn_node_name, $node_names, true ) ) { return new \WP_Error( 'breznflow_invalid_connection', sprintf( /* translators: %s: node name from connections */ __( 'Connection references unknown node: "%s".', 'breznflow' ), esc_html( $conn_node_name ) ) ); } } return $data; } /** * Validates a single node structure. * * @param mixed $node Node data (should be array). * @param int $index Node index for error messages. * @return true|\WP_Error */ private static function validate_node( $node, int $index ) { if ( ! is_array( $node ) ) { return new \WP_Error( 'breznflow_invalid_node', sprintf( /* translators: %d: node index */ __( 'Node %d must be an object.', 'breznflow' ), $index ) ); } // id: UUID format. if ( empty( $node['id'] ) || ! is_string( $node['id'] ) || ! preg_match( self::UUID_PATTERN, $node['id'] ) ) { return new \WP_Error( 'breznflow_invalid_node_id', sprintf( /* translators: %d: node index */ __( 'Node %d has an invalid or missing "id" (must be UUID format).', 'breznflow' ), $index ) ); } // name: non-empty string. if ( ! isset( $node['name'] ) || ! is_string( $node['name'] ) || '' === $node['name'] ) { return new \WP_Error( 'breznflow_invalid_node_name', sprintf( /* translators: %d: node index */ __( 'Node %d has an invalid or missing "name".', 'breznflow' ), $index ) ); } // type: alphanumeric + allowed chars. if ( empty( $node['type'] ) || ! is_string( $node['type'] ) || ! preg_match( self::TYPE_PATTERN, $node['type'] ) ) { return new \WP_Error( 'breznflow_invalid_node_type', sprintf( /* translators: %d: node index */ __( 'Node %d has an invalid or missing "type".', 'breznflow' ), $index ) ); } // position: [int, int]. if ( ! isset( $node['position'] ) || ! is_array( $node['position'] ) || count( $node['position'] ) < 2 || ! is_numeric( $node['position'][0] ) || ! is_numeric( $node['position'][1] ) ) { return new \WP_Error( 'breznflow_invalid_node_position', sprintf( /* translators: %d: node index */ __( 'Node %d has an invalid or missing "position" ([x, y] required).', 'breznflow' ), $index ) ); } // typeVersion: numeric. if ( ! isset( $node['typeVersion'] ) || ! is_numeric( $node['typeVersion'] ) ) { return new \WP_Error( 'breznflow_invalid_node_version', sprintf( /* translators: %d: node index */ __( 'Node %d has an invalid or missing "typeVersion".', 'breznflow' ), $index ) ); } return true; } }