readMetadata.php 17.3 KB
Newer Older
haemmer's avatar
haemmer committed
1
<?php // Copyright (c) 2012, SWITCH - Serving Swiss Universities
haemmer's avatar
haemmer committed
2
3
4
5

// This file is used to dynamically create the list of IdPs to be 
// displayed for the WAYF/DS service based on the federation metadata.
// Configuration parameters are specified in config.php.
haemmer's avatar
haemmer committed
6
7
8
9
//
// The list of Identity Providers can also be updated by running the script
// readMetadata.php periodically as web server user, e.g. with a cron entry like:
// 5 * * * * /usr/bin/php readMetadata.php > /dev/null
haemmer's avatar
haemmer committed
10

11
12
13
// Init log file
openlog("SWITCHwayf.readMetadata.php", LOG_PID | LOG_PERROR, LOG_LOCAL0);

14

haemmer's avatar
haemmer committed
15
// Make sure this script is not accessed directly
16
if(isRunViaCLI()){
haemmer's avatar
haemmer committed
17
18
19
	// Run in cli mode.
	// Could be used for testing purposes or to facilitate startup confiduration.
	// Results are dumped in $metadataIDPFile (see config.php)
20
21
22
23
24
	
	// Set dummy server name
	$_SERVER['SERVER_NAME'] = 'localhost';
	
	// Load configuration files
haemmer's avatar
haemmer committed
25
	require('config.php');
26
27
28
29
30
31
	require_once('functions.php');
	
	// Set default config options
	initConfigOptions();
	
	// Load Identity Providers
haemmer's avatar
haemmer committed
32
33
34
	require($IDPConfigFile);
	
	if (
35
		   !file_exists($metadataFile) 
haemmer's avatar
haemmer committed
36
37
38
		|| trim(@file_get_contents($metadataFile)) == '') {
	  exit ("Exiting: File ".$metadataFile." is empty or does not exist\n");
	}
39
	
40
41
42
43
44
45
46
47
48
49
	// Get an exclusive lock to generate our parsed IdP and SP files.
	if (($lockFp = fopen($metadataLockFile, 'a+')) === false) {
		$errorMsg = 'Could not open lock file '.$metadataLockFile;
		die($errorMsg);
	}
	if (flock($lockFp, LOCK_EX) === false) { 
		$errorMsg = 'Could not lock file '.$metadataLockFile;
		die($errorMsg);
	}
	
50
51
	echo 'Parsing metadata file '.$metadataFile."\n";
	list($metadataIDProviders, $metadataSProviders) = parseMetadata($metadataFile, $defaultLanguage);
haemmer's avatar
haemmer committed
52
	
53
	// If $metadataIDProviders is not FALSE, dump results in $metadataIDPFile.
haemmer's avatar
haemmer committed
54
55
	if(is_array($metadataIDProviders)){ 
		
56
57
		echo 'Dumping parsed Identity Providers to file '.$metadataIDPFile."\n";
		dumpFile($metadataIDPFile, $metadataIDProviders, 'metadataIDProviders');
58
59
60
	}
	// If $metadataSProviders is not FALSE, dump results in $metadataSPFile.
	if(is_array($metadataSProviders)){ 
haemmer's avatar
haemmer committed
61
		
62
63
64
65
66
67
68
69
70
71
72
		echo 'Dumping parsed Service Providers to file '.$metadataSPFile."\n";
		dumpFile($metadataSPFile, $metadataSProviders, 'metadataSProviders');
	}

	// Release the lock, and close.
	flock($lockFp, LOCK_UN);
	fclose($lockFp);
		
	// If $metadataIDProviders is not FALSE, update $IDProviders and print the Identity Providers lists.
	if(is_array($metadataIDProviders)){ 

73
		echo 'Merging parsed Identity Providers with data from file '.$IDProviders."\n";
haemmer's avatar
haemmer committed
74
75
76
		$IDProviders = mergeInfo($IDProviders, $metadataIDProviders, $SAML2MetaOverLocalConf, $includeLocalConfEntries);
		
		echo "Printing parsed Identity Providers:\n";
77
78
79
		print_r($metadataIDProviders);
		
		echo "Printing effective Identity Providers:\n";
haemmer's avatar
haemmer committed
80
81
82
		print_r($IDProviders);
	}
	
83
	// If $metadataSProviders is not FALSE, update $SProviders and print the list.
84
85
86
87
88
89
90
91
92
	if(is_array($metadataSProviders)){ 
		
		// Fow now copy the array by reference
		$SProviders = &$metadataSProviders;
		
		echo "Printing parsed Service Providers:\n";
		print_r($metadataSProviders);
	}
	
haemmer's avatar
haemmer committed
93
	
94
} elseif (isRunViaInclude()) {
95
	
96
97
98
99
100
101
	// Open the metadata lock file.
	if (($lockFp = fopen($metadataLockFile, 'a+')) === false) {
		$errorMsg = 'Could not open lock file '.$metadataLockFile;
		syslog(LOG_ERR, $errorMsg);
	}

102
	// Run as included file
haemmer's avatar
haemmer committed
103
	if(!file_exists($metadataIDPFile) or filemtime($metadataFile) > filemtime($metadataIDPFile)){
104
105
106
107
108
109
110
		// Get an exclusive lock to regenerate the parsed files.
		if ($lockFp !== false) {
			if (flock($lockFp, LOCK_EX) === false) { 
				$errorMsg = 'Could not get exclusive lock on '.$metadataLockFile;
				syslog(LOG_ERR, $errorMsg);
			}
		}
haemmer's avatar
haemmer committed
111
		// Regenerate $metadataIDPFile.
112
		list($metadataIDProviders, $metadataSProviders) = parseMetadata($metadataFile, $defaultLanguage);
haemmer's avatar
haemmer committed
113
114
115
116
		
		// If $metadataIDProviders is not an array (parse error in metadata),
		// $IDProviders from $IDPConfigFile will be used.
		if(is_array($metadataIDProviders)){
117
			dumpFile($metadataIDPFile, $metadataIDProviders, 'metadataIDProviders');
haemmer's avatar
haemmer committed
118
119
			$IDProviders = mergeInfo($IDProviders, $metadataIDProviders, $SAML2MetaOverLocalConf, $includeLocalConfEntries);
		}
120
121
122
123
124
125
		
		if(is_array($metadataSProviders)){
			dumpFile($metadataSPFile, $metadataSProviders, 'metadataSProviders');
			require($metadataSPFile);
		}
		
126
127
128
129
130
		// Release the lock.
		if ($lockFp !== false) {
			flock($lockFp, LOCK_UN);
		}

131
132
133
134
135
136
				// Now merge IDPs from metadata and static file
		$IDProviders = mergeInfo($IDProviders, $metadataIDProviders, $SAML2MetaOverLocalConf, $includeLocalConfEntries);
		
		// Fow now copy the array by reference
		$SProviders = &$metadataSProviders;
		
haemmer's avatar
haemmer committed
137
138
	} elseif (file_exists($metadataIDPFile)){
		
139
140
141
142
143
144
145
146
147
		// Get a shared lock to read the IdP and SP files
		// generated from the metadata file.
		if ($lockFp !== false) {
			if (flock($lockFp, LOCK_SH) === false) { 
				$errorMsg = 'Could not lock file '.$metadataLockFile;
				syslog(LOG_ERR, $errorMsg);
			}
		}

148
		// Read SP and IDP files generated with metadata
haemmer's avatar
haemmer committed
149
		require($metadataIDPFile);
150
		require($metadataSPFile);
haemmer's avatar
haemmer committed
151
	
152
153
154
155
156
		// Release the lock.
		if ($lockFp !== false) {
			flock($lockFp, LOCK_UN);
		}

157
158
159
160
161
162
		// Now merge IDPs from metadata and static file
		$IDProviders = mergeInfo($IDProviders, $metadataIDProviders, $SAML2MetaOverLocalConf, $includeLocalConfEntries);
		
		// Fow now copy the array by reference
		$SProviders = &$metadataSProviders;
	}
163
164
165
166
167

	// Close the metadata lock file.
	if ($lockFp !== false) {
		fclose($lockFp);
	}
168
	
haemmer's avatar
haemmer committed
169
170
171
172
} else {
	exit('No direct script access allowed');
}

