Passed
Pull Request — master (#255)
by Christopher
03:33
created

ChangeSetParser::getRawRequests()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace POData\BatchProcessor;
6
7
use Exception;
8
use POData\BaseService;
9
use POData\Common\ODataException;
10
use POData\Common\UrlFormatException;
11
use POData\OperationContext\HTTPRequestMethod;
12
use POData\OperationContext\ServiceHost;
13
use POData\OperationContext\Web\IncomingRequest;
14
use POData\OperationContext\Web\WebOperationContext;
15
16
/**
17
 * Class ChangeSetParser.
18
 * @package POData\BatchProcessor
19
 */
20
class ChangeSetParser implements IBatchParser
21
{
22
    protected $data;
23
    protected $changeSetBoundary;
24
    /**
25
     * @var WebOperationContext[]
26
     */
27
    protected $rawRequests = [];
28
    protected $service;
29
    protected $contentIDToLocationLookup = [];
30
31
    /**
32
     * ChangeSetParser constructor.
33
     * @param BaseService $service
34
     * @param $body
35
     */
36
    public function __construct(BaseService $service, $body)
37
    {
38
        $this->service            = $service;
39
        $this->data               = trim(str_replace("\r", '', $body)); // removes windows specific character
40
    }
41
42
    /**
43
     * @return mixed
44
     */
45
    public function getBoundary()
46
    {
47
        return $this->changeSetBoundary;
48
    }
49
50
    /**
51
     * @throws ODataException
52
     * @throws UrlFormatException
53
     * @throws Exception
54
     */
55
    public function process()
56
    {
57
        $raw = $this->getRawRequests();
58
        foreach ($raw as $contentID => &$workingObject) {
59
            foreach ($this->contentIDToLocationLookup as $lookupID => $location) {
60
                if (0 > $lookupID) {
61
                    continue;
62
                }
63
                $workingObject->incomingRequest()->applyContentID($lookupID, $location);
64
            }
65
66
            $this->processSubRequest($workingObject);
67
            if (HTTPRequestMethod::GET() != $workingObject->incomingRequest()->getMethod() &&
68
                !strpos($workingObject->incomingRequest()->getRawUrl(), '/$links/') === false) {
69
                if (null === $workingObject->outgoingResponse()->getHeaders()['Location']) {
70
                    $msg = 'Location header not set in subrequest response for ' . $workingObject->incomingRequest()->getMethod()
71
                        . ' request url ' . $workingObject->incomingRequest()->getRawUrl();
72
                    throw new Exception($msg);
73
                }
74
                $this->contentIDToLocationLookup[$contentID] = $workingObject->outgoingResponse()->getHeaders()['Location'];
75
            }
76
        }
77
    }
78
79
    /**
80
     * @return array
81
     */
82
    public function getRawRequests()
83
    {
84
        return $this->rawRequests;
85
    }
86
87
    /**
88
     * @param $newContext
89
     * @throws ODataException
90
     * @throws UrlFormatException
91
     */
92
    protected function processSubRequest(&$newContext)
93
    {
94
        $newHost    = new ServiceHost($newContext);
95
        $oldHost    = $this->getService()->getHost();
96
        $this->getService()->setHost($newHost);
97
        $this->getService()->handleRequest();
98
        $this->getService()->setHost($oldHost);
99
    }
100
101
    /**
102
     * @return BaseService
103
     */
104
    public function getService()
105
    {
106
        return $this->service;
107
    }
108
109
    /**
110
     * @return string
111
     */
112
    public function getResponse()
113
    {
114
        $ODataEOL = $this->getService()->getConfiguration()->getLineEndings();
115
116
        $response = '';
117
        $splitter = false === $this->changeSetBoundary ?
118
            '' :
119
            '--' . $this->changeSetBoundary . $ODataEOL;
120
        $raw = $this->getRawRequests();
121
        foreach ($raw as $contentID => &$workingObject) {
122
            $headers = $workingObject->outgoingResponse()->getHeaders();
123
            $response .= $splitter;
124
125
            $response .= 'Content-Type: application/http' . $ODataEOL;
126
            $response .= 'Content-Transfer-Encoding: binary' . $ODataEOL;
127
            $response .= $ODataEOL;
128
            $response .= 'HTTP/1.1 ' . $headers['Status'] . $ODataEOL;
129
            $response .= 'Content-ID: ' . $contentID . $ODataEOL;
130
131
            foreach ($headers as $headerName => $headerValue) {
132
                if (null !== $headerValue) {
133
                    $response .= $headerName . ': ' . $headerValue . $ODataEOL;
134
                }
135
            }
136
            $response .= $ODataEOL;
137
            $response .= $workingObject->outgoingResponse()->getStream();
138
        }
139
        $response .= trim($splitter);
140
        $response .= false === $this->changeSetBoundary ?
141
            $ODataEOL :
142
            '--' . $ODataEOL;
143
        $response = 'Content-Length: ' .
144
            strlen($response) .
145
            $ODataEOL .
146
            $ODataEOL .
147
            $response;
148
        $response = false === $this->changeSetBoundary ?
149
            $response :
150
            'Content-Type: multipart/mixed; boundary=' .
151
            $this->changeSetBoundary .
152
            $ODataEOL .
153
            $response;
154
        return $response;
155
    }
156
157
    public function handleData()
158
    {
159
        list($headerBlock, $contentBlock) = explode("\n\n", $this->getData(), 2);
160
        $headers                          = self::parse_headers($headerBlock);
161
        $this->changeSetBoundary          = $headers['Content-Type']['boundary'];
162
        $prefix                           = 'HTTP_';
163
        $matches                          = array_filter(explode('--' . $this->changeSetBoundary, $contentBlock));
164
        $contentIDinit                    = -1;
165
        foreach ($matches as $match) {
166
            if ('--' === trim($match)) {
167
                continue;
168
            }
169
170
            list($RequestParams, $requestHeaders, $RequestBody) = explode("\n\n", $match);
171
            $RequestBody                                        = trim($RequestBody);
172
            $requestHeadersArray                                = self::parse_headers($requestHeaders);
173
            list($RequesetType, $RequestPath, $RequestProticol) = explode(' ', $requestHeadersArray['default'], 3);
174
            $contentID                                          = array_key_exists('Content-ID', $requestHeadersArray) ? $requestHeadersArray['Content-ID'] : $contentIDinit;
175
            $inboundRequestHeaders                              = [];
176
            $skip                                               = true;
177
            foreach (explode("\n", $requestHeaders) as $headerLine) {
178
                if ($skip) {
179
                    $skip = false;
180
                    continue;
181
                }
182
                list($key, $value)            = explode(':', $headerLine);
183
                $name                         = strtr(strtoupper(trim($key)), '-', '_');
184
                $value                        = trim($value);
185
                $name                         = substr($name, 0, strlen($prefix)) === $prefix || $name == 'CONTENT_TYPE' ? $name : $prefix . $name;
186
                $inboundRequestHeaders[$name] = $value;
187
            }
188
            $this->rawRequests[$contentID] = new WebOperationContext(
189
                new IncomingRequest(
190
                    new HTTPRequestMethod($RequesetType),
191
                    [],
192
                    [],
193
                    $inboundRequestHeaders,
194
                    null,
195
                    $RequestBody,
196
                    $RequestPath
197
                )
198
            );
199
            if ($contentIDinit == $contentID) {
200
                $contentIDinit--;
201
            }
202
            continue;
203
        }
204
    }
205
206
    /**
207
     * @return string
208
     */
209
    public function getData()
210
    {
211
        return $this->data;
212
    }
213
214
215
    private static function parse_headers($headers, $splitter = "\n", $assignmentChar = ':')
216
    {
217
        $results = [];
218
        foreach (array_filter(explode($splitter, trim($headers))) as $line) {
219
            list($key, $value) = strpos($line, $assignmentChar) !== false ? explode($assignmentChar, $line, 2) : ['default', $line];
220
            $key               = trim($key);
221
            $value             = trim($value);
222
            if (strpos($value, ';') !== false) {
223
                $value = self::parse_headers($value, ';', '=');
224
            }
225
            if (isset($results[$key])) {
226
                if (is_array($results[$key])) {
227
                    $results[$key][] = $value;
228
                } else {
229
                    $results[$key] = [$results[$key], $value];
230
                }
231
            } else {
232
                $results[$key] = $value;
233
            }
234
        }
235
        return $results;
236
    }
237
}
238