Project

General

Profile

Download (18.6 KB) Statistics
| Branch: | Tag: | Revision:
1
<?php
2
/*
3
	xmlrpc.php
4
	Copyright (C) 2013-2015 Electric Sheep Fencing, LP
5
	Copyright (C) 2009, 2010 Scott Ullrich
6
	Copyright (C) 2005 Colin Smith
7
	All rights reserved.
8

    
9
	Redistribution and use in source and binary forms, with or without
10
	modification, are permitted provided that the following conditions are met:
11

    
12
	1. Redistributions of source code must retain the above copyright notice,
13
	   this list of conditions and the following disclaimer.
14

    
15
	2. Redistributions in binary form must reproduce the above copyright
16
	   notice, this list of conditions and the following disclaimer in the
17
	   documentation and/or other materials provided with the distribution.
18

    
19
	THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
20
	INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
21
	AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22
	AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
23
	OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24
	SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
	INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
	CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27
	ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
	POSSIBILITY OF SUCH DAMAGE.
29
*/
30

    
31
##|+PRIV
32
##|*IDENT=page-xmlrpclibrary
33
##|*NAME=XMLRPC Library page
34
##|*DESCR=Allow access to the 'XMLRPC Library' page.
35
##|*MATCH=xmlrpc.php*
36
##|-PRIV
37

    
38
require("config.inc");
39
require("functions.inc");
40
require_once("filter.inc");
41
require("ipsec.inc");
42
require("vpn.inc");
43
require("shaper.inc");
44
require("xmlrpc_server.inc");
45
require("xmlrpc.inc");
46

    
47
function xmlrpc_loop_detect() {
48
	global $config;
49

    
50
	/* grab sync to ip if enabled */
51
	if ($config['hasync']) {
52
		$synchronizetoip = $config['hasync']['synchronizetoip'];
53
	}
54
	if ($synchronizetoip) {
55
		if ($synchronizetoip == $_SERVER['REMOTE_ADDR']) {
56
			return true;
57
		}
58
	}
59

    
60
	return false;
61
}
62

    
63
$xmlrpc_g = array(
64
	"return" => array(
65
		"true" => new XML_RPC_Response(new XML_RPC_Value(true, $XML_RPC_Boolean)),
66
		"false" => new XML_RPC_Response(new XML_RPC_Value(false, $XML_RPC_Boolean)),
67
		"authfail" => new XML_RPC_Response(new XML_RPC_Value(gettext("Authentication failed"), $XML_RPC_String))
68
	)
69
);
70

    
71
/*
72
 *   pfSense XMLRPC errors
73
 *   $XML_RPC_erruser + 1 = Auth failure
74
 */
