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 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 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 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 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 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 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 let name = init(key.as_ptr(), key.as_bytes().len(), true);
311 if name.is_null() {
312 continue;
313 }
314
315 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}