1+ <?php
2+
3+ namespace SaintSystems \OData ;
4+
5+ class ODataBatchResponse implements IODataResponse
6+ {
7+ private IODataRequest $ request ;
8+ private ?string $ body ;
9+ private array $ headers ;
10+ private ?string $ httpStatusCode ;
11+ private array $ responses ;
12+ private ?string $ boundary ;
13+
14+ public function __construct (IODataRequest $ request , ?string $ body = null , ?string $ httpStatusCode = null , array $ headers = array ())
15+ {
16+ $ this ->request = $ request ;
17+ $ this ->body = $ body ;
18+ $ this ->httpStatusCode = $ httpStatusCode ;
19+ $ this ->headers = $ headers ;
20+ $ this ->boundary = $ this ->extractBoundary ();
21+ $ this ->responses = $ this ->parseBatchResponse ();
22+ }
23+
24+ private function extractBoundary (): ?string
25+ {
26+ $ contentType = $ this ->getContentTypeHeader ();
27+ if ($ contentType !== null && $ contentType !== '' && preg_match ('/boundary=([" \']?)([^" \';]+)\1/ ' , $ contentType , $ matches )) {
28+ $ boundary = $ matches [2 ];
29+
30+ if (strpos (strtolower ($ boundary ), 'batchresponse ' ) !== false ) {
31+ return $ boundary ;
32+ }
33+ }
34+ return null ;
35+ }
36+
37+ private function getContentTypeHeader (): ?string
38+ {
39+ foreach ($ this ->headers as $ key => $ value ) {
40+ if (strtolower ($ key ) === 'content-type ' ) {
41+ return is_array ($ value ) ? $ value [0 ] : $ value ;
42+ }
43+ }
44+ return null ;
45+ }
46+
47+ private function parseBatchResponse (): array
48+ {
49+ if ($ this ->body === null || $ this ->body === '' || $ this ->boundary === null || $ this ->boundary === '' ) {
50+ return array ();
51+ }
52+
53+ $ responses = array ();
54+ $ parts = explode ('-- ' . $ this ->boundary , $ this ->body );
55+
56+ foreach ($ parts as $ part ) {
57+ $ part = trim ($ part );
58+ // Skip empty parts and boundary end marker
59+ if ($ part === '' || $ part === '-- ' || $ part === "\r\n-- " || trim ($ part , "\r\n- " ) === '' ) {
60+ continue ;
61+ }
62+
63+ if ($ this ->isChangesetPart ($ part )) {
64+ $ changesetResponses = $ this ->parseChangesetPart ($ part );
65+ $ responses = array_merge ($ responses , $ changesetResponses );
66+ } else {
67+ $ response = $ this ->parseIndividualResponse ($ part );
68+ if ($ response !== null ) {
69+ $ responses [] = $ response ;
70+ }
71+ }
72+ }
73+
74+ return $ responses ;
75+ }
76+
77+ private function isChangesetPart (string $ part ): bool
78+ {
79+ return preg_match ('/Content-Type:\s*multipart\/mixed;\s*boundary=[" \']?[^" \'\s]*changeset[^" \'\s]*[" \']?/i ' , $ part ) === 1 ;
80+ }
81+
82+ private function parseChangesetPart (string $ part ): array
83+ {
84+ if (preg_match ('/boundary=([" \']?)([^" \'\r\n;]+)\1/i ' , $ part , $ matches ) !== 1 ) {
85+ return array ();
86+ }
87+
88+ $ changesetBoundary = $ matches [2 ];
89+
90+ if ($ changesetBoundary === '' || strpos (strtolower ($ changesetBoundary ), 'changeset ' ) === false ) {
91+ return array ();
92+ }
93+
94+ // Find where changeset content starts (after empty line)
95+ $ changesetContent = $ this ->extractChangesetContent ($ part );
96+
97+ // Parse individual responses within changeset
98+ $ changesetParts = explode ('-- ' . $ changesetBoundary , $ changesetContent );
99+ $ responses = array ();
100+
101+ foreach ($ changesetParts as $ changesetPart ) {
102+ $ changesetPart = trim ($ changesetPart );
103+ if ($ changesetPart === '' || $ changesetPart === '-- ' || trim ($ changesetPart , "\r\n- " ) === '' ) {
104+ continue ;
105+ }
106+
107+ $ response = $ this ->parseIndividualResponse ($ changesetPart );
108+ if ($ response !== null ) {
109+ $ responses [] = $ response ;
110+ }
111+ }
112+
113+ return $ responses ;
114+ }
115+
116+ private function extractChangesetContent (string $ part ): string
117+ {
118+ $ lines = explode ("\n" , $ part );
119+ $ contentStarted = false ;
120+ $ content = array ();
121+
122+ foreach ($ lines as $ line ) {
123+ $ line = rtrim ($ line , "\r" );
124+
125+ // Skip until we find an empty line (end of changeset headers)
126+ if (!$ contentStarted ) {
127+ if (trim ($ line ) === '' ) {
128+ $ contentStarted = true ;
129+ }
130+ continue ;
131+ }
132+
133+ $ content [] = $ line ;
134+ }
135+
136+ return implode ("\n" , $ content );
137+ }
138+
139+ private function parseIndividualResponse (string $ part ): ?ODataResponse
140+ {
141+ $ lines = explode ("\n" , $ part );
142+ $ inHeaders = true ;
143+ $ responseHeaders = array ();
144+ $ responseBody = '' ;
145+ $ statusCode = null ;
146+ $ foundHttpResponse = false ;
147+
148+ foreach ($ lines as $ line ) {
149+ $ line = rtrim ($ line , "\r" );
150+
151+ if ($ inHeaders ) {
152+ if (trim ($ line ) === '' ) {
153+ // Only switch to body parsing if we've found an HTTP response line
154+ if ($ foundHttpResponse ) {
155+ $ inHeaders = false ;
156+ }
157+ continue ;
158+ }
159+
160+ if (strpos ($ line , 'HTTP/ ' ) === 0 ) {
161+ $ statusParts = explode (' ' , $ line , 3 );
162+ $ statusCode = (array_key_exists (1 , $ statusParts ) && $ statusParts [1 ] !== null ) ? $ statusParts [1 ] : (string )HttpStatusCode::OK ;
163+ $ foundHttpResponse = true ;
164+ continue ;
165+ }
166+
167+ // Only parse headers after we've found the HTTP response line
168+ if ($ foundHttpResponse && strpos ($ line , ': ' ) !== false ) {
169+ list ($ key , $ value ) = explode (': ' , $ line , 2 );
170+ $ responseHeaders [trim ($ key )] = trim ($ value );
171+ }
172+ } else {
173+ $ responseBody .= $ line . "\n" ;
174+ }
175+ }
176+
177+ $ responseBody = trim ($ responseBody );
178+
179+ if ($ statusCode !== null ) {
180+ return new ODataResponse ($ this ->request , $ responseBody , $ statusCode , $ responseHeaders );
181+ }
182+
183+ return null ;
184+ }
185+
186+ public function getBody (): array
187+ {
188+ $ bodies = array ();
189+ foreach ($ this ->responses as $ response ) {
190+ $ bodies [] = $ response ->getBody ();
191+ }
192+ return $ bodies ;
193+ }
194+
195+ public function getRawBody (): ?string
196+ {
197+ return $ this ->body ;
198+ }
199+
200+ public function getStatus (): ?string
201+ {
202+ return $ this ->httpStatusCode ;
203+ }
204+
205+ public function getHeaders (): array
206+ {
207+ return $ this ->headers ;
208+ }
209+
210+ public function getResponses (): array
211+ {
212+ return $ this ->responses ;
213+ }
214+
215+ public function getResponse (int $ index ): ?ODataResponse
216+ {
217+ return (array_key_exists ($ index , $ this ->responses ) && $ this ->responses [$ index ] !== null ) ? $ this ->responses [$ index ] : null ;
218+ }
219+ }
0 commit comments