haemmer's avatar
haemmer committed
173
174
closelog();

haemmer's avatar
haemmer committed
175
/*****************************************************************************/
176
177
// Function parseMetadata, parses metadata file and returns Array($IdPs, SPs)  or
// Array(false, false) if error occurs while parsing metadata file
haemmer's avatar
haemmer committed
178
179
function parseMetadata($metadataFile, $defaultLanguage){
	
180
	if(!file_exists($metadataFile)){
haemmer's avatar
haemmer committed
181
		$errorMsg = 'File '.$metadataFile." does not exist"; 
182
		if (isRunViaCLI()){
haemmer's avatar
haemmer committed
183
			echo $errorMsg."\n";
184
185
186
187
188
189
190
		} else {
			syslog(LOG_ERR, $errorMsg);
		}
		return Array(false, false);
	}

	if(!is_readable($metadataFile)){
haemmer's avatar
haemmer committed
191
192
193
194
195
196
		$errorMsg = 'File '.$metadataFile." cannot be read due to insufficient permissions"; 
		if (isRunViaCLI()){
			echo $errorMsg."\n";
		} else {
			syslog(LOG_ERR, $errorMsg);
		}
197
198
		return Array(false, false);
	}
haemmer's avatar
haemmer committed
199
	
200
201
	$doc = new DOMDocument();
	if(!$doc->load( $metadataFile )){
haemmer's avatar
haemmer committed
202
203
204
205
206
207
		$errorMsg = 'Could not parse metadata file '.$metadataFile; 
		if (isRunViaCLI()){
			echo $errorMsg."\n";
		} else {
			syslog(LOG_ERR, $errorMsg);
		}
208
209
210
211
212
213
214
215
216
		return Array(false, false);
	}
	
	$EntityDescriptors = $doc->getElementsByTagNameNS( 'urn:oasis:names:tc:SAML:2.0:metadata', 'EntityDescriptor' );
	
	$metadataIDProviders = Array();
	$metadataSProviders = Array();
	foreach( $EntityDescriptors as $EntityDescriptor ){
		$entityID = $EntityDescriptor->getAttribute('entityID');
haemmer's avatar
haemmer committed
217
		
218
		foreach($EntityDescriptor->childNodes as $RoleDescriptor) {
219
			$nodeName = $RoleDescriptor->localName;
220
 			switch($nodeName){
221
222
223
224
				case 'IDPSSODescriptor':
					$IDP = processIDPRoleDescriptor($RoleDescriptor);
					if ($IDP){
						$metadataIDProviders[$entityID] = $IDP;
haemmer's avatar
haemmer committed
225
					}
226
227
228
229
230
					break;
				case 'SPSSODescriptor':
					$SP = processSPRoleDescriptor($RoleDescriptor);
					if ($SP){
						$metadataSProviders[$entityID] = $SP;
231
232
					} else {
						$errorMsg = "Failed to load SP with entityID $entityID from metadata file $metadataFile";
haemmer's avatar
haemmer committed
233
234
235
236
237
						if (isRunViaCLI()){
							echo $errorMsg."\n";
						} else {
							syslog(LOG_WARNING, $errorMsg);
						}
238
					}
239
240
					break;
				default:
haemmer's avatar
haemmer committed
241
242
243
244
			}
		}
	}
	
haemmer's avatar
haemmer committed
245
246
247
248
249
250
251
252
253
254
	
	// Output result
	$infoMsg = "Successfully parsed metadata file ".$metadataFile. ". Found ".count($metadataIDProviders)." IdPs and ".count($metadataSProviders)." SPs";
	if (isRunViaCLI()){
		echo $infoMsg."\n";
	} else {
		syslog(LOG_INFO, $infoMsg);
	}
	
	
255
	return Array($metadataIDProviders, $metadataSProviders);
haemmer's avatar
haemmer committed
256
257
258
}

