use std::collections::HashMap;
use std::ffi::{CString, NulError, c_char, c_int};
use std::sync::atomic::Ordering;
use std::sync::{Arc, Mutex};
use claviron_http::ext::ConnCtx;
use claviron_php::PhpRuntime;
use ext_php_rs::zend::SapiGlobals;
use http::header::CONTENT_TYPE;
use http::request::Parts;
use http::{HeaderName, HeaderValue, StatusCode, Version};
use tokio_util::sync::CancellationToken;
use super::body::request::RequestBodySource;
use super::body::response::{BodySink, CapturedResponse};
use super::{PhpSapiRequest, SAPI_SHUTTING_DOWN};
use crate::sapi::{ServerContext, php_handle_auth_data, php_output_end_all};
pub(crate) struct PhpRequestContext {
pub(crate) runtime: Arc<PhpRuntime>,
pub(crate) sapi_req: PhpSapiRequest,
pub(crate) parts: Parts,
pub(crate) conn_ctx: Option<Arc<ConnCtx>>,
pub(crate) request_body: RequestBodySource,
pub(crate) output: Arc<Mutex<CapturedResponse>>,
pub(crate) body_sink: BodySink,
environment: Option<RequestEnvironment>,
}
#[derive(Clone)]
pub(crate) struct RequestEnvironment(
pub claviron_runtime::congif::dotenw::Snapshot,
);
impl RequestEnvironment {
fn values<'a>(
snapshot: &'a Option<Self>,
runtime: &'a PhpRuntime,
) -> &'a HashMap<String, String> {
snapshot.as_ref().map_or(runtime.environment.as_ref(), |environment| {
environment.0.as_ref()
})
}
}
impl PhpRequestContext {
fn environment(&self) -> &HashMap<String, String> {
RequestEnvironment::values(&self.environment, &self.runtime)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum PhpWorkUnit {
Request,
Event,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ProgramModel {
Console,
Evaluation,
#[cfg_attr(not(feature = "tasks"), allow(dead_code))]
Task,
Application,
}
impl ProgramModel {
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Console => "console",
Self::Evaluation => "evaluation",
Self::Task => "task",
Self::Application => "application",
}
}
pub(crate) fn receives_work(self) -> bool {
matches!(self, Self::Application)
}
pub(crate) fn uses_console_output(self) -> bool {
matches!(self, Self::Task | Self::Console)
}
}
pub(crate) struct PhpProgramContext {
pub(crate) model: ProgramModel,
pub(crate) name: String,
pub(crate) instance: u8,
pub(crate) script: std::path::PathBuf,
pub(crate) environment: std::collections::BTreeMap<String, String>,
pub(crate) cancellation: CancellationToken,
pub(crate) current: Option<PhpWorkUnit>,
#[cfg(feature = "tasks")]
pub(crate) output: Option<claviron_tasks::TaskOutput>,
}
#[allow(clippy::large_enum_variant)]
pub(crate) enum PhpExecution {
Request(PhpRequestContext),
Program(PhpProgramContext),
}
pub(crate) struct ClavironContext {
pub(crate) execution: PhpExecution,
pub(crate) event_subscription:
Mutex<Option<crate::event::EventSubscription>>,
#[cfg(feature = "tasks")]
pub(crate) invocation_subscription:
Mutex<Option<crate::functions::orbit::invoke::InvocationSubscriptions>>,
pub(crate) env_overlay: Mutex<HashMap<String, String>>,
pub(crate) request_finished: bool,
}
impl ClavironContext {
pub(super) fn new(
runtime: Arc<PhpRuntime>,
sapi_req: PhpSapiRequest,
parts: Parts,
conn_ctx: Option<Arc<ConnCtx>>,
request_body: RequestBodySource,
output: Arc<Mutex<CapturedResponse>>,
body_sink: BodySink,
) -> Self {
let environment = parts.extensions.get::<RequestEnvironment>().cloned();
Self {
execution: PhpExecution::Request(PhpRequestContext {
runtime,
sapi_req,
parts,
conn_ctx,
request_body,
output,
body_sink,
environment,
}),
event_subscription: Mutex::new(None),
#[cfg(feature = "tasks")]
invocation_subscription: Mutex::new(None),
env_overlay: Mutex::new(HashMap::new()),
request_finished: false,
}
}
pub(crate) fn environment(
&self
) -> impl Iterator<Item = (&String, &String)> {
let (request, program) = match &self.execution {
PhpExecution::Request(request) => {
(Some(request.environment().iter()), None)
}
PhpExecution::Program(program) => {
(None, Some(program.environment.iter()))
}
};
request.into_iter().flatten().chain(program.into_iter().flatten())
}
pub(crate) fn environment_value(
&self,
name: &str,
) -> Option<&String> {
match &self.execution {
PhpExecution::Request(request) => request.environment().get(name),
PhpExecution::Program(program) => program.environment.get(name),
}
}
pub(crate) fn program(&self) -> Option<&PhpProgramContext> {
match &self.execution {
PhpExecution::Request(_) => None,
PhpExecution::Program(program) => Some(program),
}
}
pub(crate) fn program_mut(&mut self) -> Option<&mut PhpProgramContext> {
match &mut self.execution {
PhpExecution::Request(_) => None,
PhpExecution::Program(program) => Some(program),
}
}
pub(crate) fn is_on_http(&self) -> bool {
match &self.execution {
PhpExecution::Request(_) => true,
PhpExecution::Program(program) => {
matches!(program.current, Some(PhpWorkUnit::Request))
}
}
}
pub(crate) fn is_cancelled(&self) -> bool {
match &self.execution {
PhpExecution::Request(_) => {
SAPI_SHUTTING_DOWN.load(Ordering::Acquire)
}
PhpExecution::Program(program) => {
program.cancellation.is_cancelled()
}
}
}
pub(crate) fn append_output(
&mut self,
data: &[u8],
) -> bool {
if self.request_finished {
return false;
}
let request = match &self.execution {
PhpExecution::Request(request) => request,
PhpExecution::Program(program) => {
if program.model.uses_console_output() {
return self
.write_program_output(false, data)
.unwrap_or(true);
}
return true;
}
};
match &request.body_sink {
BodySink::Streaming(sink) => {
sink.write(&request.output, data)
}
}
}
pub(crate) fn php_flush(&mut self) -> bool {
if !self.flush_output() {
return false;
}
match &self.execution {
PhpExecution::Request(request) => match &request.body_sink {
BodySink::Streaming(sink) => sink.flush(&request.output),
},
PhpExecution::Program(_) => true,
}
}
pub(crate) fn append_header(
&mut self,
name: &str,
value: &str,
) {
let Some(output) = self.response_output() else {
return;
};
let Ok(header_name) = HeaderName::try_from(name.to_ascii_lowercase())
else {
return;
};
let Ok(header_value) = HeaderValue::from_str(value) else {
return;
};
if let Ok(mut output) = output.lock() {
output.headers.append(header_name, header_value);
}
}
pub(crate) fn set_status_from_sapi(&mut self) {
let code = SapiGlobals::get().sapi_headers().http_response_code;
let Ok(code) = u16::try_from(code) else {
return;
};
let Ok(status) = StatusCode::from_u16(code) else {
return;
};
if let Some(output) = self.response_output()
&& let Ok(mut output) = output.lock()
{
output.status = status;
}
}
pub(crate) fn flush_output(&mut self) -> bool {
match &self.execution {
PhpExecution::Program(program)
if program.model.uses_console_output() =>
{
self.flush_program_output(false).unwrap_or(true)
}
PhpExecution::Program(_) => true,
PhpExecution::Request(_) => {
self.set_status_from_sapi();
true
}
}
}
pub(crate) fn response_output(
&self
) -> Option<&Arc<Mutex<CapturedResponse>>> {
match &self.execution {
PhpExecution::Request(request) => Some(&request.output),
PhpExecution::Program(_) => None,
}
}
pub(crate) fn application_log_scope(&self) -> Option<(&str, u8)> {
match &self.execution {
PhpExecution::Program(program)
if program.model == ProgramModel::Application =>
{
Some((&program.name, program.instance))
}
PhpExecution::Program(_) | PhpExecution::Request(_) => None,
}
}
pub(crate) fn write_program_output(
&self,
error: bool,
bytes: &[u8],
) -> Option<bool> {
let PhpExecution::Program(program) = &self.execution else {
return None;
};
if !program.model.uses_console_output() {
return None;
}
#[cfg(feature = "tasks")]
if let Some(output) = &program.output {
return Some(if error {
output.write_stderr(bytes)
} else {
output.write_stdout(bytes)
});
}
use std::io::Write;
Some(if error {
std::io::stderr().lock().write_all(bytes).is_ok()
} else {
std::io::stdout().lock().write_all(bytes).is_ok()
})
}
pub(crate) fn flush_program_output(
&self,
error: bool,
) -> Option<bool> {
let PhpExecution::Program(program) = &self.execution else {
return None;
};
if !program.model.uses_console_output() {
return None;
}
#[cfg(feature = "tasks")]
if let Some(output) = &program.output {
return Some(if error {
output.flush_stderr()
} else {
output.flush_stdout()
});
}
use std::io::Write;
Some(if error {
std::io::stderr().lock().flush().is_ok()
} else {
std::io::stdout().lock().flush().is_ok()
})
}
pub(crate) fn cookie_header(&self) -> Option<String> {
match &self.execution {
PhpExecution::Request(request) => {
claviron_http::resolvers::join_cookie_header(
&request.parts.headers,
)
}
PhpExecution::Program(_) => None,
}
}
}
impl ServerContext for ClavironContext {
fn request_time(&self) -> Option<std::time::SystemTime> {
match &self.execution {
PhpExecution::Request(request) => request
.parts
.extensions
.get::<claviron_http::ext::ReceivedAt>()
.map(|at| at.0),
PhpExecution::Program(_) => None,
}
}
fn init_sapi_globals(&mut self) -> Result<(), NulError> {
let mut sapi_globals = SapiGlobals::get_mut();
if let PhpExecution::Program(program) = &self.execution {
let script = &program.script;
sapi_globals.sapi_headers.http_response_code =
StatusCode::OK.as_u16() as c_int;
sapi_globals.request_info.request_method = std::ptr::null();
sapi_globals.request_info.query_string = std::ptr::null_mut();
sapi_globals.request_info.path_translated =
CString::new(script.as_os_str().as_encoded_bytes())?.into_raw();
sapi_globals.request_info.request_uri = std::ptr::null_mut();
sapi_globals.request_info.content_length = 0;
sapi_globals.request_info.content_type = std::ptr::null();
sapi_globals.request_info.proto_num = 0;
return Ok(());
}
let PhpExecution::Request(request) = &self.execution else {
unreachable!()
};
sapi_globals.sapi_headers.http_response_code =
StatusCode::OK.as_u16() as c_int;
sapi_globals.request_info.request_method =
CString::new(request.parts.method.as_str())?
.into_raw()
.cast_const();
sapi_globals.request_info.query_string =
if request.sapi_req.query_string.is_empty() {
std::ptr::null_mut()
} else {
CString::new(request.sapi_req.query_string.as_str())?.into_raw()
};
sapi_globals.request_info.path_translated = CString::new(
request.sapi_req.script_file.to_string_lossy().as_bytes(),
)?
.into_raw();
sapi_globals.request_info.request_uri =
CString::new(request.sapi_req.request_uri.as_str())?.into_raw();
sapi_globals.request_info.content_length =
request.request_body.content_length(&request.parts.headers);
sapi_globals.request_info.content_type = request
.parts
.headers
.get(CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.map(CString::new)
.transpose()?
.map(|value| value.into_raw().cast_const())
.unwrap_or_else(std::ptr::null);
sapi_globals.request_info.proto_num = match request.parts.version {
Version::HTTP_09 => 900,
Version::HTTP_10 => 1000,
Version::HTTP_11 => 1100,
Version::HTTP_2 => 2000,
Version::HTTP_3 => 3000,
_ => 1100,
};
if let Some(auth) = request.parts.headers.get("Authorization")
&& let Ok(auth) = CString::new(auth.as_bytes())
{
unsafe {
php_handle_auth_data(auth.as_ptr());
}
}
Ok(())
}
fn read_post(
&mut self,
buffer: *mut c_char,
to_read: usize,
) -> usize {
match &mut self.execution {
PhpExecution::Request(request) => {
request.request_body.read_post(buffer, to_read)
}
PhpExecution::Program { .. } => 0,
}
}
fn is_request_finished(&self) -> bool {
self.request_finished
}
fn finish_request(&mut self) -> bool {
if self.request_finished {
return false;
}
self.request_finished = true;
unsafe {
php_output_end_all();
}
let flushed = self.flush_output();
if let PhpExecution::Request(request) = &self.execution
&& let BodySink::Streaming(sink) = &request.body_sink
{
sink.finish(&request.output);
}
flushed
}
}
#[cfg(test)]
#[path = "../../tests/engine/context.rs"]
mod tests;