use std::{ffi::c_void, mem, ptr};
use crate::{
error::{Error, Result},
ffi::{
ZEND_RESULT_CODE_SUCCESS, ext_php_rs_output_activated, sapi_header_line, sapi_header_op,
sapi_header_op_enum, sapi_header_op_enum_SAPI_HEADER_ADD,
sapi_header_op_enum_SAPI_HEADER_DELETE, sapi_header_op_enum_SAPI_HEADER_DELETE_ALL,
sapi_header_op_enum_SAPI_HEADER_REPLACE, sapi_header_op_enum_SAPI_HEADER_SET_STATUS,
},
zend::globals::lock,
};
pub fn set_header(header: &str) -> Result<()> {
line_op(sapi_header_op_enum_SAPI_HEADER_REPLACE, header)
}
pub fn add_header(header: &str) -> Result<()> {
line_op(sapi_header_op_enum_SAPI_HEADER_ADD, header)
}
pub fn remove_header(name: &str) -> Result<()> {
line_op(sapi_header_op_enum_SAPI_HEADER_DELETE, name)
}
pub fn remove_all_headers() -> Result<()> {
header_op(sapi_header_op_enum_SAPI_HEADER_DELETE_ALL, ptr::null_mut())
}
pub fn set_response_code(code: u16) -> Result<()> {
header_op(
sapi_header_op_enum_SAPI_HEADER_SET_STATUS,
ptr::without_provenance_mut(usize::from(code)),
)
}
fn line_op(op: sapi_header_op_enum, line: &str) -> Result<()> {
let mut header_line: sapi_header_line = unsafe { mem::zeroed() };
header_line.line = line.as_ptr().cast();
header_line.line_len = line.len();
header_op(op, (&raw mut header_line).cast())
}
fn header_op(op: sapi_header_op_enum, arg: *mut c_void) -> Result<()> {
if !unsafe { ext_php_rs_output_activated() } || sapi_globals_borrowed() {
return Err(Error::ResponseHeaderFailed);
}
if unsafe { sapi_header_op(op, arg) } == ZEND_RESULT_CODE_SUCCESS {
Ok(())
} else {
Err(Error::ResponseHeaderFailed)
}
}
fn sapi_globals_borrowed() -> bool {
cfg_if::cfg_if! {
if #[cfg(php_zts)] {
lock::SAPI_GLOBALS_LOCK.with(|lock| lock.is_locked())
} else {
lock::SAPI_GLOBALS_LOCK.is_locked()
}
}
}
#[cfg(all(test, feature = "embed"))]
mod tests {
use super::*;
use crate::{embed::Embed, zend::SapiGlobals};
fn headers() -> String {
Embed::eval("implode('|', preg_grep('/^X-Test/', headers_list()));")
.ok()
.and_then(|list| list.string())
.unwrap_or_default()
}
#[test]
fn set_header_replaces_same_name() {
let headers = Embed::run(|| {
set_header("X-Test: a").expect("set_header failed");
set_header("X-Test: b").expect("set_header failed");
headers()
});
assert_eq!(headers, "X-Test: b");
}
#[test]
fn add_header_keeps_same_name() {
let headers = Embed::run(|| {
add_header("X-Test: a").expect("add_header failed");
add_header("X-Test: b").expect("add_header failed");
headers()
});
assert_eq!(headers, "X-Test: a|X-Test: b");
}
#[test]
fn remove_header_drops_name() {
let headers = Embed::run(|| {
set_header("X-Test-Keep: a").expect("set_header failed");
set_header("X-Test-Drop: b").expect("set_header failed");
remove_header("X-Test-Drop").expect("remove_header failed");
headers()
});
assert_eq!(headers, "X-Test-Keep: a");
}
#[test]
fn remove_all_headers_empties_list() {
let headers = Embed::run(|| {
set_header("X-Test: a").expect("set_header failed");
remove_all_headers().expect("remove_all_headers failed");
headers()
});
assert_eq!(headers, "");
}
#[test]
fn set_response_code_sets_status() {
let code = Embed::run(|| {
set_response_code(404).expect("set_response_code failed");
Embed::eval("http_response_code();")
.ok()
.and_then(|code| code.long())
});
assert_eq!(code, Some(404));
}
#[test]
fn header_with_new_line_is_refused() {
let refused = Embed::run(|| {
matches!(
set_header("X-Test: a\r\nX-Injected: b"),
Err(Error::ResponseHeaderFailed)
)
});
assert!(refused);
}
#[test]
fn empty_header_is_refused() {
let refused = Embed::run(|| matches!(set_header(""), Err(Error::ResponseHeaderFailed)));
assert!(refused);
}
#[test]
fn alive_sapi_globals_guard_refuses_change() {
let refused = Embed::run(|| {
let _guard = SapiGlobals::get();
matches!(set_header("X-Test: a"), Err(Error::ResponseHeaderFailed))
});
assert!(refused);
}
}