/******************************************************************************/
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
// Is this script run in CLI mode
function isRunViaCLI(){
	return !isset($_SERVER['REMOTE_ADDR']);
}

/******************************************************************************/
// Is this script run in CLI mode
function isRunViaInclude(){
	return basename($_SERVER['SCRIPT_NAME']) != 'readMetadata.php';
}

/******************************************************************************/
// Processes an IDPRoleDescriptor XML node and returns an IDP entry or false if 
// something went wrong
function processIDPRoleDescriptor($IDPRoleDescriptorNode){
haemmer's avatar
haemmer committed
274
275
	global $defaultLanguage;
	
276
277
278
279
280
281
282
	$IDP = Array();
	
	// Get SSO URL
	$SSOServices = $IDPRoleDescriptorNode->getElementsByTagNameNS( 'urn:oasis:names:tc:SAML:2.0:metadata', 'SingleSignOnService' );
	foreach( $SSOServices as $SSOService ){
		if ($SSOService->getAttribute('Binding') == 'urn:mace:shibboleth:1.0:profiles:AuthnRequest'){
			$IDP['SSO'] =  $SSOService->getAttribute('Location');
283
284
285
286
			break;
		} else if ($SSOService->getAttribute('Binding') == 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'){
			$IDP['SSO'] =  $SSOService->getAttribute('Location');
			break;
287
288
289
290
		}
	}
	
	if (!isset($IDP['SSO'])){
291
		$IDP['SSO'] = 'https://no.saml1.or.saml2.sso.url.defined.com/error';
292
293
	}
	
294
295
296
297
298
299
300
301
	// First get MDUI name
	$MDUIDisplayNames = getMDUIDisplayNames($IDPRoleDescriptorNode);
	if (count($MDUIDisplayNames)){
		$IDP['Name'] = current($MDUIDisplayNames);
	}
	foreach ($MDUIDisplayNames as $lang => $value){
		$IDP[$lang]['Name'] = $value;
	}
302
	
303
304
305
306
	// Then try organization names 
	if (empty($IDP['Name'])){
		$OrgnizationNames = getOrganizationNames($IDPRoleDescriptorNode);
		$IDP['Name'] = current($OrgnizationNames);
haemmer's avatar
haemmer committed
307
		
308
309
		foreach ($OrgnizationNames as $lang => $value){
			$IDP[$lang]['Name'] = $value;
310
		}
311
312
313
314
	} 
	
	// As last resort, use entityID
	if (empty($IDP['Name'])){
315
316
317
		$IDP['Name'] = $IDPRoleDescriptorNode->parentNode->getAttribute('entityID');
	}
	
318
319
320
321
322
323
324
325
326
327
328
	// Set default name
	if (isset($IDP[$defaultLanguage])){
		$IDP['Name'] = $IDP[$defaultLanguage]['Name'];
	} elseif (isset($IDP['en'])){
		$IDP['Name'] = $IDP['en']['Name'];
	}
	
	// Get supported protocols
	$protocols = $IDPRoleDescriptorNode->getAttribute('protocolSupportEnumeration');
	$IDP['Protocols'] = $protocols;
	
329
330
331
332
333
334
	// Get keywords
	$MDUIKeywords = getMDUIKeywords($IDPRoleDescriptorNode);
	foreach ($MDUIKeywords as $lang => $keywords){
		$IDP[$lang]['Keywords'] = $keywords;
	}
	
335
336
337
338
339
340
341
	return $IDP;
}

