From e8520be90c3d8ff000e5bcd2c4eb7ab83cf28441 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20=C3=85kre=20Solberg?= <andreas.solberg@uninett.no>
Date: Thu, 17 Jan 2008 11:06:59 +0000
Subject: [PATCH] Added the possibility for the logger to get the trackID it
 self... Temporary thing, we shuold rewrite and improve the logging class a
 bit.

git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@165 44740490-163a-0410-bde0-09ae8108e29a
---
 lib/SimpleSAML/Logger.php                     |   9 +-
 .../MetaDataStorageHandlerSAML2Meta.php       | 205 +++++++++++++++---
 lib/SimpleSAML/XML/Parser.php                 |   9 +
 templates/default/en/admin-metadatalist.php   |   9 +
 www/admin/metadata.php                        |  53 +++++
 5 files changed, 256 insertions(+), 29 deletions(-)

diff --git a/lib/SimpleSAML/Logger.php b/lib/SimpleSAML/Logger.php
index 7b2daad00..d6392d798 100644
--- a/lib/SimpleSAML/Logger.php
+++ b/lib/SimpleSAML/Logger.php
@@ -9,7 +9,7 @@
  */
 
 require_once('SimpleSAML/Configuration.php');
-
+require_once('SimpleSAML/Session.php');
 
 /**
  * A logger class.
@@ -33,9 +33,14 @@ class SimpleSAML_Logger {
 	/*
 	 * Log a message to syslog.
 	 */
