use std::ffi::{CStr, NulError, c_char, c_int, c_void};
use std::sync::atomic::{AtomicPtr, Ordering};
use std::time::SystemTime;
use ext_php_rs::builders::SapiBuilder;
use ext_php_rs::embed::{Embed, EmbedError};
use ext_php_rs::types::Zval;
use ext_php_rs::zend::{ModuleEntry, SapiGlobals, SapiModule};
use thiserror::Error;
pub(crate) mod callbacks;
mod module;
#[cfg(test)]
#[path = "../tests/sapi/tests.rs"]
mod tests;
pub use callbacks::init_server_scoped_vars;
pub type ZendResultCode = c_int;
pub const ZEND_RESULT_CODE_SUCCESS: ZendResultCode = 0;
pub const ZEND_RESULT_CODE_FAILURE: ZendResultCode = -1;
static ADDITIONAL_MODULE: AtomicPtr<ModuleEntry> =
AtomicPtr::new(std::ptr::null_mut());
pub(crate) fn install_additional_module(
module: *mut ModuleEntry
) -> Result<(), String> {
if module.is_null() {
return Err("PHP additional module pointer is null".to_string());
}
match ADDITIONAL_MODULE.compare_exchange(
std::ptr::null_mut(),
module,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => Ok(()),
Err(existing) if existing == module => Ok(()),
Err(_) => Err("PHP additional module already installed".to_string()),
}
}
pub mod ffi {
use super::*;
unsafe extern "C" {
pub fn sapi_startup(sf: *mut SapiModule);
pub fn php_module_startup(
sapi: *mut SapiModule,
additional_modules: *mut c_void,
) -> c_int;
pub fn php_module_shutdown();
pub fn php_request_startup() -> c_int;
pub fn php_request_shutdown(dummy: *mut c_void);
pub fn zend_unset_timeout();
pub fn php_build_argv(
query: *const c_char,
vars: *mut Zval,
);
pub fn php_output_end_all();
pub fn php_handle_auth_data(auth: *const c_char) -> c_int;
pub fn php_register_variable_safe(
var: *const c_char,
str: *const c_char,
str_len: usize,
track_vars_array: *mut Zval,
);
pub fn zend_update_current_locale();
pub fn zend_eval_string(
str: *const c_char,
retval: *mut Zval,
string_name: *const c_char,
) -> c_int;
}
}
pub use ffi::{
php_handle_auth_data, php_output_end_all, zend_update_current_locale,
};
pub static PHP_SELF: &CStr = c"PHP_SELF";
pub static SERVER_ADDR: &CStr = c"SERVER_ADDR";
pub static SERVER_NAME: &CStr = c"SERVER_NAME";
pub static SERVER_SOFTWARE: &CStr = c"SERVER_SOFTWARE";
pub static SERVER_PROTOCOL: &CStr = c"SERVER_PROTOCOL";
pub static REQUEST_METHOD: &CStr = c"REQUEST_METHOD";
pub static QUERY_STRING: &CStr = c"QUERY_STRING";
pub static DOCUMENT_ROOT: &CStr = c"DOCUMENT_ROOT";
pub static HTTPS: &CStr = c"HTTPS";
pub static REMOTE_ADDR: &CStr = c"REMOTE_ADDR";
pub static REMOTE_PORT: &CStr = c"REMOTE_PORT";
pub static SCRIPT_FILENAME: &CStr = c"SCRIPT_FILENAME";
pub static SERVER_PORT: &CStr = c"SERVER_PORT";
pub static PATH_TRANSLATED: &CStr = c"PATH_TRANSLATED";
pub static SCRIPT_NAME: &CStr = c"SCRIPT_NAME";
pub static REQUEST_URI: &CStr = c"REQUEST_URI";
pub static PATH_INFO: &CStr = c"PATH_INFO";
pub static CLAVIRON_THREAD_ID: &CStr = c"CLAVIRON_THREAD_ID";
pub static HTTP_COOKIE: &CStr = c"HTTP_COOKIE";
#[derive(Debug, Error, PartialEq)]
pub enum ExecutePhpError {
#[error(transparent)]
InitSapiGlobalsError(#[from] NulError),
#[error("Request startup failed")]
RequestStartupFailed,
#[error("A bailout occurred during the execution")]
Bailout,
}
pub fn handle_abort_connection() {
const PHP_CONNECTION_ABORTED: core::ffi::c_short = 1;
unsafe {
let pg = ext_php_rs::ffi::ext_php_rs_process_globals();
if !pg.is_null() {
(*pg).connection_status = PHP_CONNECTION_ABORTED;
}
}
}
pub unsafe fn register_variable<V: AsRef<[u8]>>(
name: &CStr,
value: V,
vars: *mut Zval,
) {
let value = value.as_ref();
unsafe {
ffi::php_register_variable_safe(
name.as_ptr(),
value.as_ptr().cast(),
value.len(),
vars,
);
}
}
pub unsafe fn register_variable_cstr(
name: &CStr,
value: &CStr,
vars: *mut Zval,
) {
let value = value.to_bytes();
unsafe {
ffi::php_register_variable_safe(
name.as_ptr(),
value.as_ptr().cast(),
value.len(),
vars,
);
}
}
#[macro_export]
macro_rules! free_raw_cstring {
($struct:expr, $field:ident) => {
if !$struct.$field.is_null() {
unsafe {
drop(std::ffi::CString::from_raw($struct.$field.cast_mut()))
};
#[allow(unused_assignments)]
{
$struct.$field = std::ptr::null();
}
}
};
}
#[macro_export]
macro_rules! free_raw_cstring_mut {
($struct:expr, $field:ident) => {
if !$struct.$field.is_null() {
unsafe { drop(std::ffi::CString::from_raw($struct.$field)) };
#[allow(unused_assignments)]
{
$struct.$field = std::ptr::null_mut();
}
}
};
}
pub trait ServerContext: Sized {
#[must_use = "losing the pointer leaks memory"]
fn into_raw(self) -> *mut Self {
Box::into_raw(Box::new(self))
}
#[must_use = "drop the returned Box if you intend to drop the context"]
unsafe fn from_raw(ptr: *mut c_void) -> Box<Self> {
unsafe { Box::from_raw(ptr.cast()) }
}
fn from_server_context<'a>(server_context: *mut c_void) -> &'a mut Self {
let context = server_context.cast();
unsafe { &mut *context }
}
fn request_time(&self) -> Option<SystemTime> {
None
}
fn init_sapi_globals(&mut self) -> Result<(), NulError>;
fn read_post(
&mut self,
buffer: *mut c_char,
to_read: usize,
) -> usize;
fn is_request_finished(&self) -> bool;
fn finish_request(&mut self) -> bool;
}
pub fn run_request<C, P, F>(
context: C,
script: P,
bailout_handler: F,
) -> Result<(), ExecutePhpError>
where
C: ServerContext,
P: AsRef<std::path::Path> + std::fmt::Debug + std::panic::RefUnwindSafe,
F: Fn(EmbedError) -> bool + std::panic::RefUnwindSafe,
{
run_execution_with_startup(context, script, bailout_handler, || {})
}
pub(crate) fn run_execution_with_startup<C, P, F>(
context: C,
script: P,
bailout_handler: F,
started: impl FnOnce(),
) -> Result<(), ExecutePhpError>
where
C: ServerContext,
P: AsRef<std::path::Path> + std::fmt::Debug + std::panic::RefUnwindSafe,
F: Fn(EmbedError) -> bool + std::panic::RefUnwindSafe,
{
let failed = run_execution_scope(context, started, || {
match Embed::run_script(&script) {
Ok(()) => false,
Err(e) => bailout_handler(e),
}
})?;
if failed { Err(ExecutePhpError::Bailout) } else { Ok(()) }
}
pub(crate) fn run_execution_scope<C, T>(
mut context: C,
started: impl FnOnce(),
execute: impl FnOnce() -> T,
) -> Result<T, ExecutePhpError>
where
C: ServerContext,
{
#[cfg(feature = "mysql")]
crate::functions::mysql::begin_request();
#[cfg(feature = "sqlite")]
crate::functions::sqlite::begin_request();
#[cfg(feature = "mysql")]
let _mysql_scope = crate::functions::mysql::ExecutionCleanup;
#[cfg(feature = "sqlite")]
let _sqlite_scope = crate::functions::sqlite::ExecutionCleanup;
if let Err(err) = context.init_sapi_globals() {
return Err(ExecutePhpError::from(err));
}
SapiGlobals::get_mut().server_context = context.into_raw().cast();
if unsafe { ffi::php_request_startup() } == ZEND_RESULT_CODE_FAILURE {
return Err(ExecutePhpError::RequestStartupFailed);
}
unsafe { ffi::zend_unset_timeout() };
crate::intercept::pre_execute_work_hooks();
started();
let catch = ext_php_rs::zend::try_catch_first(
std::panic::AssertUnwindSafe(execute),
);
unsafe { ffi::php_request_shutdown(std::ptr::null_mut()) };
let mut request_info = SapiGlobals::get().request_info;
crate::free_raw_cstring_mut!(request_info, path_translated);
catch.map_err(|_| ExecutePhpError::Bailout)
}
pub fn build_sapi_module<C: ServerContext>(
name: &str,
pretty_name: &str,
log_message: extern "C" fn(*const c_char, c_int),
customise: impl FnOnce(SapiBuilder) -> SapiBuilder,
) -> ext_php_rs::error::Result<SapiModule> {
let mut module = customise(SapiBuilder::new(name, pretty_name)).build()?;
if module.startup.is_none() {
module.startup = Some(default_startup);
}
if module.shutdown.is_none() {
module.shutdown = Some(default_shutdown);
}
if module.deactivate.is_none() {
module.deactivate = Some(default_deactivate::<C>);
}
if module.read_post.is_none() {
module.read_post = Some(default_read_post::<C>);
}
if module.log_message.is_none() {
module.log_message = Some(log_message);
}
if module.get_request_time.is_none() {
module.get_request_time = Some(default_get_request_time::<C>);
}
Ok(module)
}
pub fn run_sapi_startup(module: *mut SapiModule) -> ZendResultCode {
unsafe {
let ini_entries = (*module).ini_entries;
ffi::sapi_startup(module);
(*module).ini_entries = ini_entries;
if let Some(startup) = (*module).startup {
return startup(module);
}
}
ZEND_RESULT_CODE_FAILURE
}
unsafe extern "C" fn default_startup(sapi: *mut SapiModule) -> ZendResultCode {
let additional_module = ADDITIONAL_MODULE.load(Ordering::Acquire);
unsafe { ffi::php_module_startup(sapi, additional_module.cast::<c_void>()) }
}
extern "C" fn default_shutdown(_sapi: *mut SapiModule) -> ZendResultCode {
unsafe { ffi::php_module_shutdown() };
ZEND_RESULT_CODE_SUCCESS
}
extern "C" fn default_deactivate<C: ServerContext>() -> ZendResultCode {
let sapi_globals = SapiGlobals::get();
if !sapi_globals.sapi_started {
return ZEND_RESULT_CODE_SUCCESS;
}
if sapi_globals.server_context.is_null() {
return ZEND_RESULT_CODE_SUCCESS;
}
let mut request_info = sapi_globals.request_info;
crate::free_raw_cstring!(request_info, request_method);
crate::free_raw_cstring_mut!(request_info, query_string);
crate::free_raw_cstring_mut!(request_info, request_uri);
crate::free_raw_cstring!(request_info, content_type);
crate::free_raw_cstring_mut!(request_info, cookie_data);
let mut context = unsafe { C::from_raw(sapi_globals.server_context) };
drop(sapi_globals);
if !context.is_request_finished() && !context.finish_request() {
handle_abort_connection();
}
SapiGlobals::get_mut().server_context = std::ptr::null_mut();
ZEND_RESULT_CODE_SUCCESS
}
extern "C" fn default_read_post<C: ServerContext>(
buffer: *mut c_char,
length: usize,
) -> usize {
let sapi_globals = SapiGlobals::get();
let content_length = sapi_globals.request_info().content_length();
if content_length <= 0 {
return C::from_server_context(sapi_globals.server_context)
.read_post(buffer, length);
}
if sapi_globals.read_post_bytes >= content_length {
return 0;
}
let to_read =
length.min((content_length - sapi_globals.read_post_bytes) as usize);
C::from_server_context(sapi_globals.server_context)
.read_post(buffer, to_read)
}
unsafe extern "C" fn default_get_request_time<C: ServerContext>(
time: *mut f64
) -> ZendResultCode {
let server_context = SapiGlobals::get().server_context;
let received = (!server_context.is_null())
.then(|| C::from_server_context(server_context).request_time())
.flatten();
let timestamp = received
.unwrap_or_else(SystemTime::now)
.duration_since(SystemTime::UNIX_EPOCH)
.expect("system time is before Unix epoch");
unsafe { time.write(timestamp.as_secs_f64()) };
ZEND_RESULT_CODE_SUCCESS
}