/******************************************************************************/
// Processes an SPRoleDescriptor XML node and returns an SP entry or false if 
// something went wrong
function processSPRoleDescriptor($SPRoleDescriptorNode){
haemmer's avatar
haemmer committed
342
343
	global $defaultLanguage;

344
345
346
347
348
349
350
351
352
353
	$SP = Array();
	
	// Get <idpdisc:DiscoveryResponse> extensions
	$DResponses = $SPRoleDescriptorNode->getElementsByTagNameNS('urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol', 'DiscoveryResponse');
	foreach( $DResponses as $DResponse ){
		if ($DResponse->getAttribute('Binding') == 'urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol'){
			$SP['DSURL'][] =  $DResponse->getAttribute('Location');
		}
	}
	
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
	// First get MDUI name
	$MDUIDisplayNames = getMDUIDisplayNames($SPRoleDescriptorNode);
	if (count($MDUIDisplayNames)){
		$SP['Name'] = current($MDUIDisplayNames);
	}
	foreach ($MDUIDisplayNames as $lang => $value){
		$SP[$lang]['Name'] = $value;
	}
	
	// Then try attribute consuming service
	if (empty($SP['Name'])){
		$ConsumingServiceNames = getAttributeConsumingServiceNames($SPRoleDescriptorNode);
		$SP['Name'] = current($ConsumingServiceNames);
		
		foreach ($ConsumingServiceNames as $lang => $value){
			$SP[$lang]['Name'] = $value;
		}
	} 
	
	// As last resort, use entityID
	if (empty($SP['Name'])){
		$SP['Name'] = $SPRoleDescriptorNode->parentNode->getAttribute('entityID');
	}
	
	// Set default name
	if (isset($SP[$defaultLanguage])){
		$SP['Name'] = $SP[$defaultLanguage]['Name'];
	} elseif (isset($SP['en'])){
		$SP['Name'] = $SP['en']['Name'];
	}
	
385
386
387
388
389
390
	// Get Assertion Consumer Services and store their hostnames
	$ACServices = $SPRoleDescriptorNode->getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:metadata', 'AssertionConsumerService');
	foreach( $ACServices as $ACService ){
		$SP['ACURL'][] =  $ACService->getAttribute('Location');
	}
	
391
392
393
394
	// Get supported protocols
	$protocols = $SPRoleDescriptorNode->getAttribute('protocolSupportEnumeration');
	$SP['Protocols'] = $protocols;
	
395
396
397
398
399
400
	// Get keywords
	$MDUIKeywords = getMDUIKeywords($SPRoleDescriptorNode);
	foreach ($MDUIKeywords as $lang => $keywords){
		$SP[$lang]['Keywords'] = $keywords;
	}
	
401
402
403
404
405
406
407
408
	return $SP;
}