-	public function log($priority, $trackid, $module, $submodule, $eventtype, $content, $message) {
+	public function log($priority, $trackid = null, $module, $submodule, $eventtype, $content, $message) {
 		if ($priority < $this->loglevel) return;
 		
+		if ($trackid == null) {
+			$session = SimpleSAML_Session::getInstance(true);
+			$trackid = $session->getTrackID();
+		}
+		
 		$contentstring = '';
 		if (is_array($content)) {
 			$contentstring = implode('|', $content); 
diff --git a/lib/SimpleSAML/Metadata/MetaDataStorageHandlerSAML2Meta.php b/lib/SimpleSAML/Metadata/MetaDataStorageHandlerSAML2Meta.php
index 018a368f6..f5376c440 100644
--- a/lib/SimpleSAML/Metadata/MetaDataStorageHandlerSAML2Meta.php
+++ b/lib/SimpleSAML/Metadata/MetaDataStorageHandlerSAML2Meta.php
@@ -13,6 +13,7 @@ require_once('SimpleSAML/Configuration.php');
 require_once('SimpleSAML/Utilities.php');
 require_once('SimpleSAML/XML/Parser.php');
 require_once('SimpleSAML/Metadata/MetaDataStorageHandler.php');
+require_once('SimpleSAML/Logger.php');
 
 /**
  * Configuration of SimpleSAMLphp
@@ -21,6 +22,7 @@ class SimpleSAML_Metadata_MetaDataStorageHandlerSAML2Meta extends SimpleSAML_Met
 
 
 	private static $cachedfiles;
+	private $logger;
 
 	/* This constructor is included in case it is needed in the the
 	 * future. Including it now allows us to write parent::__construct() in
@@ -28,6 +30,8 @@ class SimpleSAML_Metadata_MetaDataStorageHandlerSAML2Meta extends SimpleSAML_Met
 	 */
 	protected function __construct() {
 		if (!isset($this->cachedfiles)) $this->cachedfiles = array();
+		$this->logger = new SimpleSAML_Logger();
+
 	}
 
 
@@ -54,36 +58,61 @@ class SimpleSAML_Metadata_MetaDataStorageHandlerSAML2Meta extends SimpleSAML_Met
 		$config = SimpleSAML_Configuration::getInstance();
 		assert($config instanceof SimpleSAML_Configuration);
 
+
+		$metadatalocations = $config->getValue('metadata.locations');
 		
-		$metadatasetfile = $config->getBaseDir() . '' . 
-			$config->getValue('metadatadir') . 'xml/' . $settofile[$set] . '.xml';
+		if (!is_array($metadatalocations)) throw new Exception('Could not find config parameter: metadata.locations in config.php');
+		if (!array_key_exists($set, $metadatalocations)) throw new Exception('Could not find metadata location for this set: ' . $set);
+		$metadatalocation = $metadatalocations[$set];
 		
-		if (array_key_exists($metadatasetfile, $this->cachedfiles)) {
-			$metadataxml = self::$cachedfiles[$metadatasetfile];
+		$xml = true;
+		if (preg_match('@^http(s)?://@i', $metadatalocation)) {
+			// The metadata location is an URL
+			$metadatasetfile = $metadatalocation;
 		} else {
+			$metadatasetfile = $config->getBaseDir() . '' . 
+				$config->getValue('metadatadir') . $metadatalocation;
 			if (!file_exists($metadatasetfile)) throw new Exception('Could not find SAML 2.0 Metadata file :'. $metadatasetfile);
-		
-			// for now testing with the shib aai metadata...
-			//$metadataxml = file_get_contents("http://www.switch.ch/aai/federation/SWITCHaai/metadata.switchaai_signed.xml");
-			$metadataxml = file_get_contents($metadatasetfile);
-			self::$cachedfiles[$metadatasetfile] = $metadataxml;
+			if (preg_match('@\.php$@', $metadatalocation)) {
+				$xml = false;
+			}
 		}
 		
-		$metadata = null;
-		switch ($set) {
-			case 'saml20-idp-remote' : $metadata = $this->getmetadata_saml20idpremote($metadataxml); break;
-			case 'saml20-idp-hosted' : $metadata = $this->getmetadata_saml20idphosted($metadataxml); break;
-			case 'saml20-sp-remote' : $metadata = $this->getmetadata_saml20spremote($metadataxml); break; 
-			case 'saml20-sp-hosted' : $metadata = $this->getmetadata_saml20sphosted($metadataxml); break; 
-			case 'shib13-idp-remote' : $metadata = $this->getmetadata_shib13idpremote($metadataxml); break;
-			case 'shib13-idp-hosted' : throw new Exception('Meta data parsing for Shib 1.3 IdP Hosted not yet implemented.');
-			case 'shib13-sp-remote' : throw new Exception('Meta data parsing for Shib 1.3 SP Remote not yet implemented.');
-			case 'shib13-sp-hosted' : throw new Exception('Meta data parsing for Shib 1.3 SP Hosted not yet implemented.');
+		
+		if ($xml) {
+		
+			if (array_key_exists($metadatasetfile, $this->cachedfiles)) {
+				$metadataxml = self::$cachedfiles[$metadatasetfile];
+			} else {		
+				$metadataxml = file_get_contents($metadatasetfile);
+				self::$cachedfiles[$metadatasetfile] = $metadataxml;
+			}
+			/*
+			echo '<pre>content:'; print_r($metadataxml); echo '</pre>';
+			echo '<p>file[' . $metadatasetfile. ']';
+			*/
+			$metadata = null;
+			switch ($set) {
+				case 'saml20-idp-remote' : $metadata = $this->getmetadata_saml20idpremote($metadataxml); break;
+				case 'saml20-idp-hosted' : $metadata = $this->getmetadata_saml20idphosted($metadataxml); break;
+				case 'saml20-sp-remote' : $metadata = $this->getmetadata_saml20spremote($metadataxml); break; 
+				case 'saml20-sp-hosted' : $metadata = $this->getmetadata_saml20sphosted($metadataxml); break; 
+				case 'shib13-idp-remote' : $metadata = $this->getmetadata_shib13idpremote($metadataxml); break;
+				case 'shib13-idp-hosted' : throw new Exception('Not implemented SAML 2.0 XML metadata for Shib 1.3 IdP Hosted, use files instead.'); break;
+				case 'shib13-sp-remote' : $metadata = $this->getmetadata_shib13spremote($metadataxml); break;
+				case 'shib13-sp-hosted' : throw new Exception('Not implemented SAML 2.0 XML metadata for Shib 1.3 SP Hosted, use files instead.'); break;
+			}
+			
+		} else {
+		
+			$metadata = $this->loadFile($metadatasetfile);
 		}
 		
-		if (!is_array($metadata)) {
+		$this->logger->log(LOG_INFO, null, 'MetaData', 'Handler.SAML2Meta', 'INFO', 'Loading', 
+			'Loading metadata set [' . $set . '] from [' . $metadatasetfile . ']' );
+		
+		if (!is_array($metadata))
 			throw new Exception('Could not load metadata set [' . $set . '] from file: ' . $metadatasetfile);
-		}
 		/*
 		echo '<pre>';
 		print_r($metadata);
@@ -98,6 +127,21 @@ class SimpleSAML_Metadata_MetaDataStorageHandlerSAML2Meta extends SimpleSAML_Met
 				$this->hostmap[$set][$entry['host']] = $key;
 			}
 		}
+		
+
+	}
+	
+	private function loadFile($metadatasetfile) {
+		$metadata = null;
+		if (!file_exists($metadatasetfile)) {
+			throw new Exception('Could not open file: ' . $metadatasetfile);
+		}
+		include($metadatasetfile);
+		
+		if (!is_array($metadata)) {
+			throw new Exception('(SAML2Metastoragehandler:loadFile)Could not load metadata set [' . $set . '] from file: ' . $metadatasetfile);
+		}
+		return $metadata;
 
 	}
 	
@@ -141,7 +185,8 @@ class SimpleSAML_Metadata_MetaDataStorageHandlerSAML2Meta extends SimpleSAML_Met
 												
 
 			} catch (Exception $e) {
-				echo 'Error reading one metadata entry: ' . $e;
+				$this->logger->log(LOG_INFO, null, 'MetaData', 'Handler.SAML2Meta', 'WARNING', 'Parsing', 
+					'Error parsing [' . __FUNCTION__ . '] ' . $e->getMessage() );
 			}
 
 		}
@@ -178,7 +223,8 @@ class SimpleSAML_Metadata_MetaDataStorageHandlerSAML2Meta extends SimpleSAML_Met
 				$metadata[$entityid]['ForceAuthn'] = (isset($seek_forceauth) ? ($seek_forceauth === 'true') : false);
 				
 			} catch (Exception $e) {
-				echo 'Error reading one metadata entry: ' . $e;
+				$this->logger->log(LOG_INFO, null, 'MetaData', 'Handler.SAML2Meta', 'WARNING', 'Parsing', 
+					'Error parsing [' . __FUNCTION__ . '] ' . $e->getMessage() );
 			}
 
 		}
@@ -223,8 +269,8 @@ class SimpleSAML_Metadata_MetaDataStorageHandlerSAML2Meta extends SimpleSAML_Met
 				$metadata[$entityid]['requireconsent'] = (isset($seek_requireconsent) ? ($seek_requireconsent === 'true') : false);
 				
 			} catch (Exception $e) {
-				// TODO: do syslog, not echo.
-				echo 'Error reading one metadata entry: ' . $e;
+				$this->logger->log(LOG_INFO, null, 'MetaData', 'Handler.SAML2Meta', 'WARNING', 'Parsing', 
+					'Error parsing [' . __FUNCTION__ . '] ' . $e->getMessage() );
 			}
 
 		}
@@ -289,7 +335,8 @@ class SimpleSAML_Metadata_MetaDataStorageHandlerSAML2Meta extends SimpleSAML_Met
 				
 				
 			} catch (Exception $e) {
-				echo 'Error reading one metadata entry: ' . $e;
+				$this->logger->log(LOG_INFO, null, 'MetaData', 'Handler.SAML2Meta', 'WARNING', 'Parsing', 
+					'Error parsing [' . __FUNCTION__ . '] ' . $e->getMessage() );
 			}
 
 		}
@@ -319,15 +366,119 @@ class SimpleSAML_Metadata_MetaDataStorageHandlerSAML2Meta extends SimpleSAML_Met
 				$metadata_entry = SimpleSAML_XML_Parser::fromSimpleXMLElement($idpentity);
 				
 				$metadata[$entityid]['SingleSignOnService'] = $metadata_entry->getValue("/saml2meta:EntityDescriptor/saml2meta:IDPSSODescriptor/saml2meta:SingleSignOnService[@Binding='urn:mace:shibboleth:1.0:profiles:AuthnRequest']/@Location", true);
+				
+				$metadata[$entityid]['certFingerprint'] = SimpleSAML_Utilities::cert_fingerprint($metadata_entry->getValue("/saml2meta:EntityDescriptor/saml2meta:IDPSSODescriptor/saml2meta:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate", true));
+				
+				$seek_base64 = $metadata_entry->getValue("/saml2meta:EntityDescriptor/saml2meta:IDPSSODescriptor/saml2meta:Extensions/saml2:Attribute[@Name='urn:mace:feide.no:simplesamlphp:base64attributes']/saml2:AttributeValue");
+				$metadata[$entityid]['base64attributes'] = (isset($seek_base64) ? ($seek_base64 === 'true') : false);
+				
+				
+				$metadata[$entityid]['name'] = $metadata_entry->getValueAlternatives(
+					array("/saml2meta:EntityDescriptor/saml2meta:IDPSSODescriptor/saml2meta:Extensions/saml2:Attribute[@Name='urn:mace:feide.no:simplesamlphp:name']/saml2:AttributeValue",
+					"/saml2meta:EntityDescriptor/saml2meta:IDPSSODescriptor/saml2meta:Organization/saml2meta:OrganizationDisplayName"
+					));
+					
+				$metadata[$entityid]['description'] = $metadata_entry->getValue("/saml2meta:EntityDescriptor/saml2meta:IDPSSODescriptor/saml2meta:Extensions/saml2:Attribute[@Name='urn:mace:feide.no:simplesamlphp:description']/saml2:AttributeValue");
+												
 
 			} catch (Exception $e) {
-				echo 'Error reading one metadata entry: ' . $e;
+				$this->logger->log(LOG_INFO, null, 'MetaData', 'Handler.SAML2Meta', 'WARNING', 'Parsing', 
+					'Error parsing [' . __FUNCTION__ . '] ' . $e->getMessage() );
 			}
 
 		}
 		return $metadata;
 	}
 	
+	
+	/*
+		<EntityDescriptor entityID="https://tim-test.ethz.ch/shibboleth">
+		<SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol">
+			<KeyDescriptor use="signing">
+				<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+					<ds:KeyName>tim-test.ethz.ch</ds:KeyName>
+				</ds:KeyInfo>
+			</KeyDescriptor>
+			<NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
+
+			<AssertionConsumerService Binding="urn:oasis:names:tc:SAML:1.0:profiles:browser-post"
+				Location="https://tim-test.ethz.ch/Shibboleth.shire" index="1" isDefault="true"/>
+
+		</SPSSODescriptor>
+
+
+	</EntityDescriptor>
+	*/
+	private function getmetadata_shib13spremote($metadataxml) {
+		// Create a parser for the metadata document.
+		$metadata_parser = new SimpleSAML_XML_Parser($metadataxml);
+		
+		// Get all entries in the metadata.
+		$idpentities = $metadata_parser->simplexml->xpath('/saml2meta:EntitiesDescriptor/saml2meta:EntityDescriptor[./saml2meta:SPSSODescriptor]');
+		if (!$idpentities) throw new Exception('Could not find any entity descriptors in the meta data file: ' . $metadatasetfile);
+		
+		// Array to hold the resulting metadata, to return at the end of this function.
+		$metadata = array();
+		
+		// Traverse all entries.
+		foreach ($idpentities as $idpentity) {
+			try {
+				$entityid = (string) $idpentity['entityID'];
+				if (!$entityid) throw new Exception('Could not find entityID in element');
+				
+				
+				/*
+					array('entityid', 'spNameQualifier', 'AssertionConsumerService', 'SingleLogoutService', 'NameIDFormat'),
+					array('base64attributes', 'attributemap', 'simplesaml.attributes', 'attributes')
+				*/
+				
+				$metadata[$entityid] = array('entityid' => $entityid);				
+				$metadata_entry = SimpleSAML_XML_Parser::fromSimpleXMLElement($idpentity);
+
+				$metadata[$entityid]['spNameQualifier'] = $metadata_entry->getValueDefault("/saml2meta:EntityDescriptor/saml2meta:SPSSODescriptor/saml2meta:Extensions/saml2:Attribute[@Name='urn:mace:feide.no:simplesamlphp:spnamequalifier']/saml2:AttributeValue", $metadata[$entityid]['entityid']);
+
+				$metadata[$entityid]['audience'] = $metadata_entry->getValueDefault("/saml2meta:EntityDescriptor/saml2meta:SPSSODescriptor/saml2meta:Extensions/saml2:Attribute[@Name='urn:mace:feide.no:simplesamlphp:audience']/saml2:AttributeValue", $metadata[$entityid]['entityid']);
+
+				
+				$metadata[$entityid]['NameIDFormat'] = $metadata_entry->getValueDefault("/saml2meta:EntityDescriptor/saml2meta:SPSSODescriptor/saml2meta:NameIDFormat", 
+					'urn:mace:shibboleth:1.0:nameIdentifier');
+				
+				$seek_base64 = $metadata_entry->getValue("/saml2meta:EntityDescriptor/saml2meta:SPSSODescriptor/saml2meta:Extensions/saml2:Attribute[@Name='urn:mace:feide.no:simplesamlphp:base64attributes']/saml2:AttributeValue");
+				$metadata[$entityid]['base64attributes'] = (isset($seek_base64) ? ($seek_base64 === 'true') : false);
+				
+				
+				$metadata[$entityid]['name'] = $metadata_entry->getValueAlternatives(
+					array("/saml2meta:EntityDescriptor/saml2meta:SPSSODescriptor/saml2meta:Extensions/saml2:Attribute[@Name='urn:mace:feide.no:simplesamlphp:name']/saml2:AttributeValue",
+					"/saml2meta:EntityDescriptor/saml2meta:SPSSODescriptor/saml2meta:Organization/saml2meta:OrganizationDisplayName"
+					));
+					
+				$metadata[$entityid]['description'] = $metadata_entry->getValue("/saml2meta:EntityDescriptor/saml2meta:SPSSODescriptor/saml2meta:Extensions/saml2:Attribute[@Name='urn:mace:feide.no:simplesamlphp:description']/saml2:AttributeValue");
+
+				$metadata[$entityid]['simplesaml.attributes'] = $metadata_entry->getValue("/saml2meta:EntityDescriptor/saml2meta:SPSSODescriptor/saml2meta:Extensions/saml2:Attribute[@Name='urn:mace:feide.no:simplesamlphp:simplesaml.attributes']/saml2:AttributeValue");
+				
+				
+				$seek_attributes = $metadata_entry->getValue("/saml2meta:EntityDescriptor/saml2meta:SPSSODescriptor/saml2meta:Extensions/saml2:Attribute[@Name='urn:mace:feide.no:simplesamlphp:attributes']/saml2:AttributeValue");
+				if (isset($seek_attributes)) $metadata[$entityid]['attributes'] = explode(',', $seek_attributes);
+				
+				$metadata[$entityid]['attributemap'] = $metadata_entry->getValue("/saml2meta:EntityDescriptor/saml2meta:SPSSODescriptor/saml2meta:Extensions/saml2:Attribute[@Name='urn:mace:feide.no:simplesamlphp:attributemap']/saml2:AttributeValue");
+				
+				$metadata[$entityid]['AssertionConsumerService'] = $metadata_entry->getValue("/saml2meta:EntityDescriptor/saml2meta:SPSSODescriptor/saml2meta:AssertionConsumerService/@Location", true);
+				
+				
+				
+			} catch (Exception $e) {
+				$this->logger->log(LOG_INFO, null, 'MetaData', 'Handler.SAML2Meta', 'WARNING', 'Parsing', 
+					'Error parsing [' . __FUNCTION__ . '] ' . $e->getMessage() );
+			}
+
+		}
+		return $metadata;
+	}
+	
+	
+
+	
+	
 	public function getMetaData($entityid = null, $set = 'saml20-sp-hosted') {
 		if (!isset($entityid)) {
 			return $this->getMetaDataCurrent($set);
diff --git a/lib/SimpleSAML/XML/Parser.php b/lib/SimpleSAML/XML/Parser.php
index f5e88866f..82ce7ada1 100644
--- a/lib/SimpleSAML/XML/Parser.php
+++ b/lib/SimpleSAML/XML/Parser.php
@@ -17,6 +17,7 @@
 class SimpleSAML_XML_Parser  {
 
 	var $simplexml = null;
+
 	
 	function __construct($xml) {
 		#parent::construct($xml);
@@ -44,6 +45,14 @@ class SimpleSAML_XML_Parser  {
 		
 	}
 	
+	public function getValueDefault($xpath, $defvalue) {
+		try {
+			return $this->getValue($xpath, true);
+		} catch (Exception $e) {
+			return $defvalue;
+		}
+	}
+	
 	public function getValue($xpath, $required = false) {
 		
 		$result = $this->simplexml->xpath($xpath);
diff --git a/templates/default/en/admin-metadatalist.php b/templates/default/en/admin-metadatalist.php
index 22fd84de3..9dec31ca1 100644
--- a/templates/default/en/admin-metadatalist.php
+++ b/templates/default/en/admin-metadatalist.php
@@ -84,6 +84,15 @@
 		if (array_key_exists('metadata.saml20-idp-remote', $data)) 
 			showEntry('SAML 2.0 Identity Provider (Remote)', $data['metadata.saml20-idp-remote']);
 
+		if (array_key_exists('metadata.shib13-sp-hosted', $data)) 
+			showEntry('Shib 1.3 Service Provider (Hosted)', $data['metadata.shib13-sp-hosted']);
+		if (array_key_exists('metadata.shib13-sp-remote', $data)) 
+			showEntry('Shib 1.3 Service Provider (Remote)', $data['metadata.shib13-sp-remote']);
+		if (array_key_exists('metadata.shib13-idp-hosted', $data)) 
+			showEntry('Shib 1.3 Identity Provider (Hosted)', $data['metadata.shib13-idp-hosted']);
+		if (array_key_exists('metadata.shib13-idp-remote', $data)) 
+			showEntry('Shib 1.3 Identity Provider (Remote)', $data['metadata.shib13-idp-remote']);
+
 		
 		?>
 
diff --git a/www/admin/metadata.php b/www/admin/metadata.php
index 56491ee8c..301aacb28 100644
--- a/www/admin/metadata.php
+++ b/www/admin/metadata.php
@@ -65,6 +65,59 @@ try {
 		
 	}
 
+
+
+
+	if ($config->getValue('enable.shib13-sp') === true) {
+		$results = array();	
+
+		$metalist = $metadata->getList('shib13-sp-hosted');
+		foreach ($metalist AS $entityid => $mentry) {
+			$results[$entityid] = SimpleSAML_Utilities::checkAssocArrayRules($mentry,
+				array('entityid', 'host', 'NameIDFormat', 'ForceAuthn'),
+				array()
+			);
+		}
+		$et->data['metadata.shib13-sp-hosted'] = $results;
+
+		$results = array();	
+		$metalist = $metadata->getList('shib13-idp-remote');
+		foreach ($metalist AS $entityid => $mentry) {
+			$results[$entityid] = SimpleSAML_Utilities::checkAssocArrayRules($mentry,
+				array('entityid', 'SingleSignOnService', 'SingleLogoutService', 'certFingerprint'),
+				array('name', 'description', 'base64attributes')
+			);
+		}
+		$et->data['metadata.shib13-idp-remote'] = $results;
+		
+	}
+	
+	if ($config->getValue('enable.shib13-idp') === true) {
+		$results = array();	
+		$metalist = $metadata->getList('shib13-idp-hosted');
+		foreach ($metalist AS $entityid => $mentry) {
+			$results[$entityid] = SimpleSAML_Utilities::checkAssocArrayRules($mentry,
+				array('entityid', 'host', 'privatekey', 'certificate', 'auth'),
+				array('requireconsent')
+			);
+		}
+		$et->data['metadata.shib13-idp-hosted'] = $results;
+		
+		$results = array();	
+		$metalist = $metadata->getList('shib13-sp-remote');
+		foreach ($metalist AS $entityid => $mentry) {
+			$results[$entityid] = SimpleSAML_Utilities::checkAssocArrayRules($mentry,
+				array('entityid', 'spNameQualifier', 'AssertionConsumerService', 'audience', 'NameIDFormat'),
+				array('base64attributes', 'attributemap', 'simplesaml.attributes', 'attributes', 'name', 'description')
+			);
+		}
+		$et->data['metadata.shib13-sp-remote'] = $results;
+		
+	}
+
+
+
+
 	
 
 	$et->data['header'] = 'Metadata overview';
-- 
GitLab