Completed
Push — 16.1 ( 4aafcf...095a95 )
by Ralf
70:38 queued 43:13
created

Request::cleanup()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 14
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 5
nc 4
nop 0
dl 0
loc 14
rs 8.8571
c 0
b 0
f 0
1
<?php
2
/**
3
 * EGroupware - eTemplate request object storing request-data directly in the form itself
4
 *
5
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
6
 * @package api
7
 * @subpackage etemplate
8
 * @link http://www.egroupware.org
9
 * @author Ralf Becker <[email protected]>
10
 * @copyright (c) 2007-16 by Ralf Becker <[email protected]>
11
 * @version $Id$
12
 */
13
14
namespace EGroupware\Api\Etemplate;
15
16
use EGroupware\Api;
17
18
/**
19
 * Class to represent the persitent information of an eTemplate request
20
 *
21
 * Current default for etemplate_request is to store request-data in egw_cache by
22
 * setting a not set etemplate_request::$request_class to 'etemplate_request_cache'.
23
 *
24
 * This class stores the request-data direct in a hidden var in the form.
25
 * As this would allow an evil user to manipulate it and therefore compromise the security
26
 * of an EGroupware instance, this class should only be used, if mcrypt is available
27
 * to encrypt that data. The factory method etemplate_request::read() ensures that,
28
 * by using etemplate_request_session instead.
29
 *
30
 * The key used to encrypt the request can be set in header.inc.php by setting
31
 *
32
 *		$GLOBALS['egw_info']['server']['etemplate_form_key'] = 'something secret';
33
 *
34
 * if this var is not set, the db_pass and EGW_SERVER_ROOT is used instead.
35
 *
36
 * The request object should be instancated only via the factory method etemplate_request::read($id=null)
37
 *
38
 * $request = Api\Etemplate\Request::read();
39
 *
40
 * // add request data
41
 *
42
 * $id = $request->id();
43
 *
44
 * b) open or modify an existing request:
45
 *
46
 * if (!($request = Api\Etemplate\Request::read($id)))
47
 * {
48
 * 		// request not found
49
 * }
50
 *
51
 * Ajax requests can use this object to open the original request by using the id, they have to transmitt back,
52
 * and register further variables, modify the registered ones or delete them AND then update the id, if it changed:
53
 *
54
 * eTemplate2:
55
 *
56
 *	if (($new_id = $request->id()) != $exec_id)
57
 *	{
58
 *		Api\Json\Response::get()->generic('assign', array(
59
 *			'etemplate_exec_id' => $id,
60
 *			'id' => '',
61
 *			'key' => 'etemplate_exec_id',
62
 *			'value' => $new_id,
63
 *		));
64
 *	}
65
 *
66
 * old eTemplate:
67
 *
68
 *	if (($new_id = $request->id()) != $exec_id)
69
 *	{
70
 *		Api\Json\Response::get()->assign('etemplate_exec_id','value',$new_id);
71
 *	}
72
 *
73
 * For an example look in link_widget::ajax_search()
74
 *
75
 * @property-read boolean $data_modified true if data was modified and therefore needs saving
76
 * @property int $output_mode
77
 * @property array $content
78
 * @property array $changes
79
 * @property array $sel_options
80
 * @property array $readonlys
81
 * @property array $preserv
82
 * @property string $method
83
 * @property array $ignore_validation
84
 * @property array $template
85
 * @property string $app_header
86
 */
