summaryrefslogtreecommitdiff
path: root/framework/Web/UI/ActiveControls/TActivePageAdapter.php
blob: cc32376a7c3f14bbce3967213fbdf218900f292b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
<?php
/**
 * TActivePageAdapter, TCallbackErrorHandler and TInvalidCallbackException class file.
 *
 * @author Wei Zhuo <weizhuo[at]gamil[dot]com>
 * @author Gabor Berczi <gabor.berczi@devworx.hu> (lazyload additions & progressive rendering)
 * @link http://www.pradosoft.com/
 * @copyright Copyright &copy; 2005-2014 PradoSoft
 * @license http://www.pradosoft.com/license/
 * @package System.Web.UI.ActiveControls
 */

/**
 * Load callback response adapter class.
 */
Prado::using('System.Web.UI.ActiveControls.TCallbackResponseAdapter');
Prado::using('System.Web.UI.ActiveControls.TCallbackClientScript');
Prado::using('System.Web.UI.ActiveControls.TCallbackEventParameter');

/**
 * TActivePageAdapter class.
 *
 * Callback request handler.
 *
 * @author Wei Zhuo <weizhuo[at]gamil[dot]com>
 * @author Gabor Berczi <gabor.berczi@devworx.hu> (lazyload additions & progressive rendering)
 * @package System.Web.UI.ActiveControls
 * @since 3.1
 */
class TActivePageAdapter extends TControlAdapter
{
	/**
	 * Callback response data header name.
	 */
	const CALLBACK_DATA_HEADER = 'X-PRADO-DATA';
	/**
	 * Callback response client-side action header name.
	 */
	const CALLBACK_ACTION_HEADER = 'X-PRADO-ACTIONS';
	/**
	 * Callback error header name.
	 */
	const CALLBACK_ERROR_HEADER = 'X-PRADO-ERROR';
	/**
	 * Callback page state header name.
	 */
	const CALLBACK_PAGESTATE_HEADER = 'X-PRADO-PAGESTATE';
	/**
	 * Script list header name.
	 */
	const CALLBACK_SCRIPTLIST_HEADER = 'X-PRADO-SCRIPTLIST';
	/**
	 * Stylesheet list header name.
	 */
	const CALLBACK_STYLESHEETLIST_HEADER = 'X-PRADO-STYLESHEETLIST';
	/**
	 * Stylesheet header name.
	 */
	const CALLBACK_STYLESHEET_HEADER = 'X-PRADO-STYLESHEET';
	/**
	 * Hidden field list header name.
	 */
	const CALLBACK_HIDDENFIELDLIST_HEADER = 'X-PRADO-HIDDENFIELDLIST';

	/**
	 * Callback redirect url header name.
	 */
	const CALLBACK_REDIRECT = 'X-PRADO-REDIRECT';

	/**
	 * @var ICallbackEventHandler callback event handler.
	 */
	private $_callbackEventTarget;
	/**
	 * @var mixed callback event parameter.
	 */
	private $_callbackEventParameter;
	/**
	 * @var TCallbackClientScript callback client script handler
	 */
	private $_callbackClient;

	private $_controlsToRender=array();

	/**
	 * Constructor, trap errors and exception to let the callback response
	 * handle them.
	 */
	public function __construct(TPage $control)
	{
		parent::__construct($control);

		//TODO: can this be done later?
		$response = $this->getApplication()->getResponse();
		$response->setAdapter(new TCallbackResponseAdapter($response));

		$this->trapCallbackErrorsExceptions();
	}

	/**
	 * Process the callback request.
	 * @param THtmlWriter html content writer.
	 */
	public function processCallbackEvent($writer)
	{
		Prado::trace("ActivePage raiseCallbackEvent()",'System.Web.UI.ActiveControls.TActivePageAdapter');
		$this->raiseCallbackEvent();
	}

	/**
	 * Register a control for defered render() call.
	 * @param TControl control for defered rendering
	 * @param THtmlWriter the renderer
	 */
	public function registerControlToRender($control,$writer)
	{
		$id = $control->getUniqueID();
		if(!isset($this->_controlsToRender[$id]))
			$this->_controlsToRender[$id] = array($control,$writer);
	}

	/**
	 * Trap errors and exceptions to be handled by TCallbackErrorHandler.
	 */
	protected function trapCallbackErrorsExceptions()
	{
		$this->getApplication()->setErrorHandler(new TCallbackErrorHandler);
	}

	/**
	 * Render the callback response.
	 * @param THtmlWriter html content writer.
	 */
	public function renderCallbackResponse($writer)
	{
		Prado::trace("ActivePage renderCallbackResponse()",'System.Web.UI.ActiveControls.TActivePageAdapter');
		if(($url = $this->getResponse()->getAdapter()->getRedirectedUrl())===null)
			$this->renderResponse($writer);
		else
			$this->redirect($url);
	}

