ripht_php_sapi/sapi/
executor.rs1use 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#[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
38pub 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 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 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 exit_status = ffi::ripht_php_sapi_exit_status();
181 let success = exec_result != ffi::FAILURE;
182 hooks.on_script_executed(success);
183
184 hooks.on_request_finishing();
185
186 #[cfg(feature = "tracing")]
187 trace!("Shutting down request");
188
189 ffi::sapi_globals.post_read = 1;
190 ffi::php_request_shutdown(std::ptr::null_mut());
191 ffi::sapi_globals.server_context = std::ptr::null_mut();
192
193 let mut server_ctx = Box::from_raw(ctx_ptr);
194
195 Self::cleanup_globals();
197
198 let headers: Vec<ResponseHeader> =
199 if TypeId::of::<H>() == TypeId::of::<NoOpHooks>() {
200 std::mem::take(&mut server_ctx.response_headers)
201 } else {
202 server_ctx
203 .response_headers
204 .iter()
205 .filter(|h| hooks.on_header(h.name(), h.value()))
206 .cloned()
207 .collect()
208 };
209
210 let status = server_ctx.status_code();
211 hooks.on_status(status);
212
213 for message in &server_ctx.messages {
214 hooks.on_php_message(message);
215 }
216
217 let body = match hooks.on_output(&server_ctx.output_buffer) {
218 OutputAction::Continue => server_ctx.output_buffer,
219 OutputAction::Done => Vec::new(),
220 };
221
222 #[cfg(feature = "tracing")]
223 debug!(
224 status = status,
225 body_len = body.len(),
226 headers_count = headers.len(),
227 messages_count = server_ctx.messages.len(),
228 "{}",
229 if success {
230 "Execution succeeded"
231 } else {
232 "Execution failed"
233 }
234 );
235
236 let result = ExecutionResult::new(
237 status,
238 exit_status,
239 body,
240 headers,
241 server_ctx.messages,
242 );
243
244 hooks.on_request_finished(&result);
245
246 Ok(result)
247 }
248 }
249
250 unsafe fn setup_globals(ctx: &ServerContext) {
252 ffi::sapi_globals
253 .request_info
254 .request_method = ctx.request_method_ptr();
255
256 ffi::sapi_globals
257 .request_info
258 .content_type = ctx.content_type_ptr();
259
260 ffi::sapi_globals
261 .request_info
262 .content_length = ctx.post_data.len() as i64;
263
264 ffi::sapi_globals
265 .request_info
266 .query_string = ctx.query_string_ptr();
267
268 ffi::sapi_globals
269 .sapi_headers
270 .http_response_code = 200;
271 }
272
273 unsafe fn run_script(script_cstr: &CString) -> i32 {
275 let mut file_handle = ffi::zend_file_handle::default();
276 ffi::zend_stream_init_filename(&mut file_handle, script_cstr.as_ptr());
277 file_handle.primary_script = 1;
278
279 let exec_result = ffi::php_execute_script(&mut file_handle);
280 ffi::zend_destroy_file_handle(&mut file_handle);
281 exec_result
282 }
283
284 unsafe fn cleanup_globals() {
286 ffi::sapi_globals.server_context = std::ptr::null_mut();
287
288 ffi::sapi_globals
289 .request_info
290 .content_type = std::ptr::null();
291
292 ffi::sapi_globals
293 .request_info
294 .query_string = std::ptr::null_mut();
295
296 ffi::sapi_globals
297 .request_info
298 .cookie_data = std::ptr::null_mut();
299 }
300
301 unsafe fn apply_ini_overrides(ctx: &ServerContext) {
303 if ctx.ini_overrides.is_empty() {
304 return;
305 }
306
307 let init = ffi::zend_string_init_interned.expect("PHP not initialized");
308
309 for (key, value) in &ctx.ini_overrides {
310 let name = init(key.as_ptr(), key.as_bytes().len(), true);
313 if name.is_null() {
314 continue;
315 }
316
317 ffi::zend_alter_ini_entry_chars(
322 name,
323 value.as_ptr(),
324 value.as_bytes().len(),
325 ffi::ZEND_INI_USER | ffi::ZEND_INI_SYSTEM,
326 ffi::ZEND_INI_STAGE_RUNTIME,
327 );
328 }
329 }
330}