1--TEST--
2Curl option CURLOPT_DEBUGFUNCTION
3--EXTENSIONS--
4curl
5--FILE--
6<?php
7include 'server.inc';
8
9$allowedTypes = [
10    CURLINFO_TEXT,
11    CURLINFO_HEADER_IN,
12    CURLINFO_HEADER_OUT,
13    CURLINFO_DATA_IN,
14    CURLINFO_DATA_OUT,
15    CURLINFO_SSL_DATA_OUT,
16    CURLINFO_SSL_DATA_IN,
17];
18
19var_dump(CURLOPT_DEBUGFUNCTION);
20
21$host = curl_cli_server_start();
22
23echo "\n===Testing with regular CURLOPT_VERBOSE with verbose=false===\n";
24$ch = curl_init();
25curl_setopt($ch, CURLOPT_URL, "{$host}/get.inc?test=file");
26curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
27curl_setopt($ch, CURLOPT_VERBOSE, 0);
28var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, function() {
29    echo 'This should not be called';
30}));
31curl_exec($ch);
32curl_setopt($ch, CURLOPT_VERBOSE, 1);
33
34echo "\n===Testing with regular CURLOPT_VERBOSE on stderr===\n";
35$ch = curl_init();
36curl_setopt($ch, CURLOPT_URL, "{$host}/get.inc?test=file");
37curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
38curl_setopt($ch, CURLOPT_VERBOSE, 1);
39$stderr = fopen('php://temp', 'wb+');
40curl_setopt($ch, CURLOPT_STDERR, $stderr);
41$curlVerboseOutput = curl_exec($ch);
42rewind($stderr);
43$receivedOutput = stream_get_contents($stderr);
44fclose($stderr);
45var_dump(str_contains($receivedOutput, 'Host'));
46
47echo "\n===Testing with CURLOPT_DEBUGFUNCTION happy path===\n";
48$stderr = fopen('php://temp', 'wb+');
49curl_setopt($ch, CURLOPT_STDERR, $stderr);
50
51$debugOutput = [];
52var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, function(CurlHandle $curlHandle, int $type, string $data) use ($allowedTypes, &$debugOutput) {
53    if (!in_array($type, $allowedTypes)) {
54        throw new Exception('Unexpected type value: '. $type);
55    }
56    $debugOutput[$type][] = $data;
57}));
58
59$result = curl_exec($ch);
60rewind($stderr);
61$receivedOutputWithDebugFunction = stream_get_contents($stderr);
62fclose($stderr);
63echo "Received stderr empty:\n";
64var_dump($result);
65var_dump($receivedOutputWithDebugFunction);
66
67// Header-out should be an exact match
68var_dump(str_contains($receivedOutput, $debugOutput[CURLINFO_HEADER_OUT][0]));
69
70// Header-in fields should match, except for the "Date" header that is dynamic.
71foreach ($debugOutput[CURLINFO_HEADER_IN] as $headerReceived) {
72    if (str_starts_with($headerReceived, 'Date')) {
73        continue;
74    }
75    if (!str_contains($receivedOutput, $headerReceived)) {
76        throw new \Exception('DEBUGFUNCTION header field does not match the previous verbose debug message');
77    }
78}
79
80echo "\n===Test attempting to set CURLINFO_HEADER_OUT while CURLOPT_DEBUGFUNCTION in effect throws===\n";
81$ch = curl_init();
82var_dump(curl_setopt($ch, CURLINFO_HEADER_OUT, true));
83var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, null));
84var_dump(curl_setopt($ch, CURLINFO_HEADER_OUT, true));
85var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, 'strlen'));
86try {
87    var_dump(curl_setopt($ch, CURLINFO_HEADER_OUT, true));
88}
89catch (\ValueError $e) {
90    var_dump($e->getMessage());
91}
92$chCopy = curl_copy_handle($ch);
93try {
94    var_dump(curl_setopt($chCopy, CURLINFO_HEADER_OUT, true));
95}
96catch (\ValueError $e) {
97    var_dump($e->getMessage());
98}
99var_dump(curl_setopt($chCopy, CURLOPT_DEBUGFUNCTION, null));
100var_dump(curl_setopt($chCopy, CURLINFO_HEADER_OUT, true));
101
102echo "\n===Test attempting to set CURLOPT_DEBUGFUNCTION while CURLINFO_HEADER_OUT does not throw===\n";
103$ch = curl_init();
104var_dump(curl_setopt($ch, CURLINFO_HEADER_OUT, true));
105var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, null));
106var_dump(curl_setopt($ch, CURLINFO_HEADER_OUT, true));
107var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, 'strlen'));
108$chCopy = curl_copy_handle($ch);
109var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, 'strlen'));
110var_dump(curl_setopt($chCopy, CURLOPT_DEBUGFUNCTION, null));
111var_dump(curl_setopt($chCopy, CURLINFO_HEADER_OUT, 1));
112
113echo "\n===Test CURLOPT_DEBUGFUNCTION=null works===\n";
114$ch = curl_init("{$host}/get.inc?test=file");
115var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, null));
116curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
117var_dump(curl_exec($ch));
118
119echo "\n===Test CURLINFO_HEADER_OUT works while CURLOPT_DEBUGFUNCTION in effect===\n";
120$ch = curl_init("{$host}/get.inc?test=file");
121curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
122var_dump(curl_getinfo($ch, CURLINFO_HEADER_OUT));
123var_dump(curl_setopt($ch, CURLINFO_HEADER_OUT, true));
124var_dump(curl_getinfo($ch, CURLINFO_HEADER_OUT));
125var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, static function() {}));
126var_dump(curl_getinfo($ch, CURLINFO_HEADER_OUT));
127var_dump($result = curl_exec($ch));
128var_dump(curl_getinfo($ch, CURLINFO_HEADER_OUT));
129
130echo "\n===Test CURLOPT_DEBUGFUNCTION can throw within callback===\n";
131$ch = curl_init("{$host}/get.inc?test=file");
132curl_setopt($ch, CURLOPT_DEBUGFUNCTION, static function() {
133    throw new \RuntimeException('This should get caught after verbose=true');
134});
135var_dump(curl_exec($ch));
136curl_setopt($ch, CURLOPT_VERBOSE, true);
137try {
138    var_dump($result = curl_exec($ch));
139}
140catch (\RuntimeException $e) {
141    var_dump($e->getMessage());
142}
143var_dump(curl_getinfo($ch, CURLINFO_HEADER_OUT));
144
145echo "\n===Test CURLOPT_DEBUGFUNCTION on shared handles work===\n";
146$ch = curl_init("{$host}/get.inc?test=file");
147$called = false;
148curl_setopt_array($ch, [
149    CURLOPT_VERBOSE => true,
150    CURLOPT_DEBUGFUNCTION => static function() use (&$called) {
151        $called = true;
152    },
153]);
154var_dump($called);
155curl_exec($ch);
156var_dump($called);
157$called = false;
158$ch2 = curl_copy_handle($ch);
159curl_exec($ch2);
160var_dump($called);
161var_dump(curl_getinfo($ch2, CURLINFO_HEADER_OUT));
162
163echo "\nDone";
164?>
165--EXPECTF--
166int(20094)
167
168===Testing with regular CURLOPT_VERBOSE with verbose=false===
169bool(true)
170
171===Testing with regular CURLOPT_VERBOSE on stderr===
172bool(true)
173
174===Testing with CURLOPT_DEBUGFUNCTION happy path===
175bool(true)
176Received stderr empty:
177string(0) ""
178string(0) ""
179bool(true)
180
181===Test attempting to set CURLINFO_HEADER_OUT while CURLOPT_DEBUGFUNCTION in effect throws===
182bool(true)
183bool(true)
184bool(true)
185bool(true)
186string(87) "CURLINFO_HEADER_OUT option must not be set when the CURLOPT_DEBUGFUNCTION option is set"
187string(87) "CURLINFO_HEADER_OUT option must not be set when the CURLOPT_DEBUGFUNCTION option is set"
188bool(true)
189bool(true)
190
191===Test attempting to set CURLOPT_DEBUGFUNCTION while CURLINFO_HEADER_OUT does not throw===
192bool(true)
193bool(true)
194bool(true)
195bool(true)
196bool(true)
197bool(true)
198bool(true)
199
200===Test CURLOPT_DEBUGFUNCTION=null works===
201bool(true)
202string(0) ""
203
204===Test CURLINFO_HEADER_OUT works while CURLOPT_DEBUGFUNCTION in effect===
205bool(false)
206bool(true)
207bool(false)
208bool(true)
209bool(false)
210string(0) ""
211string(%d) "GET /get.inc?test=file HTTP/%s
212Host: %s:%d
213Accept: */*
214
215"
216
217===Test CURLOPT_DEBUGFUNCTION can throw within callback===
218bool(true)
219string(41) "This should get caught after verbose=true"
220string(%d) "GET /get.inc?test=file HTTP/%s
221Host: %s:%d
222Accept: */*
223
224"
225
226===Test CURLOPT_DEBUGFUNCTION on shared handles work===
227bool(false)
228bool(true)
229bool(true)
230string(71) "GET /get.inc?test=file HTTP/%s
231Host: %s:%d
232Accept: */*
233
234"
235
236Done
237