	/**
	 * Redirect url on the client-side using javascript.
	 * @param string new url to load.
	 */
	protected function redirect($url)
	{
		Prado::trace("ActivePage redirect()",'System.Web.UI.ActiveControls.TActivePageAdapter');
		$this->appendContentPart($this->getResponse(), self::CALLBACK_REDIRECT, $url);
	}

	/**
	 * Renders the callback response by adding additional callback data and
	 * javascript actions in the header and page state if required.
	 * @param THtmlWriter html content writer.
	 */
	protected function renderResponse($writer)
	{
		Prado::trace("ActivePage renderResponse()",'System.Web.UI.ActiveControls.TActivePageAdapter');
		//renders all the defered render() calls.
		foreach($this->_controlsToRender as $rid => $forRender)
			$forRender[0]->render($forRender[1]);

		$response = $this->getResponse();

		//send response data in header
		if($response->getHasAdapter())
		{
			$responseData = $response->getAdapter()->getResponseData();
			if($responseData!==null)
			{
				$data = TJavaScript::jsonEncode($responseData);

				$this->appendContentPart($response, self::CALLBACK_DATA_HEADER, $data);
			}
		}

		//sends page state in header
		if(($handler = $this->getCallbackEventTarget()) !== null)
		{
			if($handler->getActiveControl()->getClientSide()->getEnablePageStateUpdate())
			{
				$pagestate = $this->getPage()->getClientState();
				$this->appendContentPart($response, self::CALLBACK_PAGESTATE_HEADER, $pagestate);
			}
		}

		//safari must receive at least 1 byte of data.
		$writer->write(" ");

		//output the end javascript
		if($this->getPage()->getClientScript()->hasEndScripts())
		{
			$writer = $response->createHtmlWriter();
			$this->getPage()->getClientScript()->renderEndScripts($writer);
			$this->getPage()->getCallbackClient()->evaluateScript($writer);
		}

		//output the actions
		$executeJavascript = $this->getCallbackClientHandler()->getClientFunctionsToExecute();
		$actions = TJavaScript::jsonEncode($executeJavascript);
		$this->appendContentPart($response, self::CALLBACK_ACTION_HEADER, $actions);


		$cs = $this->Page->getClientScript();

		// collect all stylesheet file references
		$stylesheets = $cs->getStyleSheetUrls();
		if (count($stylesheets)>0)
		$this->appendContentPart($response, self::CALLBACK_STYLESHEETLIST_HEADER, TJavaScript::jsonEncode($stylesheets));

		// collect all stylesheet snippets references
		$stylesheets = $cs->getStyleSheetCodes();
		if (count($stylesheets)>0)
		$this->appendContentPart($response, self::CALLBACK_STYLESHEET_HEADER, TJavaScript::jsonEncode($stylesheets));

		// collect all script file references
		$scripts = $cs->getScriptUrls();
		if (count($scripts)>0)
		$this->appendContentPart($response, self::CALLBACK_SCRIPTLIST_HEADER, TJavaScript::jsonEncode($scripts));

		// collect all hidden field references
		$fields = $cs->getHiddenFields();
		if (count($fields)>0)
		$this->appendContentPart($response, self::CALLBACK_HIDDENFIELDLIST_HEADER, TJavaScript::jsonEncode($fields));
	}

	/**
	 * Appends data or javascript code to the body content surrounded with delimiters
	 */
	private function appendContentPart($response, $delimiter, $data)
	{
		$content = $response->createHtmlWriter();
		$content->getWriter()->setBoundary($delimiter);
		$content->write($data);
	}

	/**
	 * Trys to find the callback event handler and raise its callback event.
	 * @throws TInvalidCallbackException if call back target is not found.
	 * @throws TInvalidCallbackException if the requested target does not
	 * implement ICallbackEventHandler.
	 */
	private function raiseCallbackEvent()
	{
		 if(($callbackHandler=$this->getCallbackEventTarget())!==null)
		 {
			if($callbackHandler instanceof ICallbackEventHandler)
			{
				$param = $this->getCallbackEventParameter();
				$result = new TCallbackEventParameter($this->getResponse(), $param);
				$callbackHandler->raiseCallbackEvent($result);
			}
			else
			{
				throw new TInvalidCallbackException(
					'callback_invalid_handler', $callbackHandler->getUniqueID());
			}
		 }
		 else
		 {
		 	$target = $this->getRequest()->itemAt(TPage::FIELD_CALLBACK_TARGET);
		 	throw new TInvalidCallbackException('callback_invalid_target', $target);
		 }
	}

