ext_php_rs/zend/
headers.rs1use std::{ffi::c_void, mem, ptr};
5
6use crate::{
7 error::{Error, Result},
8 ffi::{
9 ZEND_RESULT_CODE_SUCCESS, ext_php_rs_output_activated, sapi_header_line, sapi_header_op,
10 sapi_header_op_enum, sapi_header_op_enum_SAPI_HEADER_ADD,
11 sapi_header_op_enum_SAPI_HEADER_DELETE, sapi_header_op_enum_SAPI_HEADER_DELETE_ALL,
12 sapi_header_op_enum_SAPI_HEADER_REPLACE, sapi_header_op_enum_SAPI_HEADER_SET_STATUS,
13 },
14 zend::globals::lock,
15};
16
17pub fn set_header(header: &str) -> Result<()> {
34 line_op(sapi_header_op_enum_SAPI_HEADER_REPLACE, header)
35}
36
37pub fn add_header(header: &str) -> Result<()> {
44 line_op(sapi_header_op_enum_SAPI_HEADER_ADD, header)
45}
46
47pub fn remove_header(name: &str) -> Result<()> {
53 line_op(sapi_header_op_enum_SAPI_HEADER_DELETE, name)
54}
55
56pub fn remove_all_headers() -> Result<()> {
62 header_op(sapi_header_op_enum_SAPI_HEADER_DELETE_ALL, ptr::null_mut())
63}
64
65pub fn set_response_code(code: u16) -> Result<()> {
71 header_op(
72 sapi_header_op_enum_SAPI_HEADER_SET_STATUS,
73 ptr::without_provenance_mut(usize::from(code)),
74 )
75}
76
77fn line_op(op: sapi_header_op_enum, line: &str) -> Result<()> {
78 let mut header_line: sapi_header_line = unsafe { mem::zeroed() };
81 header_line.line = line.as_ptr().cast();
82 header_line.line_len = line.len();
83 header_op(op, (&raw mut header_line).cast())
84}
85
86fn header_op(op: sapi_header_op_enum, arg: *mut c_void) -> Result<()> {
87 if !unsafe { ext_php_rs_output_activated() } || sapi_globals_borrowed() {
90 return Err(Error::ResponseHeaderFailed);
91 }
92 if unsafe { sapi_header_op(op, arg) } == ZEND_RESULT_CODE_SUCCESS {
100 Ok(())
101 } else {
102 Err(Error::ResponseHeaderFailed)
103 }
104}
105
106fn sapi_globals_borrowed() -> bool {
107 cfg_if::cfg_if! {
108 if #[cfg(php_zts)] {
109 lock::SAPI_GLOBALS_LOCK.with(|lock| lock.is_locked())
110 } else {
111 lock::SAPI_GLOBALS_LOCK.is_locked()
112 }
113 }
114}
115
116#[cfg(all(test, feature = "embed"))]
117mod tests {
118 use super::*;
119 use crate::{embed::Embed, zend::SapiGlobals};
120
121 fn headers() -> String {
122 Embed::eval("implode('|', preg_grep('/^X-Test/', headers_list()));")
123 .ok()
124 .and_then(|list| list.string())
125 .unwrap_or_default()
126 }
127
128 #[test]
129 fn set_header_replaces_same_name() {
130 let headers = Embed::run(|| {
131 set_header("X-Test: a").expect("set_header failed");
132 set_header("X-Test: b").expect("set_header failed");
133 headers()
134 });
135
136 assert_eq!(headers, "X-Test: b");
137 }
138
139 #[test]
140 fn add_header_keeps_same_name() {
141 let headers = Embed::run(|| {
142 add_header("X-Test: a").expect("add_header failed");
143 add_header("X-Test: b").expect("add_header failed");
144 headers()
145 });
146
147 assert_eq!(headers, "X-Test: a|X-Test: b");
148 }
149
150 #[test]
151 fn remove_header_drops_name() {
152 let headers = Embed::run(|| {
153 set_header("X-Test-Keep: a").expect("set_header failed");
154 set_header("X-Test-Drop: b").expect("set_header failed");
155 remove_header("X-Test-Drop").expect("remove_header failed");
156 headers()
157 });
158
159 assert_eq!(headers, "X-Test-Keep: a");
160 }
161
162 #[test]
163 fn remove_all_headers_empties_list() {
164 let headers = Embed::run(|| {
165 set_header("X-Test: a").expect("set_header failed");
166 remove_all_headers().expect("remove_all_headers failed");
167 headers()
168 });
169
170 assert_eq!(headers, "");
171 }
172
173 #[test]
174 fn set_response_code_sets_status() {
175 let code = Embed::run(|| {
176 set_response_code(404).expect("set_response_code failed");
177 Embed::eval("http_response_code();")
178 .ok()
179 .and_then(|code| code.long())
180 });
181
182 assert_eq!(code, Some(404));
183 }
184
185 #[test]
186 fn header_with_new_line_is_refused() {
187 let refused = Embed::run(|| {
188 matches!(
189 set_header("X-Test: a\r\nX-Injected: b"),
190 Err(Error::ResponseHeaderFailed)
191 )
192 });
193
194 assert!(refused);
195 }
196
197 #[test]
198 fn empty_header_is_refused() {
199 let refused = Embed::run(|| matches!(set_header(""), Err(Error::ResponseHeaderFailed)));
200
201 assert!(refused);
202 }
203
204 #[test]
205 fn alive_sapi_globals_guard_refuses_change() {
206 let refused = Embed::run(|| {
207 let _guard = SapiGlobals::get();
208 matches!(set_header("X-Test: a"), Err(Error::ResponseHeaderFailed))
209 });
210
211 assert!(refused);
212 }
213}