ripht_php_sapi/sapi/
executor.rs

1//! Request execution engine.
2//!
3//! Manages the PHP request lifecycle: startup, script execution, and shutdown.
4
5use std::any::TypeId;
6use std::ffi::CString;
7
8use thiserror::Error;
9
10#[cfg(feature = "tracing")]
11use tracing::{debug, error, trace};
12
13use super::ffi;
14use super::server_context::ServerContext;
15use super::SapiError;
16use crate::execution::{
17    ExecutionContext, ExecutionHooks, ExecutionResult, NoOpHooks, OutputAction,
18    ResponseHeader,
19};
20
21/// Errors that can occur during PHP script execution.
22#[derive(Debug, Error)]
23#[non_exhaustive]
24pub enum ExecutionError {
25    #[error("Invalid path: {0}")]
26    InvalidPath(String),
27
28    #[error("Script not found: {0}")]
29    ScriptNotFound(std::path::PathBuf),
30
31    #[error("PHP engine not initialized")]
32    NotInitialized,
33
34    #[error("Request startup failed")]
35    StartupFailed,
36}
37
38/// Executes PHP scripts within an initialized SAPI.
39pub struct Executor<'sapi> {
40    sapi: &'sapi super::RiphtSapi,
41}
42
43impl<'sapi> Executor<'sapi> {
44    pub(super) fn new(
45        sapi: &'sapi super::RiphtSapi,
46    ) -> Result<Self, SapiError> {
47        if !sapi.is_initialized() {
48            return Err(SapiError::NotInitialized);
49        }
50
51        Ok(Self { sapi })
52    }
53
54    pub fn execute(
55        &self,
56        ctx: ExecutionContext,
57    ) -> Result<ExecutionResult, ExecutionError> {
58        self.execute_with_hooks(ctx, NoOpHooks)
59    }
60
61    pub fn execute_streaming<F>(
62        &self,
63        ctx: ExecutionContext,
64        on_output: F,
65    ) -> Result<ExecutionResult, ExecutionError>
66    where
67        F: FnMut(&[u8]) + 'static,
68    {
69        #[cfg(feature = "tracing")]
70        debug!(
71            script_path = %ctx.script_path.display(),
72            "Executing PHP (streaming)"
73        );
74
75        if !self.sapi.is_initialized() {
76            return Err(ExecutionError::NotInitialized);
77        }
78
79        if !ctx.script_path.exists() {
80            return Err(ExecutionError::ScriptNotFound(
81                ctx.script_path.clone(),
82            ));
83        }
84
85        let script_cstr = ctx.path_as_cstring()?;
86
87        let mut server_ctx = Box::<ServerContext>::from(ctx);
88        server_ctx.set_output_callback(on_output);
89
90        // SAFETY: Ownership transfer for request execution. ServerContext is boxed,
91        // stored in sapi_globals.server_context, then reclaimed after php_request_shutdown.
92        // All error paths clean up properly.
93        unsafe {
94            let ctx_ptr = Box::into_raw(server_ctx);
95            ffi::sapi_globals.server_context = ctx_ptr as *mut std::ffi::c_void;
96            Self::setup_globals(&*ctx_ptr);
97
98            if ffi::php_request_startup() == ffi::FAILURE {
99                let _ = Box::from_raw(ctx_ptr);
100                ffi::php_request_shutdown(std::ptr::null_mut());
101                ffi::sapi_globals.server_context = std::ptr::null_mut();
102                return Err(ExecutionError::StartupFailed);
103            }
104
105            Self::apply_ini_overrides(&*ctx_ptr);
106            Self::run_script(&script_cstr);
107
108            ffi::sapi_globals.post_read = 1;
109            ffi::php_request_shutdown(std::ptr::null_mut());
110            ffi::sapi_globals.server_context = std::ptr::null_mut();
111
112            let server_ctx = Box::from_raw(ctx_ptr);
113            Self::cleanup_globals();
114
115            Ok((*server_ctx).into_result(Vec::new()))
116        }
117    }
118
119    pub fn execute_with_hooks<H: ExecutionHooks + 'static>(
120        &self,
121        ctx: ExecutionContext,
122        mut hooks: H,
123    ) -> Result<ExecutionResult, ExecutionError> {
124        #[cfg(feature = "tracing")]
125        debug!(
126            script_path = %ctx.script_path.display(),
127            "Executing PHP"
128        );
129
130        if !self.sapi.is_initialized() {
131            #[cfg(feature = "tracing")]
132            error!("Execute before init");
133            return Err(ExecutionError::NotInitialized);
134        }
135
136        if !ctx.script_path.exists() {
137            return Err(ExecutionError::ScriptNotFound(
138                ctx.script_path.clone(),
139            ));
140        }
141
142        let script_cstr = ctx.path_as_cstring()?;
143        let script_path = ctx.script_path.clone();
144
145        hooks.on_context_created();
146
147        let server_ctx = Box::<ServerContext>::from(ctx);
148
149        // SAFETY: Same ownership transfer pattern as execute_streaming.
150        unsafe {
151            let ctx_ptr = Box::into_raw(server_ctx);
152            ffi::sapi_globals.server_context = ctx_ptr as *mut std::ffi::c_void;
153            Self::setup_globals(&*ctx_ptr);
154
155            hooks.on_request_starting();
156
157            #[cfg(feature = "tracing")]
158            trace!("Starting PHP request");
159
160            let startup_result = ffi::php_request_startup();
161
162            if startup_result == ffi::FAILURE {
163                #[cfg(feature = "tracing")]
164                error!("Request startup failed");
165                let _ = Box::from_raw(ctx_ptr);
166                ffi::php_request_shutdown(std::ptr::null_mut());
167                ffi::sapi_globals.server_context = std::ptr::null_mut();
168                return Err(ExecutionError::StartupFailed);
169            }
170
171            Self::apply_ini_overrides(&*ctx_ptr);
172
173            hooks.on_request_started();
174            hooks.on_script_executing(&script_path);
175
176            #[cfg(feature = "tracing")]
177            trace!("Executing script");
178
179            let exec_result = Self::run_script(&script_cstr);
180            let success = exec_result != ffi::FAILURE;
181            hooks.on_script_executed(success);
182
183            hooks.on_request_finishing();
184
185            #[cfg(feature = "tracing")]
186            trace!("Shutting down request");
187
188            ffi::sapi_globals.post_read = 1;
189            ffi::php_request_shutdown(std::ptr::null_mut());
190            ffi::sapi_globals.server_context = std::ptr::null_mut();
191
192            let mut server_ctx = Box::from_raw(ctx_ptr);
193
194            // SAFETY: Defensive cleanup of request-related pointers.
195            Self::cleanup_globals();
196
197            let headers: Vec<ResponseHeader> =
198                if TypeId::of::<H>() == TypeId::of::<NoOpHooks>() {
199                    std::mem::take(&mut server_ctx.response_headers)
200                } else {
201                    server_ctx
202                        .response_headers
203                        .iter()
204                        .filter(|h| hooks.on_header(h.name(), h.value()))
205                        .cloned()
206                        .collect()
207                };
208
209            let status = server_ctx.status_code();
210            hooks.on_status(status);
211
212            for message in &server_ctx.messages {
213                hooks.on_php_message(message);
214            }
215
216            let body = match hooks.on_output(&server_ctx.output_buffer) {
217                OutputAction::Continue => server_ctx.output_buffer,
218                OutputAction::Done => Vec::new(),
219            };
220
221            #[cfg(feature = "tracing")]
222            debug!(
223                status = status,
224                body_len = body.len(),
225                headers_count = headers.len(),
226                messages_count = server_ctx.messages.len(),
227                "{}",
228                if success {
229                    "Execution succeeded"
230                } else {
231                    "Execution failed"
232                }
233            );
234
235            let result = ExecutionResult::new(
236                status,
237                body,
238                headers,
239                server_ctx.messages,
240            );
241
242            hooks.on_request_finished(&result);
243
244            Ok(result)
245        }
246    }
247
248    /// Populates `sapi_globals.request_info` from the server context.
249    unsafe fn setup_globals(ctx: &ServerContext) {
250        ffi::sapi_globals
251            .request_info
252            .request_method = ctx.request_method_ptr();
253
254        ffi::sapi_globals
255            .request_info
256            .content_type = ctx.content_type_ptr();
257
258        ffi::sapi_globals
259            .request_info
260            .content_length = ctx.post_data.len() as i64;
261
262        ffi::sapi_globals
263            .request_info
264            .query_string = ctx.query_string_ptr();
265
266        ffi::sapi_globals
267            .sapi_headers
268            .http_response_code = 200;
269    }
270
271    /// Runs the PHP script via `php_execute_script`.
272    unsafe fn run_script(script_cstr: &CString) -> i32 {
273        let mut file_handle = ffi::zend_file_handle::default();
274        ffi::zend_stream_init_filename(&mut file_handle, script_cstr.as_ptr());
275        file_handle.primary_script = 1;
276
277        let exec_result = ffi::php_execute_script(&mut file_handle);
278        ffi::zend_destroy_file_handle(&mut file_handle);
279        exec_result
280    }
281
282    /// Clears request-related pointers to prevent stale access between requests.
283    unsafe fn cleanup_globals() {
284        ffi::sapi_globals.server_context = std::ptr::null_mut();
285
286        ffi::sapi_globals
287            .request_info
288            .content_type = std::ptr::null();
289
290        ffi::sapi_globals
291            .request_info
292            .query_string = std::ptr::null_mut();
293
294        ffi::sapi_globals
295            .request_info
296            .cookie_data = std::ptr::null_mut();
297    }
298
299    /// Applies per-request INI overrides from the server context.
300    unsafe fn apply_ini_overrides(ctx: &ServerContext) {
301        if ctx.ini_overrides.is_empty() {
302            return;
303        }
304
305        let init = ffi::zend_string_init_interned.expect("PHP not initialized");
306
307        for (key, value) in &ctx.ini_overrides {
308            // SAFETY: Create an interned zend_string for the INI key.
309            // CString::as_ptr() returns a valid null-terminated string.
310            let name = init(key.as_ptr(), key.as_bytes().len(), true);
311            if name.is_null() {
312                continue;
313            }
314
315            // SAFETY: zend_alter_ini_entry_chars modifies PHP's INI settings.
316            // This is safe to call after php_request_startup().
317            // ZEND_INI_USER | ZEND_INI_SYSTEM allows changing most settings.
318            // ZEND_INI_STAGE_RUNTIME indicates we're in script execution.
319            ffi::zend_alter_ini_entry_chars(
320                name,
321                value.as_ptr(),
322                value.as_bytes().len(),
323                ffi::ZEND_INI_USER | ffi::ZEND_INI_SYSTEM,
324                ffi::ZEND_INI_STAGE_RUNTIME,
325            );
326        }
327    }
328}