87
class Request
0 ignored issues
show
Coding Style introduced by
Since you have declared the constructor as private, maybe you should also declare the class as final.
Loading history...
88
{
89
	/**
90
	 * here is the request data stored
91
	 *
92
	 * @var array
93
	 */
94
	protected $data=array();
95
	/**
96
	 * Flag if data has been modified and therefor need to be stored again in the session
97
	 *
98
	 * @var boolean
99
	 */
100
	protected $data_modified=false;
101
	/**
102
	 * Flag that stored data should be removed by destructor, if not modified.
103
	 *
104
	 * @var boolean
105
	 */
106
	protected $remove_if_not_modified=false;
107
	/**
108
	 * mcrypt resource
109
	 *
110
	 * @var resource
111
	 */
112
	static protected $mcrypt;
113
114
	/**
115
	 * See gzcompress, set it to 0 to not compress
116
	 *
117
	 * @var int
118
	 */
119
	static public $compression_level = 6;
120
121
	/**
122
	 * Name of request class used
123
	 *
124
	 * Can be set here to force a certain class, otherwise the factory method chooses one
125
	 *
126
	 * @var string
127
	 */
128
	static public $request_class; // = 'etemplate_request_session';
129
130
	/**
131
	 * Factory method to get a new request object or the one for an existing request
132
	 *
133
	 * New default is to use egw_cache to store requests and no longer request or
134
	 * session documented below:
135
	 *
136
	 * If mcrypt AND gzcompress is available this factory method chooses etemplate_request,
137
	 * which stores the request data encrypted in a hidden var directly in the form,
138
	 * over etemplate_request_session, which stores the data in the session (and causing
139
	 * the sesison to constantly grow).
140
	 *
141
	 * @param string $id =null
142
	 * @return Request
143
	 */
144
	public static function read($id=null)
145
	{
146
		if (is_null(self::$request_class))
147
		{
148
			// new default to use egw_cache to store requests
149
			self::$request_class = __CLASS__.'\\Cache';
150
			/* old default to use request if mcrypt and gzcompress are available and session if not
151
			self::$request_class = check_load_extension('mcrypt') && function_exists('gzcompress') &&
152
				self::init_crypt() ? __CLASS__ : 'etemplate_request_session';
153
			 */
154
		}
155
		if (self::$request_class != __CLASS__)
156
		{
157
			$request = call_user_func(self::$request_class.'::read', $id);
158
		}
159
		else
160
		{
161
			$request = new Request();
162
163
			if (!is_null($id))
164
			{
165
				$id = base64_decode($id);
166
167
				// decrypt the data if available
168
				if (self::init_crypt())
169
				{
170
					$id = mdecrypt_generic(self::$mcrypt,$id);
171
				}
172
				// uncompress the data if available
173
				if (self::$compression_level && function_exists('gzcompress'))
174
				{
175
					//$len_compressed = bytes($id);
176
					//$time = microtime(true);
177
					$id = gzuncompress($id);
178
					//$time = number_format(1000.0 * (microtime(true) - $time),1);
179
					//$len_uncompressed = bytes($id);
180
					//error_log(__METHOD__."() uncompressed from $len_compressed to $len_uncompressed bytes $time ms");
181
				}
182
				$request->data = unserialize($id);
183
184
				if (!$request->data)
185
				{
186
					error_log(__METHOD__."() id not valid!");
187
					$request = false;
188
				}
189
				//error_log(__METHOD__."() size of request = ".bytes($id));
190
			}
191
		}
192
		if (!$request)	// eT2 request/session expired
193
		{
194
			list($app) = explode('.', $_GET['menuaction']);
195
			$global = false;
196
			if(isset($GLOBALS['egw_info']['apps'][$app]))
197
			{
198
				$index_url = isset($GLOBALS['egw_info']['apps'][$app]['index']) ?
199
					'/index.php?menuaction='.$GLOBALS['egw_info']['apps'][$app]['index'] : '/'.$app.'/index.php';
200
			}
201
			else
202
			{
203
				$index_url = Api\Framework::link('/index.php');
204
				$global = true;
205
				$app = null;
206
			}
207
			// add a unique token to redirect to avoid client-side framework tries refreshing via nextmatch
208
			$index_url .= (strpos($index_url, '?') ? '&' : '?').'redirect='.microtime(true);
209
			error_log(__METHOD__."('$id', ...) eT2 request not found / expired --> redirecting app $app to $index_url (_GET[menuaction]=$_GET[menuaction], isJSONRequest()=".array2string(Api\Json\Request::isJSONRequest()).')');
210
			if (Api\Json\Request::isJSONRequest())
211
			{
212
				// we must not redirect ajax_destroy_session calls, as they might originate from our own redirect!
213
				if (strpos($_GET['menuaction'], '.ajax_destroy_session.etemplate') === false)
214
				{
215
					$response = Api\Json\Response::get();
216
					$response->redirect($index_url, $global, $app);
217
					exit;
218
				}
219
			}
220
			else
221
			{
222
				Api\Framework::redirect_link($index_url);
223
			}
224
		}
225
		return $request;
226
	}
227
228
	/**
229
	 * Private constructor to force the instancation of this class only via it's static factory method read
230
	 *
231
	 * @param string $id =null
232
	 */
233
	private function __construct($id=null)
234
	{
235
		unset($id);
236
	}
237
238
	/**
239
	 * return the id of this request
240
	 *
241
	 * @return string
242
	 */
243
	public function &id()
244
	{
245
		$this->cleanup();
246
		$data = serialize($this->data);
247
248
		// compress the data if available
249
		if (self::$compression_level && function_exists('gzcompress'))
250
		{
251
			//$len_uncompressed = bytes($id);
252
			//$time = microtime(true);
253
			$data = gzcompress($data, self::$compression_level);
254
			//$time = number_format(1000.0 * (microtime(true) - $time),1);
255
			//$len_compressed = bytes($id);
256
			//error_log(__METHOD__."() compressed from $len_uncompressed to $len_compressed bytes in $time ms");
257
		}
258
		// encrypt the data if available
259
		if (self::init_crypt())
260
		{
261
			$data = mcrypt_generic(self::$mcrypt, $data);
262
		}
263
		$id = base64_encode($data);
264
265
		//error_log(__METHOD__."() #$this->id: size of request = ".bytes($id));//.", id='$id'");
266
		//self::debug();
267
		return $id;
268
	}
269
270
	/**
271
	 * Clean up data before storing it: currently only removes "real" nextmatch rows
272
	 */
273
	protected function cleanup()
274
	{
275
		if (isset($this->data['content']['nm']) && is_array($this->data['content']['nm']['rows']))
276
		{
277
			foreach(array_keys($this->data['content']['nm']['rows']) as $n)
278
			{
279
				if (is_int($n))
280
				{
281
					unset($this->data['content']['nm']['rows'][$n]);
282
				}
283
			}
284
			//error_log(__METHOD__."() content[nm][rows]=".array2string($this->data['content']['nm']['rows']));
285
		}
286
	}
287
288
	/**
289
	 * Register a form-variable to be processed
290
	 *
291
	 * @param string $_form_name form-name
292
	 * @param string $type etemplate type
293
	 * @param array $data =array() optional extra data
294
	 */
295
	public function set_to_process($_form_name, $type, $data=array())
296
	{
297
		if (!$_form_name || !$type) return;
298
299
		//echo '<p>'.__METHOD__."($form_name,$type,".array2string($data).")</p>\n";
300
		$data['type'] = $type;
301
302
		// unquote single and double quotes, as this is how they get returned in $_POST
303
		$form_name = str_replace(array('\\\'','&quot;'), array('\'','"'), $_form_name);
304
305
		$this->data['to_process'][$form_name] = $data;
306
		$this->data_modified = true;
307
	}
308
309
	/**
310
	 * Set an attribute of a to-process record
311
	 *
312
	 * @param string $_form_name form-name
313
	 * @param string $attribute etemplate type
314
	 * @param array $value
315
	 * @param boolean $add_to_array =false should $value be added to the attribute array
316
	 */
317
	public function set_to_process_attribute($_form_name, $attribute, $value, $add_to_array=false)
318
	{
319
		//echo '<p>'.__METHOD__."($form_name,$attribute,$value,$add_to_array)</p>\n";
320
		if (!$_form_name) return;
321
322
		// unquote single and double quotes, as this is how they get returned in $_POST
323
		$form_name = str_replace(array('\\\'','&quot;'), array('\'','"'), $_form_name);
324
325
		if ($add_to_array)
326
		{
327
			$this->data['to_process'][$form_name][$attribute][] = $value;
328
		}
329
		else
330
		{
331
			$this->data['to_process'][$form_name][$attribute] = $value;
332
		}
333
		$this->data_modified = true;
334
	}
335
336
	/**
337
	 * Unregister a form-variable to be no longer processed
338
	 *
339
	 * @param string $form_name form-name
340
	 */
341
	public function unset_to_process($form_name)
342
	{
343
		//echo '<p>'.__METHOD__."($form_name) isset_to_process($form_name)=".$this->isset_to_process($form_name)."</p>\n";
344
		unset($this->data['to_process'][$form_name]);
345
		$this->data_modified = true;
346
	}
347
348
	/**
349
	 * return the data of a form-var to process or the whole array
350
	 *
351
	 * @param string $form_name =null
352
	 * @return array
353
	 */
354
	public function get_to_process($form_name=null)
355
	{
356
		//echo '<p>'.__METHOD__."($form_name)</p>\n";
357
		return $form_name ? $this->data['to_process'][$form_name] : $this->data['to_process'];
358
	}
359
360
	/**
361
	 * check if something set for a given $form_name
362
	 *
363
	 * @param string $form_name
364
	 * @return boolean
365
	 */
366
	public function isset_to_process($form_name)
367
	{
368
		//echo '<p>'.__METHOD__."($form_name) = ".array2string(isset($this->data['to_process'][$form_name]))."</p>\n";
369
		return isset($this->data['to_process'][$form_name]);
370
	}
371
372
	/**
373
	 * creates a new unique request-id
374
	 *
375
	 * @return string
376
	 */
377
	static function request_id()
378
	{
379
		// replace url-unsafe chars with _ to not run into url-encoding issues when used in a url
380
		$userID = preg_replace('/[^a-z0-9_\\.@-]/i', '_', $GLOBALS['egw_info']['user']['account_lid']);
381
382
		// generate random token (using oppenssl if available otherwise mt_rand based Auth::randomstring)
383
		$token = function_exists('openssl_random_pseudo_bytes') ?
384
			// replace + with _ to not run into url-encoding issues when used in a url
385
			str_replace('+', '_', base64_encode(openssl_random_pseudo_bytes(32))) :
386
			\EGroupware\Api\Auth::randomstring(44);
387
388
		return $GLOBALS['egw_info']['flags']['currentapp'].'_'.$userID.'_'.$token;
389
	}
390
391
	/**
392
	 * magic function to set all request-vars, used eg. as $request->method = 'app.class.method';
393
	 *
394
	 * @param string $var
395
	 * @param mixed $val
396
	 */
397
	public function __set($var,$val)
398
	{
399
		if ($this->data[$var] !== $val)
400
		{
401
			$this->data[$var] = $val;
402
			//error_log(__METHOD__."('$var', ...) data of id=$this->id changed ...");
403
			$this->data_modified = true;
404
		}
405
	}
406
407
	/**
408
	 * magic function to access the request-vars, used eg. as $method = $request->method;
409
	 *
410
	 * @param string $var
411
	 * @return mixed
412
	 */
413
	public function &__get($var)
414
	{
415
		if ($var == 'data_modified') return $this->data_modified;
416
417
		return $this->data[$var];
418
	}
419
420
421
	/**
422
	 * magic function to see if a request-var has been set
423
	 *
424
	 * @param string $var
425
	 * @return boolean
426
	 */
427
	public function __isset($var)
428
	{
429
		return array_key_exists($var, $this->data);
430
	}
431
432
	/**
433
	 * Get the names / keys of existing variables
434
	 *
435
	 * @return array
436
	 */
437
	public function names()
438
	{
439
		return array_keys($this->data);
440
	}
441
442
	/**
443
	 * Output the size-wise important parts of a request
444
	 *
445
	 * @param double $min_share minimum share to be reported (in percent of the whole request)
446
	 * @param double $dump_share minimum share from which on a variable get output
447
	 */
448
	public function debug($min_share=1.0,$dump_share=25.0)
449
	{
450
		echo "<p><b>total size request data = ".($total=strlen(serialize($this->data)))."</b></p>\n";
451
		echo "<p>shares bigger then $min_share% percent of it:</p>\n";
452
		foreach($this->data as $key => $val)
453
		{
454
			$len = strlen(is_array($val) ? serialize($val) : $val);
455
			$len .= ' ('.sprintf('%2.1lf',($percent = 100.0 * $len / $total)).'%)';
456
			if ($percent < $min_share) continue;
457
			echo "<p><b>$key</b>: strlen(\$val)=$len</p>\n";
458
			if ($percent >= $dump_share) _debug_array($val);
459
			if (is_array($val) && $len > 2000)
460
			{
461
				foreach($val as $k => $v)
462
				{
463
					$l = strlen(is_array($v) ? serialize($v) : $v);
464
					$l .= ' ('.sprintf('%2.1lf',($p = 100.0 * $l / $total)).'%)';
465
					if ($p < $min_share) continue;
466
					echo "<p>&nbsp;- {$key}[$k]: strlen(\$v)=$l</p>\n";
467
				}
468
			}
469
		}
470
	}
471
472
	/**
473
	 * Check if session encryption is configured, possible and initialise it
474
	 *
475
	 * @param string $algo ='tripledes'
476
	 * @param string $mode ='ecb'
477
	 * @return boolean true if encryption is used, false otherwise
478
	 */
479
	static public function init_crypt($algo='tripledes',$mode='ecb')
480
	{
481
		if (is_null(self::$mcrypt))
482
		{
483 View Code Duplication
			if (isset($GLOBALS['egw_info']['server']['etemplate_form_key']))
484
			{
485
				$key = $GLOBALS['egw_info']['server']['etemplate_form_key'];
486
			}
487
			else
488
			{
489
				$key = $GLOBALS['egw_info']['server']['db_pass'].EGW_SERVER_ROOT;
490
			}
491
			if (!check_load_extension('mcrypt'))
492
			{
493
				error_log(__METHOD__."() required PHP extension mcrypt not loaded and can not be loaded, eTemplate requests get NOT encrypted!");
494
				return false;
495
			}
496 View Code Duplication
			if (!(self::$mcrypt = mcrypt_module_open($algo, '', $mode, '')))
497
			{
498
				error_log(__METHOD__."() could not mcrypt_module_open(algo='$algo','',mode='$mode',''), eTemplate requests get NOT encrypted!");
499
				return false;
500
			}
501
			$iv_size = mcrypt_enc_get_iv_size(self::$mcrypt);
502
			$iv = !isset($GLOBALS['egw_info']['server']['mcrypt_iv']) || strlen($GLOBALS['egw_info']['server']['mcrypt_iv']) < $iv_size ?
503
				mcrypt_create_iv ($iv_size, MCRYPT_RAND) : substr($GLOBALS['egw_info']['server']['mcrypt_iv'],0,$iv_size);
504
505
			$key_size = mcrypt_enc_get_key_size(self::$mcrypt);
506
			if (bytes($key) > $key_size) $key = cut_bytes($key,0,$key_size-1);
507
508 View Code Duplication
			if (mcrypt_generic_init(self::$mcrypt,$key, $iv) < 0)
509
			{
510
				error_log(__METHOD__."() could not initialise mcrypt, sessions get NOT encrypted!");
511
				return self::$mcrypt = false;
512
			}
513
		}
514
		return is_resource(self::$mcrypt);
515
	}
516
517
	/**
518
	 * Destructor
519
	 */
520
	function __destruct()
521
	{
522
		if (self::$mcrypt)
523
		{
524
			mcrypt_generic_deinit(self::$mcrypt);
525
			self::$mcrypt = null;
526
		}
527
	}
528
529
	/**
530
	 * Mark request as to destroy, if it does not get modified before destructor is called
531
	 *
532
	 * If that function is called, request is removed from storage, further modification will work!
533
	 */
534
	public function remove_if_not_modified()
535
	{
536
		$this->remove_if_not_modified = true;
537
	}
538
}
539