75
$XML_RPC_erruser = 200;
76

    
77
/* EXPOSED FUNCTIONS */
78
$exec_php_doc = gettext("XMLRPC wrapper for eval(). This method must be called with two parameters: a string containing the local system\'s password followed by the PHP code to evaluate.");
79
$exec_php_sig = array(
80
	array(
81
		$XML_RPC_Boolean, // First signature element is return value.
82
		$XML_RPC_String, // password
83
		$XML_RPC_String, // shell code to exec
84
	)
85
);
86

    
87
function xmlrpc_authfail() {
88
	log_auth("webConfigurator authentication error for 'admin' from {$_SERVER['REMOTE_ADDR']}");
89
}
90

    
91
function exec_php_xmlrpc($raw_params) {
92
	global $config, $xmlrpc_g;
93

    
94
	$params = xmlrpc_params_to_php($raw_params);
95
	if (!xmlrpc_auth($params)) {
96
		xmlrpc_authfail();
97
		return $xmlrpc_g['return']['authfail'];
98
	}
99
	$exec_php = $params[0];
100
	eval($exec_php);
101
	if ($toreturn) {
102
		$response = XML_RPC_encode($toreturn);
103
		return new XML_RPC_Response($response);
104
	} else {
105
		return $xmlrpc_g['return']['true'];
106
	}
107
}
108

    
109
/*****************************/
110
$exec_shell_doc = gettext("XMLRPC wrapper for mwexec(). This method must be called with two parameters: a string containing the local system\'s password followed by an shell command to execute.");
111
$exec_shell_sig = array(
112
	array(
113
		$XML_RPC_Boolean, // First signature element is return value.
114
		$XML_RPC_String, // password
115
		$XML_RPC_String, // shell code to exec
116
	)
117
);
118

    
119
function exec_shell_xmlrpc($raw_params) {
120
	global $config, $xmlrpc_g;
121

    
122
	$params = xmlrpc_params_to_php($raw_params);
123
	if (!xmlrpc_auth($params)) {
124
		xmlrpc_authfail();
125
		return $xmlrpc_g['return']['authfail'];
126
	}
127
	$shell_cmd = $params[0];
128
	mwexec($shell_cmd);
129

    
130
	return $xmlrpc_g['return']['true'];
131
}
132

    
133
/*****************************/
134
$backup_config_section_doc = gettext("XMLRPC wrapper for backup_config_section. This method must be called with two parameters: a string containing the local system\'s password followed by an array containing the keys to be backed up.");
135
$backup_config_section_sig = array(
136
	array(
137
		$XML_RPC_Struct, // First signature element is return value.
138
		$XML_RPC_String,
139
		$XML_RPC_Array
140
	)
141
);
142

    
143
function backup_config_section_xmlrpc($raw_params) {
144
	global $config, $xmlrpc_g;
145

    
146
	if (xmlrpc_loop_detect()) {
147
		log_error("Disallowing CARP sync loop");
148
		return;
149
	}
150

    
151
	$params = xmlrpc_params_to_php($raw_params);
152
	if (!xmlrpc_auth($params)) {
153
		xmlrpc_authfail();
154
		return $xmlrpc_g['return']['authfail'];
155
	}
156
	$val = array_intersect_key($config, array_flip($params[0]));
157

    
158
	return new XML_RPC_Response(XML_RPC_encode($val));
159
}
160

    
161
/*****************************/
162
$restore_config_section_doc = gettext("XMLRPC wrapper for restore_config_section. This method must be called with two parameters: a string containing the local system\'s password and an array to merge into the system\'s config. This function returns true upon completion.");
163
$restore_config_section_sig = array(
164
	array(
165
		$XML_RPC_Boolean,
166
		$XML_RPC_String,
167
		$XML_RPC_Struct
168
	)
169
);
170

    
171
function restore_config_section_xmlrpc($raw_params) {
172
	global $config, $xmlrpc_g;
173

    
174
	$old_config = $config;
175

    
176
	if (xmlrpc_loop_detect()) {
177
		log_error("Disallowing CARP sync loop");
178
		return;
179
	}
180

    
181
	$params = xmlrpc_params_to_php($raw_params);
182
	if (!xmlrpc_auth($params)) {
183
		xmlrpc_authfail();
184
		return $xmlrpc_g['return']['authfail'];
185
	}
186

    
187
	/*
188
	 * Make sure it doesn't end up with both dnsmasq and unbound enabled
189
	 * simultaneously in secondary
190
	 * */
191
	if (isset($params[0]['unbound']['enable']) && isset($config['dnsmasq']['enable'])) {
192
		unset($config['dnsmasq']['enable']);
193
		services_dnsmasq_configure();
194
	} else if (isset($params[0]['dnsmasq']['enable']) && isset($config['unbound']['enable'])) {
195
		unset($config['unbound']['enable']);
196
		services_unbound_configure();
197
	}
198

    
199
	// Some sections should just be copied and not merged or we end
200
	//   up unable to sync the deletion of the last item in a section
201
	$sync_full = array('dnsmasq', 'unbound', 'ipsec', 'aliases', 'wol', 'load_balancer', 'openvpn', 'cert', 'ca', 'crl', 'schedules', 'filter', 'nat', 'dhcpd', 'dhcpv6');
202
	$sync_full_done = array();
203
	foreach ($sync_full as $syncfull) {
204
		if (isset($params[0][$syncfull])) {
205
			$config[$syncfull] = $params[0][$syncfull];
206
			unset($params[0][$syncfull]);
207
			$sync_full_done[] = $syncfull;
208
		}
209
	}
210

    
211
	$vipbackup = array();
212
	$oldvips = array();
213
	if (isset($params[0]['virtualip'])) {
214
		if (is_array($config['virtualip']['vip'])) {
215
			foreach ($config['virtualip']['vip'] as $vipindex => $vip) {
216
				if ($vip['mode'] == "carp") {
217
					$oldvips["{$vip['interface']}_vip{$vip['vhid']}"]['content'] = "{$vip['password']}{$vip['advskew']}{$vip['subnet']}{$vip['subnet_bits']}{$vip['advbase']}";
218
					$oldvips["{$vip['interface']}_vip{$vip['vhid']}"]['interface'] = $vip['interface'];
219
					$oldvips["{$vip['interface']}_vip{$vip['vhid']}"]['subnet'] = $vip['subnet'];
220
				} else if ($vip['mode'] == "ipalias" && (substr($vip['interface'], 0, 4) == '_vip' || strpos($vip['interface'], "lo0"))) {
221
					$oldvips[$vip['subnet']]['content'] = "{$vip['interface']}{$vip['subnet']}{$vip['subnet_bits']}";
222
					$oldvips[$vip['subnet']]['interface'] = $vip['interface'];
223
					$oldvips[$vip['subnet']]['subnet'] = $vip['subnet'];
224
				} else if (($vip['mode'] == "ipalias" || $vip['mode'] == 'proxyarp') && !(substr($vip['interface'], 0, 4) == '_vip') || strpos($vip['interface'], "lo0")) {
225
					$vipbackup[] = $vip;
226
				}
227
			}
228
		}
229
	}
230

    
231
	// For vip section, first keep items sent from the master
232
	$config = array_merge_recursive_unique($config, $params[0]);
233

    
234
	/* Then add ipalias and proxyarp types already defined on the backup */
235
	if (is_array($vipbackup) && !empty($vipbackup)) {
236
		if (!is_array($config['virtualip'])) {
237
			$config['virtualip'] = array();
238
		}
239
		if (!is_array($config['virtualip']['vip'])) {
240
			$config['virtualip']['vip'] = array();
241
		}
242
		foreach ($vipbackup as $vip) {
243
			array_unshift($config['virtualip']['vip'], $vip);
244
		}
245
	}
246

    
247
	/* Log what happened */
248
	$mergedkeys = implode(",", array_merge(array_keys($params[0]), $sync_full_done));
249
	write_config(sprintf(gettext("Merged in config (%s sections) from XMLRPC client."), $mergedkeys));
250

    
251
	/*
252
	 * The real work on handling the vips specially
253
	 * This is a copy of intefaces_vips_configure with addition of not reloading existing/not changed carps
254
	 */
255
	if (isset($params[0]['virtualip']) && is_array($config['virtualip']) && is_array($config['virtualip']['vip'])) {
256
		$carp_setuped = false;
257
		$anyproxyarp = false;
258
		foreach ($config['virtualip']['vip'] as $vip) {
259
			if ($vip['mode'] == "carp" && isset($oldvips["{$vip['interface']}_vip{$vip['vhid']}"])) {
260
				if ($oldvips["{$vip['interface']}_vip{$vip['vhid']}"]['content'] == "{$vip['password']}{$vip['advskew']}{$vip['subnet']}{$vip['subnet_bits']}{$vip['advbase']}") {
261
					if (does_vip_exist($vip)) {
262
						unset($oldvips["{$vip['interface']}_vip{$vip['vhid']}"]);
263
						continue; // Skip reconfiguring this vips since nothing has changed.
264
					}
265
				}
266
			} else if ($vip['mode'] == "ipalias" && strstr($vip['interface'], "_vip") && isset($oldvips[$vip['subnet']])) {
267
				if ($oldvips[$vip['subnet']]['content'] == "{$vip['interface']}{$vip['subnet']}{$vip['subnet_bits']}") {
268
					if (does_vip_exist($vip)) {
269
						unset($oldvips[$vip['subnet']]);
270
						continue; // Skip reconfiguring this vips since nothing has changed.
271
					}
272
				}
273
				unset($oldvips[$vip['subnet']]);
274
			}
275

    
276
			switch ($vip['mode']) {
277
				case "proxyarp":
278
					$anyproxyarp = true;
279
					break;
280
				case "ipalias":
281
					interface_ipalias_configure($vip);
282
					break;
283
				case "carp":
284
					if ($carp_setuped == false) {
285
						$carp_setuped = true;
286
					}
287
					interface_carp_configure($vip);
288
					break;
289
			}
290
		}
291
		/* Cleanup remaining old carps */
292
		foreach ($oldvips as $oldvipar) {
293
			$oldvipif = get_real_interface($oldvipar['interface']);
294
			if (!empty($oldvipif)) {
295
				if (is_ipaddrv6($oldvipar['subnet'])) {
296
					 mwexec("/sbin/ifconfig " . escapeshellarg($oldvipif) . " inet6 " . escapeshellarg($oldvipar['subnet']) . " delete");
297
				} else {
298
					pfSense_interface_deladdress($oldvipif, $oldvipar['subnet']);
299
				}
300
			}
301
		}
302
		if ($carp_setuped == true) {
303
			interfaces_sync_setup();
304
		}
305
		if ($anyproxyarp == true) {
306
			interface_proxyarp_configure();
307
		}
308
	}
309

    
310
	if (isset($old_config['ipsec']['enable']) !== isset($config['ipsec']['enable'])) {
311
		vpn_ipsec_configure();
312
	}
313

    
314
	unset($old_config);
315

    
316
	return $xmlrpc_g['return']['true'];
317
}
318

    
319
/*****************************/
320
$merge_config_section_doc = gettext("XMLRPC wrapper for merging package sections. This method must be called with two parameters: a string containing the local system\'s password and an array to merge into the system\'s config. This function returns true upon completion.");
321
$merge_config_section_sig = array(
322
	array(
323
		$XML_RPC_Boolean,
324
		$XML_RPC_String,
325
		$XML_RPC_Struct
326
	)
327
);
328

    
329
function merge_installedpackages_section_xmlrpc($raw_params) {
330
	global $config, $xmlrpc_g;
331

    
332
	if (xmlrpc_loop_detect()) {
333
		log_error("Disallowing CARP sync loop");
334
		return;
335
	}
336

    
337
	$params = xmlrpc_params_to_php($raw_params);
338
	if (!xmlrpc_auth($params)) {
339
		xmlrpc_authfail();
340
		return $xmlrpc_g['return']['authfail'];
341
	}
342
	$config['installedpackages'] = array_merge($config['installedpackages'], $params[0]);
343
	$mergedkeys = implode(",", array_keys($params[0]));
344
	write_config(sprintf(gettext("Merged in config (%s sections) from XMLRPC client."), $mergedkeys));
345

    
346
	return $xmlrpc_g['return']['true'];
347
}
348

    
349
/*****************************/
350
$merge_config_section_doc = gettext("XMLRPC wrapper for merge_config_section. This method must be called with two parameters: a string containing the local system\'s password and an array to merge into the system\'s config. This function returns true upon completion.");
351
$merge_config_section_sig = array(
352
	array(
353
		$XML_RPC_Boolean,
354
		$XML_RPC_String,
355
		$XML_RPC_Struct
356
	)
357
);
358

    
359
function merge_config_section_xmlrpc($raw_params) {
360
	global $config, $xmlrpc_g;
361

    
362
	if (xmlrpc_loop_detect()) {
363
		log_error("Disallowing CARP sync loop");
364
		return;
365
	}
366

    
367
	$params = xmlrpc_params_to_php($raw_params);
368
	if (!xmlrpc_auth($params)) {
369
		xmlrpc_authfail();
370
		return $xmlrpc_g['return']['authfail'];
371
	}
372
	$config_new = array_overlay($config, $params[0]);
373
	$config = $config_new;
374
	$mergedkeys = implode(",", array_keys($params[0]));
375
	write_config(sprintf(gettext("Merged in config (%s sections) from XMLRPC client."), $mergedkeys));
376
	return $xmlrpc_g['return']['true'];
377
}
378

    
379
/*****************************/
380
$filter_configure_doc = gettext("Basic XMLRPC wrapper for filter_configure. This method must be called with one parameter: a string containing the local system\'s password. This function returns true upon completion.");
381
$filter_configure_sig = array(
382
	array(
383
		$XML_RPC_Boolean,
384
		$XML_RPC_String
385
	)
386
);
387

    
388
function filter_configure_xmlrpc($raw_params) {
389
	global $xmlrpc_g, $config;
390

    
391
	$params = xmlrpc_params_to_php($raw_params);
392
	if (!xmlrpc_auth($params)) {
393
		xmlrpc_authfail();
394
		return $xmlrpc_g['return']['authfail'];
395
	}
396
	filter_configure();
397
	system_routing_configure();
398
	setup_gateways_monitor();
399
	relayd_configure();
400
	require_once("openvpn.inc");
401
	openvpn_resync_all();
402
	if (isset($config['dnsmasq']['enable'])) {
403
		services_dnsmasq_configure();
404
	} elseif (isset($config['unbound']['enable'])) {
405
		services_unbound_configure();
406
	} else {
407
		# Both calls above run services_dhcpd_configure(), then we just
408
		# need to call it when they are not called to avoid restarting dhcpd
409
		# twice, as described on ticket #3797
410
		services_dhcpd_configure();
411
	}
412
	local_sync_accounts();
413

    
414
	return $xmlrpc_g['return']['true'];
415
}
416

    
417
/*****************************/
418
$carp_configure_doc = gettext("Basic XMLRPC wrapper for configuring CARP interfaces.");
419
$carp_configure_sig = array(
420
	array(
421
		$XML_RPC_Boolean,
422
		$XML_RPC_String
423
	)
424
);
425

    
426
function interfaces_carp_configure_xmlrpc($raw_params) {
427
	global $xmlrpc_g;
428

    
429
	if (xmlrpc_loop_detect()) {
430
		log_error("Disallowing CARP sync loop");
431
		return;
432
	}
433

    
434
	$params = xmlrpc_params_to_php($raw_params);
435
	if (!xmlrpc_auth($params)) {
436
		xmlrpc_authfail();
437
		return $xmlrpc_g['return']['authfail'];
438
	}
439
	interfaces_vips_configure();
440

    
441
	return $xmlrpc_g['return']['true'];
442
}
443

    
444
/*****************************/
445
$check_firmware_version_doc = gettext("Basic XMLRPC wrapper for check_firmware_version. This function will return the output of check_firmware_version upon completion.");
446

    
447
$check_firmware_version_sig = array(
448
	array(
449
		$XML_RPC_String,
450
		$XML_RPC_String
451
	)
452
);
453

    
454
function check_firmware_version_xmlrpc($raw_params) {
455
	global $xmlrpc_g, $XML_RPC_String;
456

    
457
	$params = xmlrpc_params_to_php($raw_params);
458
	if (!xmlrpc_auth($params)) {
459
		xmlrpc_authfail();
460
		return $xmlrpc_g['return']['authfail'];
461
	}
462
	return new XML_RPC_Response(new XML_RPC_Value(check_firmware_version(false), $XML_RPC_String));
463
}
464

    
465
/*****************************/
466
$pfsense_firmware_version_doc = gettext("Basic XMLRPC wrapper for check_firmware_version. This function will return the output of check_firmware_version upon completion.");
467

    
468
$pfsense_firmware_version_sig = array (
469
	array (
470
		$XML_RPC_Struct,
471
		$XML_RPC_String
472
	)
473
);
474

    
475
function pfsense_firmware_version_xmlrpc($raw_params) {
476
		global $xmlrpc_g;
477

    
478
		$params = xmlrpc_params_to_php($raw_params);
479
		if (!xmlrpc_auth($params)) {
480
			xmlrpc_authfail();
481
			return $xmlrpc_g['return']['authfail'];
482
		}
483
		return new XML_RPC_Response(XML_RPC_encode(host_firmware_version()));
484
}
485

    
486
/*****************************/
487
$reboot_doc = gettext("Basic XMLRPC wrapper for rc.reboot.");
488
$reboot_sig = array(array($XML_RPC_Boolean, $XML_RPC_String));
489
function reboot_xmlrpc($raw_params) {
490
	global $xmlrpc_g;
491

    
492
	$params = xmlrpc_params_to_php($raw_params);
493
	if (!xmlrpc_auth($params)) {
494
		xmlrpc_authfail();
495
		return $xmlrpc_g['return']['authfail'];
496
	}
497
	mwexec_bg("/etc/rc.reboot");
498

    
499
	return $xmlrpc_g['return']['true'];
500
}
501

    
502
/*****************************/
503
$get_notices_sig = array(
504
	array(
505
		$XML_RPC_Array,
506
		$XML_RPC_String
507
	),
508
	array(
509
		$XML_RPC_Array
510
	)
511
);
512

    
513
function get_notices_xmlrpc($raw_params) {
514
	global $g, $xmlrpc_g;
515

    
516
	$params = xmlrpc_params_to_php($raw_params);
517
	if (!xmlrpc_auth($params)) {
518
		xmlrpc_authfail();
519
		return $xmlrpc_g['return']['authfail'];
520
	}
521
	if (!function_exists("get_notices")) {
522
		require("notices.inc");
523
	}
524
	if (!$params) {
525
		$toreturn = get_notices();
526
	} else {
527
		$toreturn = get_notices($params);
528
	}
529
	$response = new XML_RPC_Response(XML_RPC_encode($toreturn));
530

    
531
	return $response;
532
}
533

    
534
$xmlrpclockkey = lock('xmlrpc', LOCK_EX);
535

    
536
/*****************************/
537
$server = new XML_RPC_Server(
538
	array(
539
		'pfsense.exec_shell' => array('function' => 'exec_shell_xmlrpc',
540
			'signature' => $exec_shell_sig,
541
			'docstring' => $exec_shell_doc),
542
		'pfsense.exec_php' => array('function' => 'exec_php_xmlrpc',
543
			'signature' => $exec_php_sig,
544
			'docstring' => $exec_php_doc),
545
		'pfsense.filter_configure' => array('function' => 'filter_configure_xmlrpc',
546
			'signature' => $filter_configure_sig,
547
			'docstring' => $filter_configure_doc),
548
		'pfsense.interfaces_carp_configure' => array('function' => 'interfaces_carp_configure_xmlrpc',
549
			'docstring' => $carp_configure_sig),
550
		'pfsense.backup_config_section' => array('function' => 'backup_config_section_xmlrpc',
551
			'signature' => $backup_config_section_sig,
552
			'docstring' => $backup_config_section_doc),
553
		'pfsense.restore_config_section' => array('function' => 'restore_config_section_xmlrpc',
554
			'signature' => $restore_config_section_sig,
555
			'docstring' => $restore_config_section_doc),
556
		'pfsense.merge_config_section' => array('function' => 'merge_config_section_xmlrpc',
557
			'signature' => $merge_config_section_sig,
558
			'docstring' => $merge_config_section_doc),
559
		'pfsense.merge_installedpackages_section_xmlrpc' => array('function' => 'merge_installedpackages_section_xmlrpc',
560
			'signature' => $merge_config_section_sig,
561
			'docstring' => $merge_config_section_doc),
562
		'pfsense.check_firmware_version' => array('function' => 'check_firmware_version_xmlrpc',
563
			'signature' => $check_firmware_version_sig,
564
			'docstring' => $check_firmware_version_doc),
565
		'pfsense.host_firmware_version' => array('function' => 'pfsense_firmware_version_xmlrpc',
566
			'signature' => $pfsense_firmware_version_sig,
567
			'docstring' => $host_firmware_version_doc),
568
		'pfsense.reboot' => array('function' => 'reboot_xmlrpc',
569
			'signature' => $reboot_sig,
570
			'docstring' => $reboot_doc),
571
		'pfsense.get_notices' => array('function' => 'get_notices_xmlrpc',
572
			'signature' => $get_notices_sig)
573
	)
574
);
575

    
576
unlock($xmlrpclockkey);
577

    
578
function array_overlay($a1, $a2) {
579
	foreach ($a1 as $k => $v) {
580
		if (!array_key_exists($k, $a2)) {
581
			continue;
582
		}
583
		if (is_array($v) && is_array($a2[$k])) {
584
			$a1[$k] = array_overlay($v, $a2[$k]);
585
		} else {
586
			$a1[$k] = $a2[$k];
587
		}
588
	}
589
	return $a1;
590
}
591

    
592
?>
(235-235/235)