/******************************************************************************/
// Dump variable to a file 
function dumpFile($dumpFile, $providers, $variableName){
	 
	if(($fp = fopen($dumpFile, 'w')) !== false){
haemmer's avatar
haemmer committed
409
		
410
411
412
		fwrite($fp, "<?php\n\n");
		fwrite($fp, "// This file was automatically generated by readMetadata.php\n");
		fwrite($fp, "// Don't edit!\n\n");
haemmer's avatar
haemmer committed
413
		
414
415
416
417
418
		fwrite($fp, '$'.$variableName.' = ');
		fwrite($fp, var_export($providers,true));
		
		fwrite($fp, "\n?>");
			
419
		fclose($fp);
haemmer's avatar
haemmer committed
420
	} else {
haemmer's avatar
haemmer committed
421
422
423
424
425
426
		$errorMsg = 'Could not open file '.$dumpFile.' for writting';
		if (isRunViaCLI()){
			echo $errorMsg."\n";
		} else {
			syslog(LOG_ERR, $errorMsg);
		}
haemmer's avatar
haemmer committed
427
428
429
430
431
432
433
434
435
436
437
438
439
440
	}
}


/******************************************************************************/
// Function mergeInfo is used to create the effective $IDProviders array.
// For each IDP found in the metadata, merge the values from IDProvider.conf.php.
// If an IDP is found in IDProvider.conf as well as in metadata, use metadata  
// information if $SAML2MetaOverLocalConf is true or else use IDProvider.conf data
function mergeInfo($IDProviders, $metadataIDProviders, $SAML2MetaOverLocalConf, $includeLocalConfEntries){

	// If $includeLocalConfEntries parameter is set to true, mergeInfo() will also consider IDPs
	// not listed in metadataIDProviders but defined in IDProviders file
	// This is required if you need to add local exceptions over the federation metadata
441
442
	$allIDPS = $metadataIDProviders;
	$mergedArray = Array();
haemmer's avatar
haemmer committed
443
	if ($includeLocalConfEntries) {
444
		  $allIDPS = array_merge($metadataIDProviders, $IDProviders);
haemmer's avatar
haemmer committed
445
	}
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
	
	foreach ($allIDPS as $allIDPsKey => $allIDPsEntry){
		if(isset($IDProviders[$allIDPsKey])){
			// Entry exists also in local IDProviders.conf.php
			if (isset($metadataIDProviders[$allIDPsKey]) && is_array($metadataIDProviders[$allIDPsKey])) {
				
				// Remove IdP if there is a removal rule in local IDProviders.conf.php 
				if (!is_array($IDProviders[$allIDPsKey])){
					unset($metadataIDProviders[$allIDPsKey]);
					continue;
				}
				
				// Entry exists in both IDProviders sources and is an array
				if($SAML2MetaOverLocalConf){
					// Metadata entry overwrite local conf
					$mergedArray[$allIDPsKey] = array_merge($IDProviders[$allIDPsKey], $metadataIDProviders[$allIDPsKey]);
				} else {
					// Local conf overwrites metada entry
					$mergedArray[$allIDPsKey] = array_merge($metadataIDProviders[$allIDPsKey], $IDProviders[$allIDPsKey]);
				}
			  } else {
					// Entry only exists in local IDProviders file
					$mergedArray[$allIDPsKey] = $IDProviders[$allIDPsKey];
			  }
haemmer's avatar
haemmer committed
470
		} else {
471
472
			// Entry doesnt exist in in local IDProviders.conf.php
			$mergedArray[$allIDPsKey] = $metadataIDProviders[$allIDPsKey];
haemmer's avatar
haemmer committed
473
474
475
476
477
478
		}
	}
	
	return $mergedArray;
}

