Handle FastCGI situations where PATH_INFO is not available.
[hoplite.git] / http / front_controller.php
1 <?php
2 // Hoplite
3 // Copyright (c) 2015 Blue Static
4 //
5 // This program is free software: you can redistribute it and/or modify it
6 // under the terms of the GNU General Public License as published by the Free
7 // Software Foundation, either version 3 of the License, or any later version.
8 //
9 // This program is distributed in the hope that it will be useful, but WITHOUT
10 // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12 // more details.
13 //
14 // You should have received a copy of the GNU General Public License along with
15 // this program. If not, see <http://www.gnu.org/licenses/>.
16
17 namespace hoplite\http;
18
19 require_once HOPLITE_ROOT . '/http/interceptor.php';
20 require_once HOPLITE_ROOT . '/http/request.php';
21 require_once HOPLITE_ROOT . '/http/response.php';
22 require_once HOPLITE_ROOT . '/http/response_code.php';
23
24 /*!
25 The FrontController is meant to be invoked from the index.php of the
26 application.
27 */
28 class FrontController
29 {
30 /*! @var Request */
31 private $request = NULL;
32
33 /*! @var Response */
34 private $response = NULL;
35
36 /*! @var UrlMap */
37 private $url_map = NULL;
38
39 /*! @var OutputFilter */
40 private $output_filter = NULL;
41
42 /*! @var Array<Interceptor> */
43 private $interceptors = [];
44
45 /*!
46 Creates the controller with the request context information, typicallhy
47 from the global scope ($GLOBALS), but can be injected for testing.
48 @param array PHP globals array
49 */
50 public function __construct(array $globals)
51 {
52 $this->response = new Response();
53 $this->request = new Request();
54 $this->request->data = array(
55 '_GET' => &$globals['_GET'],
56 '_POST' => &$globals['_POST'],
57 '_COOKIE' => &$globals['_COOKIE'],
58 '_SERVER' => &$globals['_SERVER']
59 );
60 }
61
62 /*! Accessors */
63 public function request() { return $this->request; }
64 public function response() { return $this->response; }
65
66 /*! Sets the UrlMap. */
67 public function set_url_map(UrlMap $url_map) { $this->url_map = $url_map; }
68
69 /*! Sest the Output Filter. */
70 public function set_output_filter(OutputFilter $output_filter)
71 {
72 $this->output_filter = $output_filter;
73 }
74
75 /*! Registers an Interceptor that will be run before executing an Action. */
76 public function AddInterceptor(Interceptor $interceptor)
77 {
78 $this->interceptors[] = $interceptor;
79 }
80
81 /*!
82 Called in index.php to process the current HTTP request. This initializes the
83 Request object from its data and then routes the request to generate the
84 response.
85 */
86 public function ProcessRequest()
87 {
88 // The query rewriter module of the webserver rewrites a request from:
89 // http://example.com/webapp/user/view/42
90 // to:
91 // http://example.com/webapp/index.php/user/view/42
92 // ... which then becomes accessible from PATH_INFO.
93 $data = $this->request->data['_SERVER'];
94 if (isset($data['PATH_INFO']))
95 $url = $data['PATH_INFO'];
96 else
97 $url = str_replace($_SERVER['SCRIPT_NAME'], '', $_SERVER['DOCUMENT_URI']);
98
99 $this->request->data['HOPLITE_PATH_INFO'] = $url;
100
101 if ($url[0] == '/')
102 $url = substr($url, 1);
103
104 // Set the final pieces of the request.
105 $this->request->url = $url;
106 $this->request->http_method = $this->request->data['_SERVER']['REQUEST_METHOD'];
107
108 // Extract any PUT data as POST params.
109 if ($this->request->http_method == 'PUT')
110 parse_str(file_get_contents('php://input'), $this->request->data['_POST']);
111
112 // Register self as the active instance.
113 $GLOBALS[__CLASS__] = $this;
114
115 // Dispatch the request to an Action.
116 $this->RouteRequest($this->request);
117
118 // When control returns here, all actions have been invoked and it's time
119 // to start the output filter and exit.
120 $this->SendResponse();
121 }
122
123 /*!
124 Prevents any other Actions from executing. This starts the OutputFilter and
125 then exits.
126 */
127 public function SendResponse()
128 {
129 $this->output_filter->FilterOutput($this->request, $this->response);
130 $this->_Exit();
131 }
132
133 /*!
134 Sets the response code and stops the controller. Returns void.
135 */
136 public function SendResponseCode($code)
137 {
138 $this->response->response_code = $code;
139 $this->SendResponse();
140 }
141
142 /*!
143 Sets the response code to HTTP 302 FOUND and redirects the page to a new
144 location.
145 @param string The destination location of the redirect.
146 */
147 public function SendResponseRedirect($location)
148 {
149 $this->response->headers['Location'] = $location;
150 $this->SendResponseCode(ResponseCode::FOUND);
151 }
152
153 /*!
154 Wrapper around PHP exit().
155 */
156 protected function _Exit()
157 {
158 exit;
159 }
160
161 /*!
162 Given an Request object, this executes the action for the corresponding URL.
163 The action is located by performing a lookup in the UrlMap. Interceptors
164 are run before invoking the action.
165 @param Request The Request whose URL will be routed
166 */
167 public function RouteRequest(Request $request)
168 {
169 $url_map_value = $this->url_map->Evaluate($request);
170
171 $action = NULL;
172 if ($url_map_value)
173 $action = $this->url_map->LookupAction($url_map_value);
174
175 foreach ($this->interceptors as $interceptor)
176 $interceptor->DoIntercept($this, $action, $request, $this->response);
177
178 if ($action)
179 $this->InvokeAction($action);
180 }
181
182 /*!
183 Invokes the action with the Controller's request and response objects.
184 */
185 public function InvokeAction(Action $action)
186 {
187 $action->Invoke($this->request, $this->response);
188 }
189
190 /*!
191 Performs a reverse-lookup in the UrlMap for the pattern/fragment for the
192 name of a given Action class.
193 @param string Class name.
194 */
195 public function LookupAction($class)
196 {
197 $map = $this->url_map->map();
198 foreach ($map as $pattern => $action) {
199 if ($action == $class)
200 return $pattern;
201 }
202 }
203
204 /*!
205 Given a relative path, return an absolute path from the root controller.
206 @param string The relative path for which a URL will be created
207 @param bool Include the HTTP scheme and host to create an RFC URL if true;
208 if false an absolute path will be returned.
209 */
210 public function MakeURL($new_path, $url = FALSE)
211 {
212 // Detect the common paths between the REQUEST_URI and the PATH_INFO. That
213 // common piece will be the path to the root controller.
214 $request_uri = $this->request()->data['_SERVER']['REQUEST_URI'];
215 $path_info = $this->request()->data['HOPLITE_PATH_INFO'];
216 if (!$path_info)
217 $common_uri = substr($request_uri, 0, -1);
218 else
219 $common_uri = strstr($request_uri, $path_info, TRUE);
220
221 // If just constructing an absolute path, return that now.
222 if (!$url)
223 return $common_uri . $new_path;
224
225 // Otherwise, build the host part.
226 $url = 'http';
227 if (isset($this->request()->data['_SERVER']['HTTPS']) &&
228 $this->request()->data['_SERVER']['HTTPS'] == 'on') {
229 $url .= 's';
230 }
231 $url .= '://' . $this->request()->data['_SERVER']['HTTP_HOST'];
232
233 $port = $this->request()->data['_SERVER']['SERVER_PORT'];
234 if ($port != 80 && $port != 443)
235 $url .= ':' . $port;
236
237 $url .= $common_uri;
238 return $url . $new_path;
239 }
240 }