	/**
	 * @return TControl the control responsible for the current callback event,
	 * null if nonexistent
	 */
	public function getCallbackEventTarget()
	{
		if($this->_callbackEventTarget===null)
		{
			$eventTarget=$this->getRequest()->itemAt(TPage::FIELD_CALLBACK_TARGET);
			if(!empty($eventTarget))
				$this->_callbackEventTarget=$this->getPage()->findControl($eventTarget);
		}
		return $this->_callbackEventTarget;
	}

	/**
	 * Registers a control to raise callback event in the current request.
	 * @param TControl control registered to raise callback event.
	 */
	public function setCallbackEventTarget(TControl $control)
	{
		$this->_callbackEventTarget=$control;
	}

	/**
	 * Gets callback parameter.
	 * @return string postback event parameter
	 */
	public function getCallbackEventParameter()
	{
		if($this->_callbackEventParameter===null)
		{
			$param = $this->getRequest()->itemAt(TPage::FIELD_CALLBACK_PARAMETER);
			$this->_callbackEventParameter=$param;
		}
		return $this->_callbackEventParameter;
	}

	/**
	 * @param mixed postback event parameter
	 */
	public function setCallbackEventParameter($value)
	{
		$this->_callbackEventParameter=$value;
	}

	/**
	 * Gets the callback client script handler. It handlers the javascript functions
	 * to be executed during the callback response.
	 * @return TCallbackClientScript callback client handler.
	 */
	public function getCallbackClientHandler()
	{
		if($this->_callbackClient===null)
			$this->_callbackClient = new TCallbackClientScript;
		return $this->_callbackClient;
	}
}

/**
 * TCallbackErrorHandler class.
 *
 * Captures errors and exceptions and send them back during callback response.
 * When the application is in debug mode, the error and exception stack trace
 * are shown. A TJavascriptLogger must be present on the client-side to view
 * the error stack trace.
 *
 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
 * @package System.Web.UI.ActiveControls
 * @since 3.1
 */
class TCallbackErrorHandler extends TErrorHandler
{
	/**
	 * Displays the exceptions to the client-side TJavascriptLogger.
	 * A HTTP 500 status code is sent and the stack trace is sent as JSON encoded.
	 * @param Exception exception details.
	 */
	protected function displayException($exception)
	{
		if($this->getApplication()->getMode()===TApplication::STATE_DEBUG)
		{
			$response = $this->getApplication()->getResponse();
			$trace = $this->getExceptionStackTrace($exception);
			// avoid error on non-utf8 strings
			try {
				$trace = TJavaScript::jsonEncode($trace);
			} catch (Exception $e) {
				// strip everythin not 7bit ascii
				$trace = preg_replace('/[^(\x20-\x7F)]*/','', serialize($trace));
			}

			// avoid exception loop if headers have already been sent
			try {
				$response->setStatusCode(500, 'Internal Server Error');
			} catch (Exception $e) { }

			$content = $response->createHtmlWriter();
			$content->getWriter()->setBoundary(TActivePageAdapter::CALLBACK_ERROR_HEADER);
			$content->write($trace);
		}
		else
		{
			error_log("Error happened while processing an existing error:\n".$exception->__toString());
			header('HTTP/1.0 500 Internal Server Error', true, 500);
		}
		$this->getApplication()->getResponse()->flush();
	}

	/**
	 * @param Exception exception details.
	 * @return array exception stack trace details.
	 */
	private function getExceptionStackTrace($exception)
	{
		$data['code']=$exception->getCode() > 0 ? $exception->getCode() : 500;
		$data['file']=$exception->getFile();
		$data['line']=$exception->getLine();
		$data['trace']=$exception->getTrace();
		if($exception instanceof TPhpErrorException)
		{
			// if PHP exception, we want to show the 2nd stack level context
			// because the 1st stack level is of little use (it's in error handler)
			if(isset($trace[0]) && isset($trace[0]['file']) && isset($trace[0]['line']))
			{
				$data['file']=$trace[0]['file'];
				$data['line']=$trace[0]['line'];
			}
		}
		$data['type']=get_class($exception);
		$data['message']=$exception->getMessage();
		$data['version']=$_SERVER['SERVER_SOFTWARE'].' '.Prado::getVersion();
		$data['time']=@strftime('%Y-%m-%d %H:%M',time());
		return $data;
	}
}

/**
 * TInvalidCallbackException class.
 *
 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
 * @package System.Web.UI.ActiveControls
 * @since 3.1
 */
class TInvalidCallbackException extends TException
{
}