479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
/******************************************************************************/
// Get MD Display Names from RoleDescriptor
function getMDUIDisplayNames($RoleDescriptorNode){
	
	$Entity = Array();
	
	$MDUIDisplayNames = $RoleDescriptorNode->getElementsByTagNameNS('urn:oasis:names:tc:SAML:metadata:ui', 'DisplayName');
	foreach( $MDUIDisplayNames as $MDUIDisplayName ){
		$lang = $MDUIDisplayName->getAttributeNodeNS('http://www.w3.org/XML/1998/namespace', 'lang')->nodeValue;
		$Entity[$lang] = $MDUIDisplayName->nodeValue;
	}
	
	return $Entity;
}

494
495
496
497
498
499
500
501
502
503
504
505
506
507
/******************************************************************************/
// Get MD Keywords from RoleDescriptor
function getMDUIKeywords($RoleDescriptorNode){
	
	$Entity = Array();
	
	$MDUIKeywords = $RoleDescriptorNode->getElementsByTagNameNS('urn:oasis:names:tc:SAML:metadata:ui', 'Keywords');
	foreach( $MDUIKeywords as $MDUIKeywordEntry ){
		$lang = $MDUIKeywordEntry->getAttributeNodeNS('http://www.w3.org/XML/1998/namespace', 'lang')->nodeValue;
		$Entity[$lang] = $MDUIKeywordEntry->nodeValue;
	}
	
	return $Entity;
}
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
/******************************************************************************/
// Get Organization Names from RoleDescriptor
function getOrganizationNames($RoleDescriptorNode){
	
	$Entity = Array();
	
	$Orgnization = $RoleDescriptorNode->parentNode->getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:metadata', 'Organization' )->item(0);
	if ($Orgnization){
		$DisplayNames = $Orgnization->getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:metadata', 'OrganizationDisplayName');
		foreach ($DisplayNames as $DisplayName){
			$lang = $DisplayName->getAttributeNodeNS('http://www.w3.org/XML/1998/namespace', 'lang')->nodeValue;
			$Entity[$lang] = $DisplayName->nodeValue;
		}
	}
	
	return $Entity;
}


/******************************************************************************/
// Get Organization Names from RoleDescriptor
function getAttributeConsumingServiceNames($RoleDescriptorNode){
	
	$Entity = Array();
	
533
534
535
536
	$ServiceNames = $RoleDescriptorNode->getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:metadata', 'ServiceName' );
	foreach ($ServiceNames as $ServiceName){
		$lang = $ServiceName->getAttributeNodeNS('http://www.w3.org/XML/1998/namespace', 'lang')->nodeValue;
		$Entity[$lang] = $ServiceName->nodeValue;
537
538
539
540
541
	}
	
	return $Entity;
}

haemmer's avatar
haemmer committed
542
?>